diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index 8a95baf3cb..dea8758140 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -48,6 +48,7 @@ import org.matrix.android.sdk.api.session.signout.SignOutService import org.matrix.android.sdk.api.session.sync.FilterService import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.session.terms.TermsService +import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService import org.matrix.android.sdk.api.session.typing.TypingUsersTracker import org.matrix.android.sdk.api.session.user.UserService import org.matrix.android.sdk.api.session.widgets.WidgetService @@ -212,6 +213,11 @@ interface Session : */ fun searchService(): SearchService + /** + * Returns the third party service associated with the session + */ + fun thirdPartyService(): ThirdPartyService + /** * Add a listener to the session. * @param listener the listener to add. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt index 61970ce848..55f3b76760 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt @@ -20,7 +20,6 @@ import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse -import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.util.Cancelable /** @@ -35,12 +34,6 @@ interface RoomDirectoryService { publicRoomsParams: PublicRoomsParams, callback: MatrixCallback): Cancelable - /** - * Fetches the overall metadata about protocols supported by the homeserver. - * Includes both the available protocols and all fields required for queries against each protocol. - */ - fun getThirdPartyProtocol(callback: MatrixCallback>): Cancelable - /** * Get the visibility of a room in the directory */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt new file mode 100644 index 0000000000..2ae4562b0b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/ThirdPartyService.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.thirdparty + +import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol +import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser + +/** + * See https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-thirdparty-protocols + */ +interface ThirdPartyService { + + /** + * Fetches the overall metadata about protocols supported by the homeserver. + * Includes both the available protocols and all fields required for queries against each protocol. + */ + suspend fun getThirdPartyProtocols(): Map + + /** + * Retrieve a Matrix User ID linked to a user on the third party service, given a set of user parameters. + * @param protocol Required. The name of the protocol. + * @param fields One or more custom fields that are passed to the AS to help identify the user. + */ + suspend fun getThirdPartyUser(protocol: String, fields: Map = emptyMap()): List +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/model/ThirdPartyUser.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/model/ThirdPartyUser.kt new file mode 100644 index 0000000000..d77dfcfe35 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/thirdparty/model/ThirdPartyUser.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.api.session.thirdparty.model + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.util.JsonDict + +@JsonClass(generateAdapter = true) +data class ThirdPartyUser( + /* + Required. A Matrix User ID represting a third party user. + */ + @Json(name = "userid") val userId: String, + /* + Required. The protocol ID that the third party location is a part of. + */ + @Json(name = "protocol") val protocol: String, + /* + Required. Information used to identify this third party location. + */ + @Json(name = "fields") val fields: JsonDict +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt index fa07b16c32..1ae4c0e5ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/DefaultSession.kt @@ -50,6 +50,7 @@ import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageServi import org.matrix.android.sdk.api.session.signout.SignOutService import org.matrix.android.sdk.api.session.sync.FilterService import org.matrix.android.sdk.api.session.terms.TermsService +import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService import org.matrix.android.sdk.api.session.typing.TypingUsersTracker import org.matrix.android.sdk.api.session.user.UserService import org.matrix.android.sdk.api.session.widgets.WidgetService @@ -113,6 +114,7 @@ internal class DefaultSession @Inject constructor( private val accountService: Lazy, private val defaultIdentityService: DefaultIdentityService, private val integrationManagerService: IntegrationManagerService, + private val thirdPartyService: Lazy, private val callSignalingService: Lazy, @UnauthenticatedWithCertificate private val unauthenticatedWithCertificateOkHttpClient: Lazy, @@ -257,6 +259,8 @@ internal class DefaultSession @Inject constructor( override fun searchService(): SearchService = searchService.get() + override fun thirdPartyService(): ThirdPartyService = thirdPartyService.get() + override fun getOkHttpClient(): OkHttpClient { return unauthenticatedWithCertificateOkHttpClient.get() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt index 659fcc8f5c..8411950171 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/SessionComponent.kt @@ -57,6 +57,7 @@ import org.matrix.android.sdk.internal.session.sync.SyncTask import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.session.sync.job.SyncWorker import org.matrix.android.sdk.internal.session.terms.TermsModule +import org.matrix.android.sdk.internal.session.thirdparty.ThirdPartyModule import org.matrix.android.sdk.internal.session.user.UserModule import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataModule import org.matrix.android.sdk.internal.session.widgets.WidgetModule @@ -89,7 +90,8 @@ import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers SessionAssistedInjectModule::class, AccountModule::class, CallModule::class, - SearchModule::class + SearchModule::class, + ThirdPartyModule::class ] ) @SessionScope diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt index 0d41c6f35e..f9047fdf3c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt @@ -21,11 +21,9 @@ import org.matrix.android.sdk.api.session.room.RoomDirectoryService import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse -import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask -import org.matrix.android.sdk.internal.session.room.directory.GetThirdPartyProtocolsTask import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith @@ -33,7 +31,6 @@ import javax.inject.Inject internal class DefaultRoomDirectoryService @Inject constructor( private val getPublicRoomTask: GetPublicRoomTask, - private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask, private val getRoomDirectoryVisibilityTask: GetRoomDirectoryVisibilityTask, private val setRoomDirectoryVisibilityTask: SetRoomDirectoryVisibilityTask, private val taskExecutor: TaskExecutor) : RoomDirectoryService { @@ -48,14 +45,6 @@ internal class DefaultRoomDirectoryService @Inject constructor( .executeBy(taskExecutor) } - override fun getThirdPartyProtocol(callback: MatrixCallback>): Cancelable { - return getThirdPartyProtocolsTask - .configureWith { - this.callback = callback - } - .executeBy(taskExecutor) - } - override suspend fun getRoomDirectoryVisibility(roomId: String): RoomDirectoryVisibility { return getRoomDirectoryVisibilityTask.execute(GetRoomDirectoryVisibilityTask.Params(roomId)) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt index aa92c1cb3b..20cb49ee8a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt @@ -20,7 +20,6 @@ import org.matrix.android.sdk.api.session.events.model.Content import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse -import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.network.NetworkConstants import org.matrix.android.sdk.internal.session.room.alias.GetAliasesResponse @@ -50,14 +49,6 @@ import retrofit2.http.Query internal interface RoomAPI { - /** - * Get the third party server protocols. - * - * Ref: https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-thirdparty-protocols - */ - @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols") - fun thirdPartyProtocols(): Call> - /** * Lists the public rooms on the server, with optional filter. * This API returns paginated responses. The rooms are ordered by the number of joined members, with the largest rooms first. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 92f4ea2aea..66b7272360 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -39,11 +39,9 @@ import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask import org.matrix.android.sdk.internal.session.room.create.DefaultCreateRoomTask import org.matrix.android.sdk.internal.session.room.directory.DefaultGetPublicRoomTask import org.matrix.android.sdk.internal.session.room.directory.DefaultGetRoomDirectoryVisibilityTask -import org.matrix.android.sdk.internal.session.room.directory.DefaultGetThirdPartyProtocolsTask import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask -import org.matrix.android.sdk.internal.session.room.directory.GetThirdPartyProtocolsTask import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.membership.DefaultLoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask @@ -153,9 +151,6 @@ internal abstract class RoomModule { @Binds abstract fun bindSetRoomDirectoryVisibilityTask(task: DefaultSetRoomDirectoryVisibilityTask): SetRoomDirectoryVisibilityTask - @Binds - abstract fun bindGetThirdPartyProtocolsTask(task: DefaultGetThirdPartyProtocolsTask): GetThirdPartyProtocolsTask - @Binds abstract fun bindInviteTask(task: DefaultInviteTask): InviteTask diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/DefaultThirdPartyService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/DefaultThirdPartyService.kt new file mode 100644 index 0000000000..13829c400a --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/DefaultThirdPartyService.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.thirdparty + +import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol +import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService +import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser +import javax.inject.Inject + +internal class DefaultThirdPartyService @Inject constructor(private val getThirdPartyProtocolTask: GetThirdPartyProtocolsTask, + private val getThirdPartyUserTask: GetThirdPartyUserTask) + : ThirdPartyService { + + override suspend fun getThirdPartyProtocols(): Map { + return getThirdPartyProtocolTask.execute(Unit) + } + + override suspend fun getThirdPartyUser(protocol: String, fields: Map): List { + val taskParams = GetThirdPartyUserTask.Params( + protocol = protocol, + fields = fields + ) + return getThirdPartyUserTask.execute(taskParams) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetThirdPartyProtocolsTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyProtocolsTask.kt similarity index 86% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetThirdPartyProtocolsTask.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyProtocolsTask.kt index 3477aa671e..fd1ed741e9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/directory/GetThirdPartyProtocolsTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyProtocolsTask.kt @@ -14,25 +14,24 @@ * limitations under the License. */ -package org.matrix.android.sdk.internal.session.room.directory +package org.matrix.android.sdk.internal.session.thirdparty import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.executeRequest -import org.matrix.android.sdk.internal.session.room.RoomAPI import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject internal interface GetThirdPartyProtocolsTask : Task> internal class DefaultGetThirdPartyProtocolsTask @Inject constructor( - private val roomAPI: RoomAPI, + private val thirdPartyAPI: ThirdPartyAPI, private val globalErrorReceiver: GlobalErrorReceiver ) : GetThirdPartyProtocolsTask { override suspend fun execute(params: Unit): Map { return executeRequest(globalErrorReceiver) { - apiCall = roomAPI.thirdPartyProtocols() + apiCall = thirdPartyAPI.thirdPartyProtocols() } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyUserTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyUserTask.kt new file mode 100644 index 0000000000..01a8b57678 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/GetThirdPartyUserTask.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.thirdparty + +import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser +import org.matrix.android.sdk.internal.network.GlobalErrorReceiver +import org.matrix.android.sdk.internal.network.executeRequest +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface GetThirdPartyUserTask : Task> { + + data class Params( + val protocol: String, + val fields: Map = emptyMap() + ) +} + +internal class DefaultGetThirdPartyUserTask @Inject constructor( + private val thirdPartyAPI: ThirdPartyAPI, + private val globalErrorReceiver: GlobalErrorReceiver +) : GetThirdPartyUserTask { + + override suspend fun execute(params: GetThirdPartyUserTask.Params): List { + return executeRequest(globalErrorReceiver) { + apiCall = thirdPartyAPI.getThirdPartyUser(params.protocol, params.fields) + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt new file mode 100644 index 0000000000..0c60a27341 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyAPI.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.thirdparty + +import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol +import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser +import org.matrix.android.sdk.internal.network.NetworkConstants +import retrofit2.Call +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.QueryMap + +internal interface ThirdPartyAPI { + + /** + * Get the third party server protocols. + * + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1.html#get-matrix-client-r0-thirdparty-protocols + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols") + fun thirdPartyProtocols(): Call> + + /** + * Retrieve a Matrix User ID linked to a user on the third party service, given a set of user parameters. + * + * Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-thirdparty-user-protocol + */ + @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "thirdparty/protocols/user/{protocol}") + fun getThirdPartyUser(@Path("protocol") protocol: String, @QueryMap params: Map?): Call> +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyModule.kt new file mode 100644 index 0000000000..d3acd7a9f3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/thirdparty/ThirdPartyModule.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.thirdparty + +import dagger.Binds +import dagger.Module +import dagger.Provides +import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService +import org.matrix.android.sdk.internal.session.SessionScope +import retrofit2.Retrofit + +@Module +internal abstract class ThirdPartyModule { + + @Module + companion object { + @Provides + @JvmStatic + @SessionScope + fun providesThirdPartyAPI(retrofit: Retrofit): ThirdPartyAPI { + return retrofit.create(ThirdPartyAPI::class.java) + } + } + + @Binds + abstract fun bindThirdPartyService(service: DefaultThirdPartyService): ThirdPartyService + + @Binds + abstract fun bindGetThirdPartyProtocolsTask(task: DefaultGetThirdPartyProtocolsTask): GetThirdPartyProtocolsTask + + @Binds + abstract fun bindGetThirdPartyUserTask(task: DefaultGetThirdPartyUserTask): GetThirdPartyUserTask +} diff --git a/vector/build.gradle b/vector/build.gradle index a59b41d910..3a25bf3127 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -448,6 +448,8 @@ dependencies { implementation 'com.vanniktech:emoji-material:0.7.0' implementation 'com.vanniktech:emoji-google:0.7.0' + implementation 'im.dlg:android-dialer:1.2.5' + // TESTS testImplementation 'junit:junit:4.13' testImplementation "org.amshove.kluent:kluent-android:$kluent_version" diff --git a/vector/src/main/assets/open_source_licenses.html b/vector/src/main/assets/open_source_licenses.html index 4ce4e9a210..3c6a9c48a4 100755 --- a/vector/src/main/assets/open_source_licenses.html +++ b/vector/src/main/assets/open_source_licenses.html @@ -385,6 +385,11 @@ SOFTWARE.
Copyright 2016 JetRadar +
  • + dialogs / android-dialer +
    + Copyright (c) 2017-present, dialog LLC +
  •  Apache License
    diff --git a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
    index b9bc935890..5203a96d69 100644
    --- a/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
    +++ b/vector/src/main/java/im/vector/app/core/error/ErrorFormatter.kt
    @@ -18,6 +18,7 @@ package im.vector.app.core.error
     
     import im.vector.app.R
     import im.vector.app.core.resources.StringProvider
    +import im.vector.app.features.call.dialpad.DialPadLookup
     import org.matrix.android.sdk.api.failure.Failure
     import org.matrix.android.sdk.api.failure.MatrixError
     import org.matrix.android.sdk.api.failure.isInvalidPassword
    @@ -109,8 +110,11 @@ class DefaultErrorFormatter @Inject constructor(
                             throwable.localizedMessage
                     }
                 }
    -            is SsoFlowNotSupportedYet    -> stringProvider.getString(R.string.error_sso_flow_not_supported_yet)
    -            else                         -> throwable.localizedMessage
    +            is SsoFlowNotSupportedYet                                                       ->
    +                stringProvider.getString(R.string.error_sso_flow_not_supported_yet)
    +            is DialPadLookup.Failure                                                        ->
    +                stringProvider.getString(R.string.call_dial_pad_lookup_error)
    +            else                                                                            -> throwable.localizedMessage
             }
                     ?: stringProvider.getString(R.string.unknown_error)
         }
    diff --git a/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt
    index 735ce2fd22..84658a830c 100644
    --- a/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt
    +++ b/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt
    @@ -64,6 +64,10 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
    +    override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetCallDialerChoiceBinding {
    +        return BottomSheetCallDialerChoiceBinding.inflate(inflater, container, false)
    +    }
    +
    +    var onDialPadClicked: (() -> Unit)? = null
    +    var onVoiceCallClicked: (() -> Unit)? = null
    +
    +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    +        super.onViewCreated(view, savedInstanceState)
    +        views.dialerChoiceDialPad.views.bottomSheetActionClickableZone.debouncedClicks {
    +            onDialPadClicked?.invoke()
    +            dismiss()
    +        }
    +
    +        views.dialerChoiceVoiceCall.views.bottomSheetActionClickableZone.debouncedClicks {
    +            onVoiceCallClicked?.invoke()
    +            dismiss()
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt
    index 5fb2d99ff6..77b2120b5f 100644
    --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt
    @@ -43,6 +43,8 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL
     import im.vector.app.core.utils.allGranted
     import im.vector.app.core.utils.checkPermissions
     import im.vector.app.databinding.ActivityCallBinding
    +import im.vector.app.features.call.dialpad.CallDialPadBottomSheet
    +import im.vector.app.features.call.dialpad.DialPadFragment
     import im.vector.app.features.call.utils.EglUtils
     import im.vector.app.features.call.webrtc.WebRtcCallManager
     import im.vector.app.features.home.AvatarRenderer
    @@ -84,9 +86,14 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
         private lateinit var callArgs: CallArgs
     
         @Inject lateinit var callManager: WebRtcCallManager
    -
         @Inject lateinit var viewModelFactory: VectorCallViewModel.Factory
     
    +    private val dialPadCallback = object : DialPadFragment.Callback {
    +        override fun onDigitAppended(digit: String) {
    +            callViewModel.handle(VectorCallViewActions.SendDtmfDigit(digit))
    +        }
    +    }
    +
         private var rootEglBase: EglBase? = null
     
         var surfaceRenderersAreInitialized = false
    @@ -114,7 +121,9 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
             if (intent.getStringExtra(EXTRA_MODE) == INCOMING_RINGING) {
                 turnScreenOnAndKeyguardOff()
             }
    -
    +        if (savedInstanceState != null) {
    +            (supportFragmentManager.findFragmentByTag(FRAGMENT_DIAL_PAD_TAG) as? CallDialPadBottomSheet)?.callback = dialPadCallback
    +        }
             configureCallViews()
     
             callViewModel.subscribe(this) {
    @@ -207,15 +216,14 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
                             }
                         } else {
                             views.callStatusText.text = state.formattedDuration
    +                        configureCallInfo(state)
                             if (callArgs.isVideoCall) {
                                 views.callVideoGroup.isVisible = true
                                 views.callInfoGroup.isVisible = false
                                 views.pipRenderer.isVisible =  !state.isVideoCaptureInError && state.otherKnownCallInfo == null
    -                            configureCallInfo(state)
                             } else {
                                 views.callVideoGroup.isInvisible = true
                                 views.callInfoGroup.isVisible = true
    -                            configureCallInfo(state)
                             }
                         }
                     } else {
    @@ -320,6 +328,11 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
                 is VectorCallViewEvents.ConnectionTimeout -> {
                     onErrorTimoutConnect(event.turn)
                 }
    +            is VectorCallViewEvents.ShowDialPad -> {
    +                CallDialPadBottomSheet.newInstance(false).apply {
    +                    callback = dialPadCallback
    +                }.show(supportFragmentManager, FRAGMENT_DIAL_PAD_TAG)
    +            }
                 is VectorCallViewEvents.ShowCallTransferScreen -> {
                     navigator.openCallTransfer(this, callArgs.callId)
                 }
    @@ -345,6 +358,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
     
             private const val CAPTURE_PERMISSION_REQUEST_CODE = 1
             private const val EXTRA_MODE = "EXTRA_MODE"
    +        private const val FRAGMENT_DIAL_PAD_TAG = "FRAGMENT_DIAL_PAD_TAG"
     
             const val OUTGOING_CREATED = "OUTGOING_CREATED"
             const val INCOMING_RINGING = "INCOMING_RINGING"
    diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt
    index 32a1deb266..7addabf724 100644
    --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt
    +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt
    @@ -27,6 +27,8 @@ sealed class VectorCallViewActions : VectorViewModelAction {
         object ToggleVideo : VectorCallViewActions()
         object ToggleHoldResume: VectorCallViewActions()
         data class ChangeAudioDevice(val device: CallAudioManager.Device) : VectorCallViewActions()
    +    object OpenDialPad: VectorCallViewActions()
    +    data class SendDtmfDigit(val digit: String) : VectorCallViewActions()
         object SwitchSoundDevice : VectorCallViewActions()
         object HeadSetButtonPressed : VectorCallViewActions()
         object ToggleCamera : VectorCallViewActions()
    diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt
    index 5ffeca6f66..91c3154d0a 100644
    --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt
    +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt
    @@ -28,6 +28,7 @@ sealed class VectorCallViewEvents : VectorViewEvents {
                 val available: Set,
                 val current: CallAudioManager.Device
         ) : VectorCallViewEvents()
    +    object ShowDialPad: VectorCallViewEvents()
         object ShowCallTransferScreen: VectorCallViewEvents()
     //    data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
     //    data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents()
    diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt
    index d12add5014..0c15b88cde 100644
    --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt
    @@ -267,6 +267,12 @@ class VectorCallViewModel @AssistedInject constructor(
                     if (!state.isVideoCall) return@withState
                     call?.setCaptureFormat(if (state.isHD) CaptureFormat.SD else CaptureFormat.HD)
                 }
    +            VectorCallViewActions.OpenDialPad -> {
    +                _viewEvents.post(VectorCallViewEvents.ShowDialPad)
    +            }
    +            is VectorCallViewActions.SendDtmfDigit -> {
    +                call?.sendDtmfDigit(action.digit)
    +            }
                 VectorCallViewActions.InitiateCallTransfer -> {
                     _viewEvents.post(
                             VectorCallViewEvents.ShowCallTransferScreen
    diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt
    new file mode 100644
    index 0000000000..06b4dbfe7b
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/CallDialPadBottomSheet.kt
    @@ -0,0 +1,100 @@
    +/*
    + * Copyright (c) 2021 New Vector Ltd
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *     http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package im.vector.app.features.call.dialpad
    +
    +import android.os.Bundle
    +import android.view.LayoutInflater
    +import android.view.View
    +import android.view.ViewGroup
    +import im.vector.app.R
    +import im.vector.app.core.extensions.addChildFragment
    +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
    +import im.vector.app.databinding.BottomSheetCallDialPadBinding
    +import im.vector.app.features.settings.VectorLocale
    +
    +class CallDialPadBottomSheet : VectorBaseBottomSheetDialogFragment() {
    +
    +    companion object {
    +
    +        private const val EXTRA_SHOW_ACTIONS = "EXTRA_SHOW_ACTIONS"
    +
    +        fun newInstance(showActions: Boolean): CallDialPadBottomSheet {
    +            return CallDialPadBottomSheet().apply {
    +                arguments = Bundle().apply {
    +                    putBoolean(EXTRA_SHOW_ACTIONS, showActions)
    +                }
    +            }
    +        }
    +    }
    +
    +    override val showExpanded = true
    +
    +    var callback: DialPadFragment.Callback? = null
    +        set(value) {
    +            field = value
    +            setCallbackToFragment(callback)
    +        }
    +
    +    override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetCallDialPadBinding {
    +        return BottomSheetCallDialPadBinding.inflate(inflater, container, false)
    +    }
    +
    +    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    +        super.onViewCreated(view, savedInstanceState)
    +        if (savedInstanceState == null) {
    +            val showActions = arguments?.getBoolean(EXTRA_SHOW_ACTIONS, false) ?: false
    +            DialPadFragment().apply {
    +                arguments = Bundle().apply {
    +                    putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, showActions)
    +                    putBoolean(DialPadFragment.EXTRA_ENABLE_OK, showActions)
    +                    putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country)
    +                }
    +                callback = DialPadFragmentCallbackWrapper(this@CallDialPadBottomSheet.callback)
    +            }.also {
    +                addChildFragment(R.id.callDialPadFragmentContainer, it)
    +            }
    +        } else {
    +            setCallbackToFragment(callback)
    +        }
    +        views.callDialPadClose.setOnClickListener {
    +            dismiss()
    +        }
    +    }
    +
    +    override fun onDestroyView() {
    +        setCallbackToFragment(null)
    +        super.onDestroyView()
    +    }
    +
    +    private fun setCallbackToFragment(callback: DialPadFragment.Callback?) {
    +        if (!isAdded) return
    +        val dialPadFragment = childFragmentManager.findFragmentById(R.id.callDialPadFragmentContainer) as? DialPadFragment
    +        dialPadFragment?.callback = DialPadFragmentCallbackWrapper(callback)
    +    }
    +
    +    private inner class DialPadFragmentCallbackWrapper(val callback: DialPadFragment.Callback?): DialPadFragment.Callback {
    +
    +        override fun onDigitAppended(digit: String) {
    +            callback?.onDigitAppended(digit)
    +        }
    +
    +        override fun onOkClicked(formatted: String?, raw: String?) {
    +            callback?.onOkClicked(formatted, raw)
    +            dismiss()
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt
    new file mode 100644
    index 0000000000..b488a1af0e
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadFragment.kt
    @@ -0,0 +1,196 @@
    +/*
    + * Copyright (c) 2021 New Vector Ltd
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *     http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package im.vector.app.features.call.dialpad
    +
    +import android.content.res.ColorStateList
    +import android.os.Bundle
    +import android.view.LayoutInflater
    +import android.view.View
    +import android.view.ViewGroup
    +import androidx.core.view.isVisible
    +import androidx.core.widget.ImageViewCompat
    +import androidx.fragment.app.Fragment
    +import com.android.dialer.dialpadview.DialpadView
    +import com.android.dialer.dialpadview.DigitsEditText
    +import com.android.dialer.dialpadview.R
    +import com.google.i18n.phonenumbers.AsYouTypeFormatter
    +import com.google.i18n.phonenumbers.PhoneNumberUtil
    +import im.vector.app.features.themes.ThemeUtils
    +
    +class DialPadFragment : Fragment() {
    +
    +    var callback: Callback? = null
    +
    +    private var digits: DigitsEditText? = null
    +    private var formatter: AsYouTypeFormatter? = null
    +    private var input = ""
    +    private var regionCode: String = DEFAULT_REGION_CODE
    +    private var formatAsYouType = true
    +    private var enableStar = true
    +    private var enablePound = true
    +    private var enablePlus = true
    +    private var cursorVisible = false
    +    private var enableDelete = true
    +    private var enableFabOk = true
    +
    +    override fun onCreateView(
    +            inflater: LayoutInflater,
    +            container: ViewGroup?,
    +            savedInstanceState: Bundle?): View {
    +        initArgs(savedInstanceState)
    +        val view = inflater.inflate(R.layout.dialpad_fragment, container, false)
    +        val dialpadView = view.findViewById(R.id.dialpad_view) as DialpadView
    +        dialpadView.findViewById(R.id.dialpad_key_voicemail).isVisible = false
    +        digits = dialpadView.digits as? DigitsEditText
    +        digits?.isCursorVisible = cursorVisible
    +        digits?.setTextColor(ThemeUtils.getColor(requireContext(), im.vector.app.R.attr.riotx_text_primary))
    +        dialpadView.findViewById(R.id.zero).setOnClickListener { append('0') }
    +        if (enablePlus) {
    +            dialpadView.findViewById(R.id.zero).setOnLongClickListener {
    +                append('+')
    +                true
    +            }
    +        }
    +        dialpadView.findViewById(R.id.one).setOnClickListener { append('1') }
    +        dialpadView.findViewById(R.id.two).setOnClickListener { append('2') }
    +        dialpadView.findViewById(R.id.three).setOnClickListener { append('3') }
    +        dialpadView.findViewById(R.id.four).setOnClickListener { append('4') }
    +        dialpadView.findViewById(R.id.four).setOnClickListener { append('4') }
    +        dialpadView.findViewById(R.id.five).setOnClickListener { append('5') }
    +        dialpadView.findViewById(R.id.six).setOnClickListener { append('6') }
    +        dialpadView.findViewById(R.id.seven).setOnClickListener { append('7') }
    +        dialpadView.findViewById(R.id.eight).setOnClickListener { append('8') }
    +        dialpadView.findViewById(R.id.nine).setOnClickListener { append('9') }
    +        if (enableStar) {
    +            dialpadView.findViewById(R.id.star).setOnClickListener { append('*') }
    +        } else {
    +            dialpadView.findViewById(R.id.star).isVisible = false
    +        }
    +        if (enablePound) {
    +            dialpadView.findViewById(R.id.pound).setOnClickListener { append('#') }
    +        } else {
    +            dialpadView.findViewById(R.id.pound).isVisible = false
    +        }
    +        if (enableDelete) {
    +            dialpadView.deleteButton.setOnClickListener { poll() }
    +            dialpadView.deleteButton.setOnLongClickListener {
    +                clear()
    +                true
    +            }
    +            val tintColor = ThemeUtils.getColor(requireContext(), im.vector.app.R.attr.riotx_text_secondary)
    +            ImageViewCompat.setImageTintList(dialpadView.deleteButton, ColorStateList.valueOf(tintColor))
    +        } else {
    +            dialpadView.deleteButton.isVisible = false
    +        }
    +
    +        // if region code is null, no formatting is performed
    +        formatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(if (formatAsYouType) regionCode else "")
    +
    +        val fabOk = view.findViewById(R.id.fab_ok)
    +        if (enableFabOk) {
    +            fabOk.setOnClickListener {
    +                callback?.onOkClicked(digits?.text.toString(), input)
    +            }
    +        } else {
    +            fabOk.isVisible = false
    +        }
    +
    +        digits?.setOnTextContextMenuClickListener {
    +            val string = digits?.text.toString()
    +            clear()
    +            for (element in string) {
    +                append(element)
    +            }
    +        }
    +        return view
    +    }
    +
    +    override fun onSaveInstanceState(outState: Bundle) {
    +        super.onSaveInstanceState(outState)
    +        outState.putString(EXTRA_REGION_CODE, regionCode)
    +        outState.putBoolean(EXTRA_FORMAT_AS_YOU_TYPE, formatAsYouType)
    +        outState.putBoolean(EXTRA_ENABLE_STAR, enableStar)
    +        outState.putBoolean(EXTRA_ENABLE_POUND, enablePound)
    +        outState.putBoolean(EXTRA_ENABLE_PLUS, enablePlus)
    +        outState.putBoolean(EXTRA_ENABLE_OK, enableFabOk)
    +        outState.putBoolean(EXTRA_ENABLE_DELETE, enableDelete)
    +        outState.putBoolean(EXTRA_CURSOR_VISIBLE, cursorVisible)
    +    }
    +
    +    private fun initArgs(savedInstanceState: Bundle?) {
    +        val args = savedInstanceState ?: arguments
    +        if (args != null) {
    +            regionCode = args.getString(EXTRA_REGION_CODE, DEFAULT_REGION_CODE)
    +            formatAsYouType = args.getBoolean(EXTRA_FORMAT_AS_YOU_TYPE, formatAsYouType)
    +            enableStar = args.getBoolean(EXTRA_ENABLE_STAR, enableStar)
    +            enablePound = args.getBoolean(EXTRA_ENABLE_POUND, enablePound)
    +            enablePlus = args.getBoolean(EXTRA_ENABLE_PLUS, enablePlus)
    +            enableDelete = args.getBoolean(EXTRA_ENABLE_DELETE, enableDelete)
    +            enableFabOk = args.getBoolean(EXTRA_ENABLE_OK, enableFabOk)
    +            cursorVisible = args.getBoolean(EXTRA_CURSOR_VISIBLE, cursorVisible)
    +        }
    +    }
    +
    +    private fun poll() {
    +        if (!input.isEmpty()) {
    +            input = input.substring(0, input.length - 1)
    +            formatter = PhoneNumberUtil.getInstance().getAsYouTypeFormatter(regionCode)
    +            if (formatAsYouType) {
    +                digits?.setText("")
    +                for (c in input.toCharArray()) {
    +                    digits?.setText(formatter?.inputDigit(c))
    +                }
    +            } else {
    +                digits?.setText(input)
    +            }
    +        }
    +    }
    +
    +    private fun clear() {
    +        formatter?.clear()
    +        digits?.setText("")
    +        input = ""
    +    }
    +
    +    private fun append(c: Char) {
    +        callback?.onDigitAppended(c.toString())
    +        input += c
    +        if (formatAsYouType) {
    +            digits?.setText(formatter?.inputDigit(c))
    +        } else {
    +            digits?.setText(input)
    +        }
    +    }
    +
    +    interface Callback {
    +        fun onOkClicked(formatted: String?, raw: String?) = Unit
    +        fun onDigitAppended(digit: String) = Unit
    +    }
    +
    +    companion object {
    +        const val EXTRA_REGION_CODE = "EXTRA_REGION_CODE"
    +        const val EXTRA_FORMAT_AS_YOU_TYPE = "EXTRA_FORMAT_AS_YOU_TYPE"
    +        const val EXTRA_ENABLE_STAR = "EXTRA_ENABLE_STAR"
    +        const val EXTRA_ENABLE_POUND = "EXTRA_ENABLE_POUND"
    +        const val EXTRA_ENABLE_PLUS = "EXTRA_ENABLE_PLUS"
    +        const val EXTRA_ENABLE_DELETE = "EXTRA_ENABLE_DELETE"
    +        const val EXTRA_ENABLE_OK = "EXTRA_ENABLE_OK"
    +        const val EXTRA_CURSOR_VISIBLE = "EXTRA_CURSOR_VISIBLE"
    +
    +        private const val DEFAULT_REGION_CODE = "US"
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt
    new file mode 100644
    index 0000000000..1c5caee2cd
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt
    @@ -0,0 +1,44 @@
    +/*
    + * Copyright (c) 2021 New Vector Ltd
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *     http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package im.vector.app.features.call.dialpad
    +
    +import im.vector.app.features.call.webrtc.WebRtcCallManager
    +import im.vector.app.features.createdirect.DirectRoomHelper
    +import org.matrix.android.sdk.api.extensions.tryOrNull
    +import org.matrix.android.sdk.api.session.Session
    +import javax.inject.Inject
    +
    +class DialPadLookup @Inject constructor(val session: Session,
    +                                        val directRoomHelper: DirectRoomHelper,
    +                                        val callManager: WebRtcCallManager
    +) {
    +
    +    class Failure : Throwable()
    +    data class Result(val userId: String, val roomId: String)
    +
    +    suspend fun lookupPhoneNumber(phoneNumber: String): Result {
    +        val supportedProtocolKey = callManager.supportedPSTNProtocol ?: throw Failure()
    +        val thirdPartyUser = tryOrNull {
    +            session.thirdPartyService().getThirdPartyUser(supportedProtocolKey, fields = mapOf(
    +                    "m.id.phone" to phoneNumber
    +            )).firstOrNull()
    +        } ?: throw Failure()
    +
    +        val roomId = directRoomHelper.ensureDMExists(thirdPartyUser.userId)
    +        return Result(userId = thirdPartyUser.userId, roomId = roomId)
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferAction.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferAction.kt
    index cbdd9c252b..bd694ad14e 100644
    --- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferAction.kt
    +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferAction.kt
    @@ -19,5 +19,6 @@ package im.vector.app.features.call.transfer
     import im.vector.app.core.platform.VectorViewModelAction
     
     sealed class CallTransferAction : VectorViewModelAction {
    -    data class Connect(val consultFirst: Boolean, val selectedUserId: String) : CallTransferAction()
    +    data class ConnectWithUserId(val consultFirst: Boolean, val selectedUserId: String) : CallTransferAction()
    +    data class ConnectWithPhoneNumber(val consultFirst: Boolean, val phoneNumber: String) : CallTransferAction()
     }
    diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt
    index 89a7caa764..c5b4dda135 100644
    --- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt
    +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferActivity.kt
    @@ -20,27 +20,16 @@ import android.content.Context
     import android.content.Intent
     import android.os.Bundle
     import android.os.Parcelable
    -import android.widget.Toast
     import com.airbnb.mvrx.MvRx
     import com.airbnb.mvrx.viewModel
    +import com.google.android.material.tabs.TabLayoutMediator
     import im.vector.app.R
     import im.vector.app.core.di.ScreenComponent
     import im.vector.app.core.error.ErrorFormatter
    -import im.vector.app.core.extensions.addFragment
    -import im.vector.app.core.extensions.addFragmentToBackstack
     import im.vector.app.core.platform.VectorBaseActivity
    -import im.vector.app.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH
    -import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS
    -import im.vector.app.core.utils.allGranted
    -import im.vector.app.core.utils.checkPermissions
     import im.vector.app.databinding.ActivityCallTransferBinding
    -import im.vector.app.features.contactsbook.ContactsBookFragment
     import im.vector.app.features.contactsbook.ContactsBookViewModel
     import im.vector.app.features.contactsbook.ContactsBookViewState
    -import im.vector.app.features.userdirectory.UserListFragment
    -import im.vector.app.features.userdirectory.UserListFragmentArgs
    -import im.vector.app.features.userdirectory.UserListSharedAction
    -import im.vector.app.features.userdirectory.UserListSharedActionViewModel
     import im.vector.app.features.userdirectory.UserListViewModel
     import im.vector.app.features.userdirectory.UserListViewState
     import kotlinx.parcelize.Parcelize
    @@ -56,16 +45,19 @@ class CallTransferActivity : VectorBaseActivity(),
             UserListViewModel.Factory,
             ContactsBookViewModel.Factory {
     
    -    private lateinit var sharedActionViewModel: UserListSharedActionViewModel
         @Inject lateinit var userListViewModelFactory: UserListViewModel.Factory
         @Inject lateinit var callTransferViewModelFactory: CallTransferViewModel.Factory
         @Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory
         @Inject lateinit var errorFormatter: ErrorFormatter
     
    +    private lateinit var sectionsPagerAdapter: CallTransferPagerAdapter
    +
         private val callTransferViewModel: CallTransferViewModel by viewModel()
     
         override fun getBinding() = ActivityCallTransferBinding.inflate(layoutInflater)
     
    +    override fun getCoordinatorLayout() = views.vectorCoordinatorLayout
    +
         override fun injectWith(injector: ScreenComponent) {
             super.injectWith(injector)
             injector.inject(this)
    @@ -86,39 +78,28 @@ class CallTransferActivity : VectorBaseActivity(),
         override fun onCreate(savedInstanceState: Bundle?) {
             super.onCreate(savedInstanceState)
             waitingView = views.waitingView.waitingView
    -        sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java)
    -        sharedActionViewModel
    -                .observe()
    -                .subscribe { sharedAction ->
    -                    when (sharedAction) {
    -                        UserListSharedAction.OpenPhoneBook -> openPhoneBook()
    -                        // not exhaustive because it's a sharedAction
    -                        else                               -> {
    -                        }
    -                    }
    -                }
    -                .disposeOnDestroy()
    -        if (isFirstCreation()) {
    -            addFragment(
    -                    R.id.callTransferFragmentContainer,
    -                    UserListFragment::class.java,
    -                    UserListFragmentArgs(
    -                            title = "",
    -                            menuResId = -1,
    -                            singleSelection = true,
    -                            showInviteActions = false,
    -                            showToolbar = false
    -                    ),
    -                    USER_LIST_FRAGMENT_TAG
    -            )
    -        }
    +
             callTransferViewModel.observeViewEvents {
    -            when (it) {
    -                is CallTransferViewEvents.Dismiss -> finish()
    -                CallTransferViewEvents.Loading    -> showWaitingView()
    +            when (it)  {
    +                is CallTransferViewEvents.Dismiss        -> finish()
    +                CallTransferViewEvents.Loading           -> showWaitingView()
                     is CallTransferViewEvents.FailToTransfer -> showSnackbar(getString(R.string.call_transfer_failure))
                 }
             }
    +
    +        sectionsPagerAdapter = CallTransferPagerAdapter(this).register()
    +        views.callTransferViewPager.adapter = sectionsPagerAdapter
    +        sectionsPagerAdapter.onDialPadOkClicked = { phoneNumber ->
    +            val action = CallTransferAction.ConnectWithPhoneNumber(views.callTransferConsultCheckBox.isChecked, phoneNumber)
    +            callTransferViewModel.handle(action)
    +        }
    +
    +        TabLayoutMediator(views.callTransferTabLayout, views.callTransferViewPager) { tab, position ->
    +            when (position) {
    +                0 -> tab.text = getString(R.string.call_transfer_users_tab_title)
    +                1 -> tab.text = getString(R.string.call_dial_pad_title)
    +            }
    +        }.attach()
             configureToolbar(views.callTransferToolbar)
             views.callTransferToolbar.title = getString(R.string.call_transfer_title)
             setupConnectAction()
    @@ -126,36 +107,14 @@ class CallTransferActivity : VectorBaseActivity(),
     
         private fun setupConnectAction() {
             views.callTransferConnectAction.debouncedClicks {
    -            val userListFragment = supportFragmentManager.findFragmentByTag(USER_LIST_FRAGMENT_TAG) as? UserListFragment
    -            val selectedUser = userListFragment?.getCurrentState()?.getSelectedMatrixId()?.firstOrNull()
    +            val selectedUser = sectionsPagerAdapter.userListFragment?.getCurrentState()?.getSelectedMatrixId()?.firstOrNull()
                 if (selectedUser != null) {
    -                val action = CallTransferAction.Connect(views.callTransferConsultCheckBox.isChecked, selectedUser)
    +                val action = CallTransferAction.ConnectWithUserId(views.callTransferConsultCheckBox.isChecked, selectedUser)
                     callTransferViewModel.handle(action)
                 }
             }
         }
     
    -    private fun openPhoneBook() {
    -        // Check permission first
    -        if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH,
    -                        this,
    -                        PERMISSION_REQUEST_CODE_READ_CONTACTS,
    -                        0)) {
    -            addFragmentToBackstack(R.id.callTransferFragmentContainer, ContactsBookFragment::class.java)
    -        }
    -    }
    -
    -    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
    -        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    -        if (allGranted(grantResults)) {
    -            if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
    -                doOnPostResume { addFragmentToBackstack(R.id.callTransferFragmentContainer, ContactsBookFragment::class.java) }
    -            }
    -        } else {
    -            Toast.makeText(baseContext, R.string.missing_permissions_error, Toast.LENGTH_SHORT).show()
    -        }
    -    }
    -
         companion object {
     
             fun newIntent(context: Context, callId: String): Intent {
    diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferPagerAdapter.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferPagerAdapter.kt
    new file mode 100644
    index 0000000000..1f2d3070dd
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferPagerAdapter.kt
    @@ -0,0 +1,88 @@
    +/*
    + * Copyright (c) 2020 New Vector Ltd
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *     http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package im.vector.app.features.call.transfer
    +
    +import android.os.Bundle
    +import androidx.fragment.app.Fragment
    +import androidx.fragment.app.FragmentActivity
    +import androidx.viewpager2.adapter.FragmentStateAdapter
    +import im.vector.app.core.extensions.toMvRxBundle
    +import im.vector.app.core.platform.Restorable
    +import im.vector.app.features.call.dialpad.DialPadFragment
    +import im.vector.app.features.settings.VectorLocale
    +import im.vector.app.features.userdirectory.UserListFragment
    +import im.vector.app.features.userdirectory.UserListFragmentArgs
    +
    +class CallTransferPagerAdapter(
    +        private val fragmentActivity: FragmentActivity
    +) : FragmentStateAdapter(fragmentActivity), Restorable {
    +
    +    val userListFragment: UserListFragment?
    +        get() = findFragmentAtPosition(0) as? UserListFragment
    +    val dialPadFragment: DialPadFragment?
    +        get() = findFragmentAtPosition(1) as? DialPadFragment
    +
    +    var onDialPadOkClicked: ((String) -> Unit)? = null
    +
    +    override fun getItemCount() = 2
    +
    +    override fun createFragment(position: Int): Fragment {
    +        val fragment: Fragment
    +        if (position == 0) {
    +            fragment = fragmentActivity.supportFragmentManager.fragmentFactory.instantiate(fragmentActivity.classLoader, UserListFragment::class.java.name)
    +            fragment.arguments = UserListFragmentArgs(
    +                    title = "",
    +                    menuResId = -1,
    +                    singleSelection = true,
    +                    showInviteActions = false,
    +                    showToolbar = false,
    +                    showContactBookAction = false
    +            ).toMvRxBundle()
    +        } else {
    +            fragment = fragmentActivity.supportFragmentManager.fragmentFactory.instantiate(fragmentActivity.classLoader, DialPadFragment::class.java.name)
    +            (fragment as DialPadFragment).apply {
    +                arguments = Bundle().apply {
    +                    putBoolean(DialPadFragment.EXTRA_ENABLE_DELETE, true)
    +                    putBoolean(DialPadFragment.EXTRA_ENABLE_OK, true)
    +                    putString(DialPadFragment.EXTRA_REGION_CODE, VectorLocale.applicationLocale.country)
    +                }
    +                applyCallback()
    +            }
    +        }
    +        return fragment
    +    }
    +
    +    private fun findFragmentAtPosition(position: Int): Fragment? {
    +        return fragmentActivity.supportFragmentManager.findFragmentByTag("f$position")
    +    }
    +
    +    override fun onSaveInstanceState(outState: Bundle) = Unit
    +
    +    override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
    +        dialPadFragment?.applyCallback()
    +    }
    +
    +    private fun DialPadFragment.applyCallback(): DialPadFragment {
    +        callback = object : DialPadFragment.Callback {
    +            override fun onOkClicked(formatted: String?, raw: String?) {
    +                if (raw.isNullOrEmpty()) return
    +                onDialPadOkClicked?.invoke(raw)
    +            }
    +        }
    +        return this
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt
    index 0b83a6bcda..2fa016feea 100644
    --- a/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/call/transfer/CallTransferViewModel.kt
    @@ -24,6 +24,7 @@ import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
    +import im.vector.app.features.call.dialpad.DialPadLookup
     import im.vector.app.features.call.webrtc.WebRtcCall
     import im.vector.app.features.call.webrtc.WebRtcCallManager
     import kotlinx.coroutines.launch
    @@ -31,6 +32,7 @@ import org.matrix.android.sdk.api.session.call.CallState
     import org.matrix.android.sdk.api.session.call.MxCall
     
     class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState,
    +                                                        private val dialPadLookup: DialPadLookup,
                                                             callManager: WebRtcCallManager)
         : VectorViewModel(initialState) {
     
    @@ -72,11 +74,12 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
     
         override fun handle(action: CallTransferAction) {
             when (action) {
    -            is CallTransferAction.Connect -> transferCall(action)
    +            is CallTransferAction.ConnectWithUserId -> connectWithUserId(action)
    +            is CallTransferAction.ConnectWithPhoneNumber -> connectWithPhoneNumber(action)
             }.exhaustive
         }
     
    -    private fun transferCall(action: CallTransferAction.Connect) {
    +    private fun connectWithUserId(action: CallTransferAction.ConnectWithUserId) {
             viewModelScope.launch {
                 try {
                     _viewEvents.post(CallTransferViewEvents.Loading)
    @@ -87,4 +90,17 @@ class CallTransferViewModel @AssistedInject constructor(@Assisted initialState:
                 }
             }
         }
    +
    +    private fun connectWithPhoneNumber(action: CallTransferAction.ConnectWithPhoneNumber) {
    +        viewModelScope.launch {
    +            try {
    +                _viewEvents.post(CallTransferViewEvents.Loading)
    +                val result = dialPadLookup.lookupPhoneNumber(action.phoneNumber)
    +                call?.mxCall?.transfer(result.userId, result.roomId)
    +                _viewEvents.post(CallTransferViewEvents.Dismiss)
    +            } catch (failure: Throwable) {
    +                _viewEvents.post(CallTransferViewEvents.FailToTransfer)
    +            }
    +        }
    +    }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/PSTNProtocol.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/PSTNProtocol.kt
    new file mode 100644
    index 0000000000..3e6d2df690
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/PSTNProtocol.kt
    @@ -0,0 +1,43 @@
    +/*
    + * Copyright (c) 2021 New Vector Ltd
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *     http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package im.vector.app.features.call.webrtc
    +
    +import kotlinx.coroutines.delay
    +import org.matrix.android.sdk.api.session.Session
    +import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
    +
    +private const val PSTN_VECTOR_KEY = "im.vector.protocol.pstn"
    +private const val PSTN_MATRIX_KEY = "m.protocol.pstn"
    +
    +suspend fun Session.getSupportedPSTN(maxTries: Int): String? {
    +    val thirdPartyProtocols: Map = try {
    +        thirdPartyService().getThirdPartyProtocols()
    +    } catch (failure: Throwable) {
    +        if (maxTries == 1) {
    +            return null
    +        } else {
    +            // Wait for 10s before trying again
    +            delay(10_000L)
    +            return getSupportedPSTN(maxTries - 1)
    +        }
    +    }
    +    return when {
    +        thirdPartyProtocols.containsKey(PSTN_VECTOR_KEY) -> PSTN_VECTOR_KEY
    +        thirdPartyProtocols.containsKey(PSTN_MATRIX_KEY) -> PSTN_MATRIX_KEY
    +        else                                             -> null
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt
    index bc55c3e924..5ad78a3801 100644
    --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt
    +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt
    @@ -291,6 +291,23 @@ class WebRtcCall(val mxCall: MxCall,
             }
         }
     
    +    /**
    +     * Sends a DTMF digit to the other party
    +     * @param digit The digit (nb. string - '#' and '*' are dtmf too)
    +     */
    +    fun sendDtmfDigit(digit: String) {
    +        for (sender in peerConnection?.senders.orEmpty()) {
    +            if (sender.track()?.kind() == "audio" && sender.dtmf()?.canInsertDtmf() == true) {
    +                try {
    +                    sender.dtmf()?.insertDtmf(digit, 100, 70)
    +                    return
    +                } catch (failure: Throwable) {
    +                    Timber.v("Fail to send Dtmf digit")
    +                }
    +            }
    +        }
    +    }
    +
         fun detachRenderers(renderers: List?) {
             Timber.v("## VOIP detachRenderers")
             if (renderers.isNullOrEmpty()) {
    diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt
    index 86cf3972c9..0f694249f3 100644
    --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt
    +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt
    @@ -26,7 +26,9 @@ import im.vector.app.features.call.VectorCallActivity
     import im.vector.app.features.call.audio.CallAudioManager
     import im.vector.app.features.call.utils.EglUtils
     import im.vector.app.push.fcm.FcmHelper
    +import kotlinx.coroutines.GlobalScope
     import kotlinx.coroutines.asCoroutineDispatcher
    +import kotlinx.coroutines.launch
     import org.matrix.android.sdk.api.extensions.tryOrNull
     import org.matrix.android.sdk.api.session.Session
     import org.matrix.android.sdk.api.session.call.CallListener
    @@ -68,7 +70,21 @@ class WebRtcCallManager @Inject constructor(
             fun onAudioDevicesChange() {}
         }
     
    +    interface PSTNSupportListener {
    +        fun onPSTNSupportUpdated()
    +    }
    +
    +    private val pstnSupportListeners = emptyList().toMutableList()
    +    fun addPstnSupportListener(listener: PSTNSupportListener) {
    +        pstnSupportListeners.add(listener)
    +    }
    +
    +    fun removePstnSupportListener(listener: PSTNSupportListener) {
    +        pstnSupportListeners.remove(listener)
    +    }
    +
         private val currentCallsListeners = CopyOnWriteArrayList()
    +
         fun addCurrentCallListener(listener: CurrentCallListener) {
             currentCallsListeners.add(listener)
         }
    @@ -88,11 +104,27 @@ class WebRtcCallManager @Inject constructor(
         private var peerConnectionFactory: PeerConnectionFactory? = null
         private val executor = Executors.newSingleThreadExecutor()
         private val dispatcher = executor.asCoroutineDispatcher()
    +    var supportedPSTNProtocol: String? = null
    +        private set
    +
    +    val supportsPSTNProtocol: Boolean
    +        get() = supportedPSTNProtocol != null
     
         private val rootEglBase by lazy { EglUtils.rootEglBase }
     
         private var isInBackground: Boolean = true
     
    +    init {
    +        GlobalScope.launch {
    +            supportedPSTNProtocol = currentSession?.getSupportedPSTN(3)
    +            if (supportedPSTNProtocol != null) {
    +                pstnSupportListeners.forEach {
    +                    tryOrNull { it.onPSTNSupportUpdated() }
    +                }
    +            }
    +        }
    +    }
    +
         @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
         fun entersForeground() {
             isInBackground = false
    diff --git a/vector/src/main/java/im/vector/app/features/createdirect/DirectRoomHelper.kt b/vector/src/main/java/im/vector/app/features/createdirect/DirectRoomHelper.kt
    new file mode 100644
    index 0000000000..171970ec1e
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/app/features/createdirect/DirectRoomHelper.kt
    @@ -0,0 +1,54 @@
    +/*
    + * Copyright (c) 2021 New Vector Ltd
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *     http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package im.vector.app.features.createdirect
    +
    +import im.vector.app.features.raw.wellknown.getElementWellknown
    +import im.vector.app.features.raw.wellknown.isE2EByDefault
    +import org.matrix.android.sdk.api.extensions.tryOrNull
    +import org.matrix.android.sdk.api.raw.RawService
    +import org.matrix.android.sdk.api.session.Session
    +import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
    +import org.matrix.android.sdk.internal.util.awaitCallback
    +import javax.inject.Inject
    +
    +class DirectRoomHelper @Inject constructor(
    +        private val rawService: RawService,
    +        private val session: Session
    +) {
    +
    +    suspend fun ensureDMExists(userId: String): String {
    +        val existingRoomId = tryOrNull { session.getExistingDirectRoomWithUser(userId) }
    +        val roomId: String
    +        if (existingRoomId != null) {
    +            roomId = existingRoomId
    +        } else {
    +            val adminE2EByDefault = rawService.getElementWellknown(session.myUserId)
    +                    ?.isE2EByDefault()
    +                    ?: true
    +
    +            val roomParams = CreateRoomParams().apply {
    +                invitedUserIds.add(userId)
    +                setDirectMessage()
    +                enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault
    +            }
    +            roomId = awaitCallback {
    +                session.createRoom(roomParams, it)
    +            }
    +        }
    +        return roomId
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
    index 9bf8b9c310..98ad6c454c 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt
    @@ -72,6 +72,8 @@ sealed class RoomDetailAction : VectorViewModelAction {
         data class IgnoreUser(val userId: String?) : RoomDetailAction()
     
         object ResendAll : RoomDetailAction()
    +
    +    data class StartCallWithPhoneNumber(val phoneNumber: String, val videoCall: Boolean): RoomDetailAction()
         data class StartCall(val isVideo: Boolean) : RoomDetailAction()
         data class AcceptCall(val callId: String): RoomDetailAction()
         object EndCall : RoomDetailAction()
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
    index ffd50f180b..e61aa59dce 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
    @@ -96,8 +96,6 @@ import im.vector.app.core.ui.views.JumpToReadMarkerView
     import im.vector.app.core.ui.views.NotificationAreaView
     import im.vector.app.core.utils.Debouncer
     import im.vector.app.core.utils.KeyboardStateUtils
    -import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL
    -import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL
     import im.vector.app.core.utils.PERMISSIONS_FOR_WRITING_FILES
     import im.vector.app.core.utils.TextUtils
     import im.vector.app.core.utils.checkPermissions
    @@ -291,17 +289,28 @@ class RoomDetailFragment @Inject constructor(
     
         private lateinit var attachmentsHelper: AttachmentsHelper
         private lateinit var keyboardStateUtils: KeyboardStateUtils
    +    private lateinit var callActionsHandler : StartCallActionsHandler
     
         private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
     
         private var lockSendButton = false
    -    private val activeCallViewHolder = KnownCallsViewHolder()
    +    private val knownCallsViewHolder = KnownCallsViewHolder()
     
         override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
             super.onViewCreated(view, savedInstanceState)
             sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
             knownCallsViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java)
             attachmentsHelper = AttachmentsHelper(requireContext(), this).register()
    +        callActionsHandler = StartCallActionsHandler(
    +                roomId = roomDetailArgs.roomId,
    +                fragment = this,
    +                vectorPreferences = vectorPreferences,
    +                roomDetailViewModel = roomDetailViewModel,
    +                callManager = callManager,
    +                startCallActivityResultLauncher = startCallActivityResultLauncher,
    +                showDialogWithMessage = ::showDialogWithMessage,
    +                onTapToReturnToCall = ::onTapToReturnToCall
    +        ).register()
             keyboardStateUtils = KeyboardStateUtils(requireActivity())
             setupToolbar(views.roomToolbar)
             setupRecyclerView()
    @@ -328,7 +337,7 @@ class RoomDetailFragment @Inject constructor(
             knownCallsViewModel
                     .liveKnownCalls
                     .observe(viewLifecycleOwner, {
    -                    activeCallViewHolder.updateCall(callManager.getCurrentCall(), it)
    +                    knownCallsViewHolder.updateCall(callManager.getCurrentCall(), it)
                         invalidateOptionsMenu()
                     })
     
    @@ -606,7 +615,7 @@ class RoomDetailFragment @Inject constructor(
         }
     
         override fun onDestroy() {
    -        activeCallViewHolder.unBind()
    +        knownCallsViewHolder.unBind()
             roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
             super.onDestroy()
         }
    @@ -637,7 +646,7 @@ class RoomDetailFragment @Inject constructor(
         }
     
         private fun setupActiveCallView() {
    -        activeCallViewHolder.bind(
    +        knownCallsViewHolder.bind(
                     views.activeCallPiP,
                     views.activeCallView,
                     views.activeCallPiPWrap,
    @@ -761,9 +770,12 @@ class RoomDetailFragment @Inject constructor(
                     roomDetailViewModel.handle(RoomDetailAction.ManageIntegrations)
                     true
                 }
    -            R.id.voice_call,
    +            R.id.voice_call -> {
    +                callActionsHandler.onVoiceCallClicked()
    +                true
    +            }
                 R.id.video_call       -> {
    -                handleCallRequest(item)
    +                callActionsHandler.onVideoCallClicked()
                     true
                 }
                 R.id.hangup_call      -> {
    @@ -786,73 +798,6 @@ class RoomDetailFragment @Inject constructor(
             }
         }
     
    -    private fun handleCallRequest(item: MenuItem) = withState(roomDetailViewModel) { state ->
    -        val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState
    -        val isVideoCall = item.itemId == R.id.video_call
    -        when (roomSummary.joinedMembersCount) {
    -            1    -> {
    -                val pendingInvite = roomSummary.invitedMembersCount ?: 0 > 0
    -                if (pendingInvite) {
    -                    // wait for other to join
    -                    showDialogWithMessage(getString(R.string.cannot_call_yourself_with_invite))
    -                } else {
    -                    // You cannot place a call with yourself.
    -                    showDialogWithMessage(getString(R.string.cannot_call_yourself))
    -                }
    -            }
    -            2 -> {
    -                val currentCall = callManager.getCurrentCall()
    -                if (currentCall != null) {
    -                    // resume existing if same room, if not prompt to kill and then restart new call?
    -                    if (currentCall.mxCall.roomId == roomDetailArgs.roomId) {
    -                        onTapToReturnToCall()
    -                    } else {
    -                        safeStartCall(isVideoCall)
    -                    }
    -                } else if (!state.isAllowedToStartWebRTCCall) {
    -                    showDialogWithMessage(getString(
    -                            if (state.isDm()) {
    -                                R.string.no_permissions_to_start_webrtc_call_in_direct_room
    -                            } else {
    -                                R.string.no_permissions_to_start_webrtc_call
    -                            })
    -                    )
    -                } else {
    -                    safeStartCall(isVideoCall)
    -                }
    -            }
    -            else -> {
    -                // it's jitsi call
    -                // can you add widgets??
    -                if (!state.isAllowedToManageWidgets) {
    -                    // You do not have permission to start a conference call in this room
    -                    showDialogWithMessage(getString(
    -                            if (state.isDm()) {
    -                                R.string.no_permissions_to_start_conf_call_in_direct_room
    -                            } else {
    -                                R.string.no_permissions_to_start_conf_call
    -                            }
    -                    ))
    -                } else {
    -                    if (state.activeRoomWidgets()?.filter { it.type == WidgetType.Jitsi }?.any() == true) {
    -                        // A conference is already in progress!
    -                        showDialogWithMessage(getString(R.string.conference_call_in_progress))
    -                    } else {
    -                        AlertDialog.Builder(requireContext())
    -                                .setTitle(if (isVideoCall) R.string.video_meeting else R.string.audio_meeting)
    -                                .setMessage(R.string.audio_video_meeting_description)
    -                                .setPositiveButton(getString(R.string.create)) { _, _ ->
    -                                    // create the widget, then navigate to it..
    -                                    roomDetailViewModel.handle(RoomDetailAction.AddJitsiWidget(isVideoCall))
    -                                }
    -                                .setNegativeButton(getString(R.string.cancel), null)
    -                                .show()
    -                    }
    -                }
    -            }
    -        }
    -    }
    -
         private fun displayDisabledIntegrationDialog() {
             AlertDialog.Builder(requireActivity())
                     .setTitle(R.string.disabled_integration_dialog_title)
    @@ -864,54 +809,6 @@ class RoomDetailFragment @Inject constructor(
                     .show()
         }
     
    -    private fun safeStartCall(isVideoCall: Boolean) {
    -        if (vectorPreferences.preventAccidentalCall()) {
    -            AlertDialog.Builder(requireActivity())
    -                    .setMessage(if (isVideoCall) R.string.start_video_call_prompt_msg else R.string.start_voice_call_prompt_msg)
    -                    .setPositiveButton(if (isVideoCall) R.string.start_video_call else R.string.start_voice_call) { _, _ ->
    -                        safeStartCall2(isVideoCall)
    -                    }
    -                    .setNegativeButton(R.string.cancel, null)
    -                    .show()
    -        } else {
    -            safeStartCall2(isVideoCall)
    -        }
    -    }
    -
    -    private val startCallActivityResultLauncher = registerForPermissionsResult { allGranted ->
    -        if (allGranted) {
    -            (roomDetailViewModel.pendingAction as? RoomDetailAction.StartCall)?.let {
    -                roomDetailViewModel.pendingAction = null
    -                roomDetailViewModel.handle(it)
    -            }
    -        } else {
    -            context?.toast(R.string.permissions_action_not_performed_missing_permissions)
    -            cleanUpAfterPermissionNotGranted()
    -        }
    -    }
    -
    -    private fun safeStartCall2(isVideoCall: Boolean) {
    -        val startCallAction = RoomDetailAction.StartCall(isVideoCall)
    -        roomDetailViewModel.pendingAction = startCallAction
    -        if (isVideoCall) {
    -            if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL,
    -                            requireActivity(),
    -                            startCallActivityResultLauncher,
    -                            R.string.permissions_rationale_msg_camera_and_audio)) {
    -                roomDetailViewModel.pendingAction = null
    -                roomDetailViewModel.handle(startCallAction)
    -            }
    -        } else {
    -            if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL,
    -                            requireActivity(),
    -                            startCallActivityResultLauncher,
    -                            R.string.permissions_rationale_msg_record_audio)) {
    -                roomDetailViewModel.pendingAction = null
    -                roomDetailViewModel.handle(startCallAction)
    -            }
    -        }
    -    }
    -
         private fun renderRegularMode(text: String) {
             autoCompleter.exitSpecialMode()
             views.composerLayout.collapse()
    @@ -1051,6 +948,18 @@ class RoomDetailFragment @Inject constructor(
             }
         }
     
    +    private val startCallActivityResultLauncher = registerForPermissionsResult { allGranted ->
    +        if (allGranted) {
    +            (roomDetailViewModel.pendingAction as? RoomDetailAction.StartCall)?.let {
    +                roomDetailViewModel.pendingAction = null
    +                roomDetailViewModel.handle(it)
    +            }
    +        } else {
    +            context?.toast(R.string.permissions_action_not_performed_missing_permissions)
    +            cleanUpAfterPermissionNotGranted()
    +        }
    +    }
    +
     // PRIVATE METHODS *****************************************************************************
     
         private fun setupRecyclerView() {
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
    index 8c73cce1a5..d01b497e55 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
    @@ -31,9 +31,11 @@ import im.vector.app.R
     import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
    +import im.vector.app.features.call.dialpad.DialPadLookup
     import im.vector.app.features.call.webrtc.WebRtcCallManager
     import im.vector.app.features.command.CommandParser
     import im.vector.app.features.command.ParsedCommand
    +import im.vector.app.features.createdirect.DirectRoomHelper
     import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
     import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
     import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
    @@ -114,8 +116,10 @@ class RoomDetailViewModel @AssistedInject constructor(
             private val typingHelper: TypingHelper,
             private val callManager: WebRtcCallManager,
             private val chatEffectManager: ChatEffectManager,
    +        private val directRoomHelper: DirectRoomHelper,
             timelineSettingsFactory: TimelineSettingsFactory
    -) : VectorViewModel(initialState), Timeline.Listener, ChatEffectManager.Delegate {
    +) : VectorViewModel(initialState),
    +        Timeline.Listener, ChatEffectManager.Delegate, WebRtcCallManager.PSTNSupportListener {
     
         private val room = session.getRoom(initialState.roomId)!!
         private val eventId = initialState.eventId
    @@ -165,10 +169,12 @@ class RoomDetailViewModel @AssistedInject constructor(
             observeMyRoomMember()
             observeActiveRoomWidgets()
             observePowerLevel()
    +        updateShowDialerOptionState()
             room.getRoomSummaryLive()
             room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, NoOpMatrixCallback())
             // Inform the SDK that the room is displayed
             session.onRoomDisplayed(initialState.roomId)
    +        callManager.addPstnSupportListener(this)
             chatEffectManager.delegate = this
         }
     
    @@ -262,6 +268,7 @@ class RoomDetailViewModel @AssistedInject constructor(
                 is RoomDetailAction.TapOnFailedToDecrypt             -> handleTapOnFailedToDecrypt(action)
                 is RoomDetailAction.SelectStickerAttachment          -> handleSelectStickerAttachment()
                 is RoomDetailAction.OpenIntegrationManager           -> handleOpenIntegrationManager()
    +            is RoomDetailAction.StartCallWithPhoneNumber         -> handleStartCallWithPhoneNumber(action)
                 is RoomDetailAction.StartCall                        -> handleStartCall(action)
                 is RoomDetailAction.AcceptCall                       -> handleAcceptCall(action)
                 is RoomDetailAction.EndCall                          -> handleEndCall()
    @@ -285,6 +292,17 @@ class RoomDetailViewModel @AssistedInject constructor(
             }.exhaustive
         }
     
    +    private fun handleStartCallWithPhoneNumber(action: RoomDetailAction.StartCallWithPhoneNumber) {
    +        viewModelScope.launch {
    +            try {
    +                val result = DialPadLookup(session, directRoomHelper, callManager).lookupPhoneNumber(action.phoneNumber)
    +                callManager.startOutgoingCall(result.roomId, result.userId, action.videoCall)
    +            } catch (failure: Throwable) {
    +                _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure))
    +            }
    +        }
    +    }
    +
         private fun handleAcceptCall(action: RoomDetailAction.AcceptCall) {
             callManager.getCallById(action.callId)?.also {
                 _viewEvents.post(RoomDetailViewEvents.DisplayAndAcceptCall(it))
    @@ -315,18 +333,15 @@ class RoomDetailViewModel @AssistedInject constructor(
         }
     
         private fun handleOpenOrCreateDm(action: RoomDetailAction.OpenOrCreateDm) {
    -        val existingDmRoomId = session.getExistingDirectRoomWithUser(action.userId)
    -        if (existingDmRoomId == null) {
    -            // First create a direct room
    -            viewModelScope.launch(Dispatchers.IO) {
    -                val roomId = awaitCallback {
    -                    session.createDirectRoom(action.userId, it)
    -                }
    -                _viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId))
    +        viewModelScope.launch {
    +            val roomId = try {
    +                directRoomHelper.ensureDMExists(action.userId)
    +            } catch (failure: Throwable) {
    +                _viewEvents.post(RoomDetailViewEvents.ActionFailure(action, failure))
    +                return@launch
                 }
    -        } else {
    -            if (existingDmRoomId != initialState.roomId) {
    -                _viewEvents.post(RoomDetailViewEvents.OpenRoom(existingDmRoomId))
    +            if (roomId != initialState.roomId) {
    +                _viewEvents.post(RoomDetailViewEvents.OpenRoom(roomId = roomId))
                 }
             }
         }
    @@ -1418,6 +1433,16 @@ class RoomDetailViewModel @AssistedInject constructor(
             _viewEvents.post(RoomDetailViewEvents.OnNewTimelineEvents(eventIds))
         }
     
    +    override fun onPSTNSupportUpdated() {
    +       updateShowDialerOptionState()
    +    }
    +
    +    private fun updateShowDialerOptionState() {
    +        setState {
    +            copy(showDialerOption = callManager.supportsPSTNProtocol)
    +        }
    +    }
    +
         override fun onCleared() {
             roomSummariesHolder.remove(room.roomId)
             timeline.dispose()
    @@ -1427,6 +1452,7 @@ class RoomDetailViewModel @AssistedInject constructor(
             }
             chatEffectManager.delegate = null
             chatEffectManager.dispose()
    +        callManager.removePstnSupportListener(this)
             super.onCleared()
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
    index 38b93f9363..8c2b3ffe98 100644
    --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt
    @@ -74,7 +74,8 @@ data class RoomDetailViewState(
             val canSendMessage: Boolean = true,
             val canInvite: Boolean = true,
             val isAllowedToManageWidgets: Boolean = false,
    -        val isAllowedToStartWebRTCCall: Boolean = true
    +        val isAllowedToStartWebRTCCall: Boolean = true,
    +        val showDialerOption: Boolean = false
     ) : MvRxState {
     
         constructor(args: RoomDetailArgs) : this(
    diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt
    new file mode 100644
    index 0000000000..30f1ecdc6d
    --- /dev/null
    +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt
    @@ -0,0 +1,202 @@
    +/*
    + * Copyright (c) 2021 New Vector Ltd
    + *
    + * Licensed under the Apache License, Version 2.0 (the "License");
    + * you may not use this file except in compliance with the License.
    + * You may obtain a copy of the License at
    + *
    + *     http://www.apache.org/licenses/LICENSE-2.0
    + *
    + * Unless required by applicable law or agreed to in writing, software
    + * distributed under the License is distributed on an "AS IS" BASIS,
    + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    + * See the License for the specific language governing permissions and
    + * limitations under the License.
    + */
    +
    +package im.vector.app.features.home.room.detail
    +
    +import android.os.Bundle
    +import androidx.activity.result.ActivityResultLauncher
    +import androidx.appcompat.app.AlertDialog
    +import androidx.fragment.app.Fragment
    +import com.airbnb.mvrx.withState
    +import im.vector.app.R
    +import im.vector.app.core.platform.Restorable
    +import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL
    +import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL
    +import im.vector.app.core.utils.checkPermissions
    +import im.vector.app.features.call.DialerChoiceBottomSheet
    +import im.vector.app.features.call.dialpad.CallDialPadBottomSheet
    +import im.vector.app.features.call.dialpad.DialPadFragment
    +import im.vector.app.features.call.webrtc.WebRtcCallManager
    +import im.vector.app.features.settings.VectorPreferences
    +import org.matrix.android.sdk.api.session.widgets.model.WidgetType
    +
    +private const val DIALER_OPTION_TAG = "DIALER_OPTION_TAG"
    +private const val DIAL_PAD_TAG = "DIAL_PAD_TAG"
    +
    +class StartCallActionsHandler(
    +        private val roomId: String,
    +        private val fragment: Fragment,
    +        private val callManager: WebRtcCallManager,
    +        private val vectorPreferences: VectorPreferences,
    +        private val roomDetailViewModel: RoomDetailViewModel,
    +        private val startCallActivityResultLauncher: ActivityResultLauncher>,
    +        private val showDialogWithMessage: (String) -> Unit,
    +        private val onTapToReturnToCall: () -> Unit): Restorable {
    +
    +    fun onVideoCallClicked() {
    +        handleCallRequest(true)
    +    }
    +
    +    fun onVoiceCallClicked() = withState(roomDetailViewModel) {
    +        if (it.showDialerOption) {
    +            displayDialerChoiceBottomSheet()
    +        } else {
    +            handleCallRequest(false)
    +        }
    +    }
    +
    +    private fun DialerChoiceBottomSheet.applyListeners(): DialerChoiceBottomSheet {
    +        onDialPadClicked = ::displayDialPadBottomSheet
    +        onVoiceCallClicked = { handleCallRequest(false) }
    +        return this
    +    }
    +
    +    private fun CallDialPadBottomSheet.applyCallback(): CallDialPadBottomSheet {
    +        callback = object : DialPadFragment.Callback {
    +            override fun onOkClicked(formatted: String?, raw: String?) {
    +                if (raw.isNullOrEmpty()) return
    +                roomDetailViewModel.handle(RoomDetailAction.StartCallWithPhoneNumber(raw, false))
    +            }
    +        }
    +        return this
    +    }
    +
    +    private fun displayDialerChoiceBottomSheet() {
    +        DialerChoiceBottomSheet()
    +                .applyListeners()
    +                .show(fragment.parentFragmentManager, DIALER_OPTION_TAG)
    +    }
    +
    +    private fun displayDialPadBottomSheet() {
    +        CallDialPadBottomSheet.newInstance(true)
    +                .applyCallback()
    +                .show(fragment.parentFragmentManager, DIAL_PAD_TAG)
    +    }
    +
    +    private fun handleCallRequest(isVideoCall: Boolean) = withState(roomDetailViewModel) { state ->
    +        val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState
    +        when (roomSummary.joinedMembersCount) {
    +            1 -> {
    +                val pendingInvite = roomSummary.invitedMembersCount ?: 0 > 0
    +                if (pendingInvite) {
    +                    // wait for other to join
    +                    showDialogWithMessage(fragment.getString(R.string.cannot_call_yourself_with_invite))
    +                } else {
    +                    // You cannot place a call with yourself.
    +                    showDialogWithMessage(fragment.getString(R.string.cannot_call_yourself))
    +                }
    +            }
    +            2 -> {
    +                val currentCall = callManager.getCurrentCall()
    +                if (currentCall != null) {
    +                    // resume existing if same room, if not prompt to kill and then restart new call?
    +                    if (currentCall.roomId == roomId) {
    +                        onTapToReturnToCall()
    +                    }
    +                    //                        else {
    +                    // TODO might not work well, and should prompt
    +                    //                            webRtcPeerConnectionManager.endCall()
    +                    //                            safeStartCall(it, isVideoCall)
    +                    //                        }
    +                } else if (!state.isAllowedToStartWebRTCCall) {
    +                    showDialogWithMessage(fragment.getString(
    +                            if (state.isDm()) {
    +                                R.string.no_permissions_to_start_webrtc_call_in_direct_room
    +                            } else {
    +                                R.string.no_permissions_to_start_webrtc_call
    +                            })
    +                    )
    +                } else {
    +                    safeStartCall(isVideoCall)
    +                }
    +            }
    +            else -> {
    +                // it's jitsi call
    +                // can you add widgets??
    +                if (!state.isAllowedToManageWidgets) {
    +                    // You do not have permission to start a conference call in this room
    +                    showDialogWithMessage(fragment.getString(
    +                            if (state.isDm()) {
    +                                R.string.no_permissions_to_start_conf_call_in_direct_room
    +                            } else {
    +                                R.string.no_permissions_to_start_conf_call
    +                            }
    +                    ))
    +                } else {
    +                    if (state.activeRoomWidgets()?.filter { it.type == WidgetType.Jitsi }?.any() == true) {
    +                        // A conference is already in progress!
    +                        showDialogWithMessage(fragment.getString(R.string.conference_call_in_progress))
    +                    } else {
    +                        AlertDialog.Builder(fragment.requireContext())
    +                                .setTitle(if (isVideoCall) R.string.video_meeting else R.string.audio_meeting)
    +                                .setMessage(R.string.audio_video_meeting_description)
    +                                .setPositiveButton(fragment.getString(R.string.create)) { _, _ ->
    +                                    // create the widget, then navigate to it..
    +                                    roomDetailViewModel.handle(RoomDetailAction.AddJitsiWidget(isVideoCall))
    +                                }
    +                                .setNegativeButton(fragment.getString(R.string.cancel), null)
    +                                .show()
    +                    }
    +                }
    +            }
    +        }
    +    }
    +
    +    private fun safeStartCall(isVideoCall: Boolean) {
    +        if (vectorPreferences.preventAccidentalCall()) {
    +            AlertDialog.Builder(fragment.requireActivity())
    +                    .setMessage(if (isVideoCall) R.string.start_video_call_prompt_msg else R.string.start_voice_call_prompt_msg)
    +                    .setPositiveButton(if (isVideoCall) R.string.start_video_call else R.string.start_voice_call) { _, _ ->
    +                        safeStartCall2(isVideoCall)
    +                    }
    +                    .setNegativeButton(R.string.cancel, null)
    +                    .show()
    +        } else {
    +            safeStartCall2(isVideoCall)
    +        }
    +    }
    +
    +    private fun safeStartCall2(isVideoCall: Boolean) {
    +        val startCallAction = RoomDetailAction.StartCall(isVideoCall)
    +        roomDetailViewModel.pendingAction = startCallAction
    +        if (isVideoCall) {
    +            if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL,
    +                            fragment.requireActivity(),
    +                            startCallActivityResultLauncher,
    +                            R.string.permissions_rationale_msg_camera_and_audio)) {
    +                roomDetailViewModel.pendingAction = null
    +                roomDetailViewModel.handle(startCallAction)
    +            }
    +        } else {
    +            if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL,
    +                            fragment.requireActivity(),
    +                            startCallActivityResultLauncher,
    +                            R.string.permissions_rationale_msg_record_audio)) {
    +                roomDetailViewModel.pendingAction = null
    +                roomDetailViewModel.handle(startCallAction)
    +            }
    +        }
    +    }
    +
    +    override fun onSaveInstanceState(outState: Bundle) = Unit
    +
    +    override fun onRestoreInstanceState(savedInstanceState: Bundle?) {
    +        if (savedInstanceState != null) {
    +            (fragment.parentFragmentManager.findFragmentByTag(DIALER_OPTION_TAG) as? DialerChoiceBottomSheet)?.applyListeners()
    +            (fragment.parentFragmentManager.findFragmentByTag(DIAL_PAD_TAG) as? CallDialPadBottomSheet)?.applyCallback()
    +        }
    +    }
    +}
    diff --git a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt
    index 6e8a530c9a..c9c43fa80d 100644
    --- a/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/matrixto/MatrixToBottomSheetViewModel.kt
    @@ -30,8 +30,7 @@ import im.vector.app.R
     import im.vector.app.core.extensions.exhaustive
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
    -import im.vector.app.features.raw.wellknown.getElementWellknown
    -import im.vector.app.features.raw.wellknown.isE2EByDefault
    +import im.vector.app.features.createdirect.DirectRoomHelper
     import kotlinx.coroutines.Dispatchers
     import kotlinx.coroutines.launch
     import org.matrix.android.sdk.api.extensions.tryOrNull
    @@ -39,7 +38,6 @@ import org.matrix.android.sdk.api.raw.RawService
     import org.matrix.android.sdk.api.session.Session
     import org.matrix.android.sdk.api.session.permalinks.PermalinkData
     import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
    -import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
     import org.matrix.android.sdk.api.session.user.model.User
     import org.matrix.android.sdk.api.util.toMatrixItem
     import org.matrix.android.sdk.internal.util.awaitCallback
    @@ -48,6 +46,7 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor(
             @Assisted initialState: MatrixToBottomSheetState,
             private val session: Session,
             private val stringProvider: StringProvider,
    +        private val directRoomHelper: DirectRoomHelper,
             private val rawService: RawService) : VectorViewModel(initialState) {
     
         @AssistedInject.Factory
    @@ -76,8 +75,8 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor(
                 return
             }
     
    -        when (permalinkData) {
    -            is PermalinkData.UserLink -> {
    +        when (permalinkData)  {
    +            is PermalinkData.UserLink     -> {
                     val user = resolveUser(permalinkData.userId)
                     setState {
                         copy(
    @@ -86,11 +85,11 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor(
                         )
                     }
                 }
    -            is PermalinkData.RoomLink -> {
    +            is PermalinkData.RoomLink     -> {
                     // not yet supported
                     _viewEvents.post(MatrixToViewEvents.Dismiss)
                 }
    -            is PermalinkData.GroupLink -> {
    +            is PermalinkData.GroupLink    -> {
                     // not yet supported
                     _viewEvents.post(MatrixToViewEvents.Dismiss)
                 }
    @@ -125,42 +124,23 @@ class MatrixToBottomSheetViewModel @AssistedInject constructor(
         }
     
         private fun handleStartChatting(action: MatrixToAction.StartChattingWithUser) {
    -        val mxId = action.matrixItem.id
    -        val existing = session.getExistingDirectRoomWithUser(mxId)
    -        if (existing != null) {
    -            // navigate to this room
    -            _viewEvents.post(MatrixToViewEvents.NavigateToRoom(existing))
    -        } else {
    +        viewModelScope.launch {
                 setState {
                     copy(startChattingState = Loading())
                 }
    -            // we should create the room then navigate
    -            viewModelScope.launch(Dispatchers.IO) {
    -                val adminE2EByDefault = rawService.getElementWellknown(session.myUserId)
    -                        ?.isE2EByDefault()
    -                        ?: true
    -
    -                val roomParams = CreateRoomParams()
    -                        .apply {
    -                            invitedUserIds.add(mxId)
    -                            setDirectMessage()
    -                            enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault
    -                        }
    -
    -                val roomId = try {
    -                    awaitCallback { session.createRoom(roomParams, it) }
    -                } catch (failure: Throwable) {
    -                    setState {
    -                        copy(startChattingState = Fail(Exception(stringProvider.getString(R.string.invite_users_to_room_failure))))
    -                    }
    -                    return@launch
    -                }
    +            val roomId = try {
    +                directRoomHelper.ensureDMExists(action.matrixItem.id)
    +            } catch (failure: Throwable) {
                     setState {
    -                    // we can hide this button has we will navigate out
    -                    copy(startChattingState = Uninitialized)
    +                    copy(startChattingState = Fail(Exception(stringProvider.getString(R.string.invite_users_to_room_failure))))
                     }
    -                _viewEvents.post(MatrixToViewEvents.NavigateToRoom(roomId))
    +                return@launch
                 }
    +            setState {
    +                // we can hide this button has we will navigate out
    +                copy(startChattingState = Uninitialized)
    +            }
    +            _viewEvents.post(MatrixToViewEvents.NavigateToRoom(roomId))
             }
         }
     }
    diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt
    index 44044304bc..322daa8ec7 100644
    --- a/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/picker/RoomDirectoryPickerViewModel.kt
    @@ -16,8 +16,10 @@
     
     package im.vector.app.features.roomdirectory.picker
     
    +import androidx.lifecycle.viewModelScope
     import com.airbnb.mvrx.Fail
     import com.airbnb.mvrx.FragmentViewModelContext
    +import com.airbnb.mvrx.Loading
     import com.airbnb.mvrx.MvRxViewModelFactory
     import com.airbnb.mvrx.Success
     import com.airbnb.mvrx.ViewModelContext
    @@ -25,9 +27,8 @@ import com.squareup.inject.assisted.Assisted
     import com.squareup.inject.assisted.AssistedInject
     import im.vector.app.core.platform.EmptyViewEvents
     import im.vector.app.core.platform.VectorViewModel
    -import org.matrix.android.sdk.api.MatrixCallback
    +import kotlinx.coroutines.launch
     import org.matrix.android.sdk.api.session.Session
    -import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
     
     class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initialState: RoomDirectoryPickerViewState,
                                                                    private val session: Session)
    @@ -52,19 +53,21 @@ class RoomDirectoryPickerViewModel @AssistedInject constructor(@Assisted initial
         }
     
         private fun load() {
    -        session.getThirdPartyProtocol(object : MatrixCallback> {
    -            override fun onSuccess(data: Map) {
    -                setState {
    -                    copy(asyncThirdPartyRequest = Success(data))
    -                }
    +        viewModelScope.launch {
    +            setState {
    +                copy(asyncThirdPartyRequest = Loading())
                 }
    -
    -            override fun onFailure(failure: Throwable) {
    +            try {
    +                val thirdPartyProtocols = session.thirdPartyService().getThirdPartyProtocols()
    +                setState {
    +                    copy(asyncThirdPartyRequest = Success(thirdPartyProtocols))
    +                }
    +            } catch (failure: Throwable) {
                     setState {
                         copy(asyncThirdPartyRequest = Fail(failure))
                     }
                 }
    -        })
    +        }
         }
     
         override fun handle(action: RoomDirectoryPickerAction) {
    diff --git a/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt
    index 45b6f0ee65..51871a58ec 100644
    --- a/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt
    +++ b/vector/src/main/java/im/vector/app/features/usercode/UserCodeSharedViewModel.kt
    @@ -26,8 +26,7 @@ import com.squareup.inject.assisted.AssistedInject
     import im.vector.app.R
     import im.vector.app.core.platform.VectorViewModel
     import im.vector.app.core.resources.StringProvider
    -import im.vector.app.features.raw.wellknown.getElementWellknown
    -import im.vector.app.features.raw.wellknown.isE2EByDefault
    +import im.vector.app.features.createdirect.DirectRoomHelper
     import kotlinx.coroutines.Dispatchers
     import kotlinx.coroutines.launch
     import org.matrix.android.sdk.api.extensions.tryOrNull
    @@ -35,7 +34,6 @@ import org.matrix.android.sdk.api.raw.RawService
     import org.matrix.android.sdk.api.session.Session
     import org.matrix.android.sdk.api.session.permalinks.PermalinkData
     import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
    -import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
     import org.matrix.android.sdk.api.session.user.model.User
     import org.matrix.android.sdk.api.util.toMatrixItem
     import org.matrix.android.sdk.internal.util.awaitCallback
    @@ -44,6 +42,7 @@ class UserCodeSharedViewModel @AssistedInject constructor(
             @Assisted val initialState: UserCodeState,
             private val session: Session,
             private val stringProvider: StringProvider,
    +        private val directRoomHelper: DirectRoomHelper,
             private val rawService: RawService) : VectorViewModel(initialState) {
     
         companion object : MvRxViewModelFactory {
    @@ -95,39 +94,20 @@ class UserCodeSharedViewModel @AssistedInject constructor(
     
         private fun handleStartChatting(withUser: UserCodeActions.StartChattingWithUser) {
             val mxId = withUser.matrixItem.id
    -        val existing = session.getExistingDirectRoomWithUser(mxId)
             setState {
                 copy(mode = UserCodeState.Mode.SHOW)
             }
    -        if (existing != null) {
    -            // navigate to this room
    -            _viewEvents.post(UserCodeShareViewEvents.NavigateToRoom(existing))
    -        } else {
    -            // we should create the room then navigate
    -            _viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen)
    -            viewModelScope.launch(Dispatchers.IO) {
    -                val adminE2EByDefault = rawService.getElementWellknown(session.myUserId)
    -                        ?.isE2EByDefault()
    -                        ?: true
    -
    -                val roomParams = CreateRoomParams()
    -                        .apply {
    -                            invitedUserIds.add(mxId)
    -                            setDirectMessage()
    -                            enableEncryptionIfInvitedUsersSupportIt = adminE2EByDefault
    -                        }
    -
    -                val roomId =
    -                        try {
    -                            awaitCallback { session.createRoom(roomParams, it) }
    -                        } catch (failure: Throwable) {
    -                            _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.invite_users_to_room_failure)))
    -                            return@launch
    -                        } finally {
    -                            _viewEvents.post(UserCodeShareViewEvents.HideWaitingScreen)
    -                        }
    -                _viewEvents.post(UserCodeShareViewEvents.NavigateToRoom(roomId))
    +        _viewEvents.post(UserCodeShareViewEvents.ShowWaitingScreen)
    +        viewModelScope.launch(Dispatchers.IO) {
    +            val roomId = try {
    +                directRoomHelper.ensureDMExists(mxId)
    +            } catch (failure: Throwable) {
    +                _viewEvents.post(UserCodeShareViewEvents.ToastMessage(stringProvider.getString(R.string.invite_users_to_room_failure)))
    +                return@launch
    +            } finally {
    +                _viewEvents.post(UserCodeShareViewEvents.HideWaitingScreen)
                 }
    +            _viewEvents.post(UserCodeShareViewEvents.NavigateToRoom(roomId))
             }
         }
     
    diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt
    index 331329ae61..a7ec9cd8c3 100644
    --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt
    +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListController.kt
    @@ -64,13 +64,15 @@ class UserListController @Inject constructor(private val session: Session,
                         })
                     }
                 }
    -            actionItem {
    -                id(R.drawable.ic_baseline_perm_contact_calendar_24)
    -                title(stringProvider.getString(R.string.contacts_book_title))
    -                actionIconRes(R.drawable.ic_baseline_perm_contact_calendar_24)
    -                clickAction(View.OnClickListener {
    -                    callback?.onContactBookClick()
    -                })
    +            if (currentState.showContactBookAction) {
    +                actionItem {
    +                    id(R.drawable.ic_baseline_perm_contact_calendar_24)
    +                    title(stringProvider.getString(R.string.contacts_book_title))
    +                    actionIconRes(R.drawable.ic_baseline_perm_contact_calendar_24)
    +                    clickAction(View.OnClickListener {
    +                        callback?.onContactBookClick()
    +                    })
    +                }
                 }
                 if (currentState.showInviteActions()) {
                     actionItem {
    diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt
    index dce1f46b2f..795d45272c 100644
    --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt
    +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListFragmentArgs.kt
    @@ -26,5 +26,6 @@ data class UserListFragmentArgs(
             val excludedUserIds: Set? = null,
             val singleSelection: Boolean = false,
             val showInviteActions: Boolean = true,
    +        val showContactBookAction: Boolean = true,
             val showToolbar: Boolean = true
     ) : Parcelable
    diff --git a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt
    index 60ba3a17da..f1cbbd3b9d 100644
    --- a/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt
    +++ b/vector/src/main/java/im/vector/app/features/userdirectory/UserListViewState.kt
    @@ -31,13 +31,15 @@ data class UserListViewState(
             val pendingSelections: Set = emptySet(),
             val searchTerm: String = "",
             val singleSelection: Boolean,
    -        private val showInviteActions: Boolean
    +        private val showInviteActions: Boolean,
    +        val showContactBookAction: Boolean
     ) : MvRxState {
     
         constructor(args: UserListFragmentArgs) : this(
                 excludedUserIds = args.excludedUserIds,
                 singleSelection = args.singleSelection,
    -            showInviteActions = args.showInviteActions
    +            showInviteActions = args.showInviteActions,
    +            showContactBookAction = args.showContactBookAction
         )
     
         fun getSelectedMatrixId(): List {
    diff --git a/vector/src/main/res/drawable/ic_call_dial_pad.xml b/vector/src/main/res/drawable/ic_call_dial_pad.xml
    new file mode 100644
    index 0000000000..a917d592bc
    --- /dev/null
    +++ b/vector/src/main/res/drawable/ic_call_dial_pad.xml
    @@ -0,0 +1,9 @@
    +
    +  
    +
    diff --git a/vector/src/main/res/layout/activity_call_transfer.xml b/vector/src/main/res/layout/activity_call_transfer.xml
    index e4e50b2ce2..64ddd29319 100644
    --- a/vector/src/main/res/layout/activity_call_transfer.xml
    +++ b/vector/src/main/res/layout/activity_call_transfer.xml
    @@ -17,14 +17,24 @@
                 android:elevation="4dp"
                 app:layout_constraintTop_toTopOf="parent" />
     
    -        
    +
    +        
    +            app:layout_constraintTop_toBottomOf="@id/callTransferTabLayout"
    +            app:layout_behavior="@string/appbar_scrolling_view_behavior" />
     
             
     
    -
             
    diff --git a/vector/src/main/res/layout/bottom_sheet_call_controls.xml b/vector/src/main/res/layout/bottom_sheet_call_controls.xml
    index 515beff368..f30d44b731 100644
    --- a/vector/src/main/res/layout/bottom_sheet_call_controls.xml
    +++ b/vector/src/main/res/layout/bottom_sheet_call_controls.xml
    @@ -15,6 +15,7 @@
             app:actionTitle="@string/call_select_sound_device"
             app:leftIcon="@drawable/ic_call_speaker_default"
             app:tint="?attr/riotx_text_primary"
    +        app:titleTextColor="?attr/riotx_text_primary"
             tools:actionDescription="Speaker" />
     
         
     
    +    
    +
         
     
         
     
         
     
     
    diff --git a/vector/src/main/res/layout/bottom_sheet_call_dial_pad.xml b/vector/src/main/res/layout/bottom_sheet_call_dial_pad.xml
    new file mode 100644
    index 0000000000..af82f3946b
    --- /dev/null
    +++ b/vector/src/main/res/layout/bottom_sheet_call_dial_pad.xml
    @@ -0,0 +1,42 @@
    +
    +
    +
    +    
    +
    +        
    +
    +        
    +
    +    
    +
    +    
    +
    +
    diff --git a/vector/src/main/res/layout/bottom_sheet_call_dialer_choice.xml b/vector/src/main/res/layout/bottom_sheet_call_dialer_choice.xml
    new file mode 100644
    index 0000000000..7d228d4a2f
    --- /dev/null
    +++ b/vector/src/main/res/layout/bottom_sheet_call_dialer_choice.xml
    @@ -0,0 +1,30 @@
    +
    +
    +
    +    
    +
    +    
    +
    +
    +
    diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
    index 67c6a33da2..4ce4bd2adf 100644
    --- a/vector/src/main/res/values/strings.xml
    +++ b/vector/src/main/res/values/strings.xml
    @@ -2777,6 +2777,9 @@
         This call has ended
         Call back
     
    +    Dial pad
    +    "There was an error looking up the phone number"
    +
     
         Active call (%1$s)
         
    @@ -2788,11 +2791,11 @@
             1 active call (%1$s) ยท %2$d paused calls
         
     
    -
         Consult first
         Connect
         Transfer
    -    An error occured while transfering call
    +    An error occurred while transferring call
    +    Users
     
     
     
    diff --git a/vector/src/main/res/values/styles_dial_pad.xml b/vector/src/main/res/values/styles_dial_pad.xml
    new file mode 100644
    index 0000000000..614923caad
    --- /dev/null
    +++ b/vector/src/main/res/values/styles_dial_pad.xml
    @@ -0,0 +1,12 @@
    +
    +
    +    
    +
    \ No newline at end of file