mirror of
https://github.com/element-hq/element-android
synced 2024-11-24 10:25:35 +03:00
Merge branch 'feature/fga/voip_dial_pad' into feature/fga/voip_v1_start
This commit is contained in:
commit
940563f663
52 changed files with 1432 additions and 342 deletions
|
@ -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.
|
||||
|
|
|
@ -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<PublicRoomsResponse>): 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<Map<String, ThirdPartyProtocol>>): Cancelable
|
||||
|
||||
/**
|
||||
* Get the visibility of a room in the directory
|
||||
*/
|
||||
|
|
|
@ -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<String, ThirdPartyProtocol>
|
||||
|
||||
/**
|
||||
* 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<String, String> = emptyMap()): List<ThirdPartyUser>
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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<AccountService>,
|
||||
private val defaultIdentityService: DefaultIdentityService,
|
||||
private val integrationManagerService: IntegrationManagerService,
|
||||
private val thirdPartyService: Lazy<ThirdPartyService>,
|
||||
private val callSignalingService: Lazy<CallSignalingService>,
|
||||
@UnauthenticatedWithCertificate
|
||||
private val unauthenticatedWithCertificateOkHttpClient: Lazy<OkHttpClient>,
|
||||
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<Map<String, ThirdPartyProtocol>>): Cancelable {
|
||||
return getThirdPartyProtocolsTask
|
||||
.configureWith {
|
||||
this.callback = callback
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
override suspend fun getRoomDirectoryVisibility(roomId: String): RoomDirectoryVisibility {
|
||||
return getRoomDirectoryVisibilityTask.execute(GetRoomDirectoryVisibilityTask.Params(roomId))
|
||||
}
|
||||
|
|
|
@ -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<Map<String, ThirdPartyProtocol>>
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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<String, ThirdPartyProtocol> {
|
||||
return getThirdPartyProtocolTask.execute(Unit)
|
||||
}
|
||||
|
||||
override suspend fun getThirdPartyUser(protocol: String, fields: Map<String, String>): List<ThirdPartyUser> {
|
||||
val taskParams = GetThirdPartyUserTask.Params(
|
||||
protocol = protocol,
|
||||
fields = fields
|
||||
)
|
||||
return getThirdPartyUserTask.execute(taskParams)
|
||||
}
|
||||
}
|
|
@ -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<Unit, Map<String, ThirdPartyProtocol>>
|
||||
|
||||
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<String, ThirdPartyProtocol> {
|
||||
return executeRequest(globalErrorReceiver) {
|
||||
apiCall = roomAPI.thirdPartyProtocols()
|
||||
apiCall = thirdPartyAPI.thirdPartyProtocols()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<GetThirdPartyUserTask.Params, List<ThirdPartyUser>> {
|
||||
|
||||
data class Params(
|
||||
val protocol: String,
|
||||
val fields: Map<String, String> = emptyMap()
|
||||
)
|
||||
}
|
||||
|
||||
internal class DefaultGetThirdPartyUserTask @Inject constructor(
|
||||
private val thirdPartyAPI: ThirdPartyAPI,
|
||||
private val globalErrorReceiver: GlobalErrorReceiver
|
||||
) : GetThirdPartyUserTask {
|
||||
|
||||
override suspend fun execute(params: GetThirdPartyUserTask.Params): List<ThirdPartyUser> {
|
||||
return executeRequest(globalErrorReceiver) {
|
||||
apiCall = thirdPartyAPI.getThirdPartyUser(params.protocol, params.fields)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Map<String, ThirdPartyProtocol>>
|
||||
|
||||
/**
|
||||
* 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<String, String>?): Call<List<ThirdPartyUser>>
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -385,6 +385,11 @@ SOFTWARE.
|
|||
<br/>
|
||||
Copyright 2016 JetRadar
|
||||
</li>
|
||||
<li>
|
||||
<b>dialogs / android-dialer</b>
|
||||
<br/>
|
||||
Copyright (c) 2017-present, dialog LLC <info@dlg.im>
|
||||
</li>
|
||||
</ul>
|
||||
<pre>
|
||||
Apache License
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -64,6 +64,10 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
|
|||
dismiss()
|
||||
}
|
||||
|
||||
views.callControlsOpenDialPad.views.bottomSheetActionClickableZone.debouncedClicks {
|
||||
callViewModel.handle(VectorCallViewActions.OpenDialPad)
|
||||
}
|
||||
|
||||
views.callControlsTransfer.views.bottomSheetActionClickableZone.debouncedClicks {
|
||||
callViewModel.handle(VectorCallViewActions.InitiateCallTransfer)
|
||||
dismiss()
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.app.databinding.BottomSheetCallDialerChoiceBinding
|
||||
|
||||
class DialerChoiceBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetCallDialerChoiceBinding>() {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ActivityCallBinding>(), 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<ActivityCallBinding>(), 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<ActivityCallBinding>(), 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<ActivityCallBinding>(), 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<ActivityCallBinding>(), 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"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -28,6 +28,7 @@ sealed class VectorCallViewEvents : VectorViewEvents {
|
|||
val available: Set<CallAudioManager.Device>,
|
||||
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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<BottomSheetCallDialPadBinding>() {
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<View>(R.id.dialpad_view) as DialpadView
|
||||
dialpadView.findViewById<View>(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<View>(R.id.zero).setOnClickListener { append('0') }
|
||||
if (enablePlus) {
|
||||
dialpadView.findViewById<View>(R.id.zero).setOnLongClickListener {
|
||||
append('+')
|
||||
true
|
||||
}
|
||||
}
|
||||
dialpadView.findViewById<View>(R.id.one).setOnClickListener { append('1') }
|
||||
dialpadView.findViewById<View>(R.id.two).setOnClickListener { append('2') }
|
||||
dialpadView.findViewById<View>(R.id.three).setOnClickListener { append('3') }
|
||||
dialpadView.findViewById<View>(R.id.four).setOnClickListener { append('4') }
|
||||
dialpadView.findViewById<View>(R.id.four).setOnClickListener { append('4') }
|
||||
dialpadView.findViewById<View>(R.id.five).setOnClickListener { append('5') }
|
||||
dialpadView.findViewById<View>(R.id.six).setOnClickListener { append('6') }
|
||||
dialpadView.findViewById<View>(R.id.seven).setOnClickListener { append('7') }
|
||||
dialpadView.findViewById<View>(R.id.eight).setOnClickListener { append('8') }
|
||||
dialpadView.findViewById<View>(R.id.nine).setOnClickListener { append('9') }
|
||||
if (enableStar) {
|
||||
dialpadView.findViewById<View>(R.id.star).setOnClickListener { append('*') }
|
||||
} else {
|
||||
dialpadView.findViewById<View>(R.id.star).isVisible = false
|
||||
}
|
||||
if (enablePound) {
|
||||
dialpadView.findViewById<View>(R.id.pound).setOnClickListener { append('#') }
|
||||
} else {
|
||||
dialpadView.findViewById<View>(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<View>(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"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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<ActivityCallTransferBinding>(),
|
|||
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<ActivityCallTransferBinding>(),
|
|||
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<ActivityCallTransferBinding>(),
|
|||
|
||||
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<String>, 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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String, ThirdPartyProtocol> = 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
|
||||
}
|
||||
}
|
|
@ -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<SurfaceViewRenderer>?) {
|
||||
Timber.v("## VOIP detachRenderers")
|
||||
if (renderers.isNullOrEmpty()) {
|
||||
|
|
|
@ -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<PSTNSupportListener>().toMutableList()
|
||||
fun addPstnSupportListener(listener: PSTNSupportListener) {
|
||||
pstnSupportListeners.add(listener)
|
||||
}
|
||||
|
||||
fun removePstnSupportListener(listener: PSTNSupportListener) {
|
||||
pstnSupportListeners.remove(listener)
|
||||
}
|
||||
|
||||
private val currentCallsListeners = CopyOnWriteArrayList<CurrentCallListener>()
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), Timeline.Listener, ChatEffectManager.Delegate {
|
||||
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(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<String> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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<Array<String>>,
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<MatrixToBottomSheetState, MatrixToAction, MatrixToViewEvents>(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<String> { 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Map<String, ThirdPartyProtocol>> {
|
||||
override fun onSuccess(data: Map<String, ThirdPartyProtocol>) {
|
||||
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) {
|
||||
|
|
|
@ -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<UserCodeState, UserCodeActions, UserCodeShareViewEvents>(initialState) {
|
||||
|
||||
companion object : MvRxViewModelFactory<UserCodeSharedViewModel, UserCodeState> {
|
||||
|
@ -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<String> { 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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -26,5 +26,6 @@ data class UserListFragmentArgs(
|
|||
val excludedUserIds: Set<String>? = null,
|
||||
val singleSelection: Boolean = false,
|
||||
val showInviteActions: Boolean = true,
|
||||
val showContactBookAction: Boolean = true,
|
||||
val showToolbar: Boolean = true
|
||||
) : Parcelable
|
||||
|
|
|
@ -31,13 +31,15 @@ data class UserListViewState(
|
|||
val pendingSelections: Set<PendingSelection> = 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<String> {
|
||||
|
|
9
vector/src/main/res/drawable/ic_call_dial_pad.xml
Normal file
9
vector/src/main/res/drawable/ic_call_dial_pad.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="22dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="22">
|
||||
<path
|
||||
android:pathData="M8,18C6.9,18 6,18.9 6,20C6,21.1 6.9,22 8,22C9.1,22 10,21.1 10,20C10,18.9 9.1,18 8,18ZM2,0C0.9,0 0,0.9 0,2C0,3.1 0.9,4 2,4C3.1,4 4,3.1 4,2C4,0.9 3.1,0 2,0ZM2,6C0.9,6 0,6.9 0,8C0,9.1 0.9,10 2,10C3.1,10 4,9.1 4,8C4,6.9 3.1,6 2,6ZM2,12C0.9,12 0,12.9 0,14C0,15.1 0.9,16 2,16C3.1,16 4,15.1 4,14C4,12.9 3.1,12 2,12ZM14,4C15.1,4 16,3.1 16,2C16,0.9 15.1,0 14,0C12.9,0 12,0.9 12,2C12,3.1 12.9,4 14,4ZM8,12C6.9,12 6,12.9 6,14C6,15.1 6.9,16 8,16C9.1,16 10,15.1 10,14C10,12.9 9.1,12 8,12ZM14,12C12.9,12 12,12.9 12,14C12,15.1 12.9,16 14,16C15.1,16 16,15.1 16,14C16,12.9 15.1,12 14,12ZM14,6C12.9,6 12,6.9 12,8C12,9.1 12.9,10 14,10C15.1,10 16,9.1 16,8C16,6.9 15.1,6 14,6ZM8,6C6.9,6 6,6.9 6,8C6,9.1 6.9,10 8,10C9.1,10 10,9.1 10,8C10,6.9 9.1,6 8,6ZM8,0C6.9,0 6,0.9 6,2C6,3.1 6.9,4 8,4C9.1,4 10,3.1 10,2C10,0.9 9.1,0 8,0Z"
|
||||
android:fillColor="#C1C6CD"/>
|
||||
</vector>
|
|
@ -17,14 +17,24 @@
|
|||
android:elevation="4dp"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/callTransferFragmentContainer"
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/callTransferTabLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@+id/callTransferToolbar"
|
||||
app:tabGravity="fill"
|
||||
app:tabMaxWidth="0dp"
|
||||
app:tabMode="fixed" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/callTransferViewPager"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/callTransferActionsLayout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/callTransferToolbar" />
|
||||
app:layout_constraintTop_toBottomOf="@id/callTransferTabLayout"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<RelativeLayout
|
||||
android:background="?riotx_header_panel_background"
|
||||
|
@ -67,7 +77,6 @@
|
|||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
<include
|
||||
android:id="@+id/waiting_view"
|
||||
layout="@layout/merge_overlay_waiting_view" />
|
||||
|
|
|
@ -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" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
|
@ -24,8 +25,19 @@
|
|||
app:actionTitle="@string/call_switch_camera"
|
||||
app:leftIcon="@drawable/ic_video_flip"
|
||||
app:tint="?attr/riotx_text_primary"
|
||||
app:titleTextColor="?attr/riotx_text_primary"
|
||||
tools:actionDescription="Front" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/callControlsOpenDialPad"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/call_dial_pad_title"
|
||||
app:leftIcon="@drawable/ic_call_dial_pad"
|
||||
app:tint="?attr/riotx_text_primary"
|
||||
app:titleTextColor="?attr/riotx_text_primary"
|
||||
tools:actionDescription="" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/callControlsToggleSDHD"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -33,6 +45,7 @@
|
|||
app:actionTitle="@string/call_format_turn_hd_on"
|
||||
app:leftIcon="@drawable/ic_hd"
|
||||
app:tint="?attr/riotx_text_primary"
|
||||
app:titleTextColor="?attr/riotx_text_primary"
|
||||
tools:actionDescription="Front" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
|
@ -42,6 +55,7 @@
|
|||
app:actionTitle="Hold/resume"
|
||||
app:leftIcon="@drawable/ic_call_hold_action"
|
||||
app:tint="?attr/riotx_text_primary"
|
||||
app:titleTextColor="?attr/riotx_text_primary"
|
||||
tools:actionDescription="" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
|
@ -51,6 +65,7 @@
|
|||
app:actionTitle="@string/call_transfer_title"
|
||||
app:leftIcon="@drawable/ic_call_transfer"
|
||||
app:tint="?attr/riotx_text_primary"
|
||||
app:titleTextColor="?attr/riotx_text_primary"
|
||||
tools:actionDescription="" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
42
vector/src/main/res/layout/bottom_sheet_call_dial_pad.xml
Normal file
42
vector/src/main/res/layout/bottom_sheet_call_dial_pad.xml
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/callDialPad"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?riotx_bottom_sheet_background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/callDialPadTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:text="@string/call_dial_pad_title"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="20sp" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/callDialPadClose"
|
||||
android:layout_width="@dimen/layout_touch_size"
|
||||
android:layout_height="@dimen/layout_touch_size"
|
||||
android:scaleType="center"
|
||||
app:tint="?riotx_text_primary"
|
||||
android:foreground="?selectableItemBackground"
|
||||
android:src="@drawable/ic_cross" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/callDialPadFragmentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/callControlsWrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?riotx_bottom_sheet_background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/dialerChoiceDialPad"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/call_dial_pad_title"
|
||||
app:leftIcon="@drawable/ic_call_dial_pad"
|
||||
app:tint="?attr/riotx_text_primary"
|
||||
app:titleTextColor="?attr/riotx_text_primary" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/dialerChoiceVoiceCall"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/action_voice_call"
|
||||
app:leftIcon="@drawable/ic_call_answer"
|
||||
app:tint="?attr/riotx_text_primary"
|
||||
app:titleTextColor="?attr/riotx_text_primary" />
|
||||
|
||||
|
||||
</LinearLayout>
|
|
@ -2777,6 +2777,9 @@
|
|||
<string name="call_tile_ended">This call has ended</string>
|
||||
<string name="call_tile_call_back">Call back</string>
|
||||
|
||||
<string name="call_dial_pad_title">Dial pad</string>
|
||||
<string name="call_dial_pad_lookup_error">"There was an error looking up the phone number"</string>
|
||||
|
||||
|
||||
<string name="call_only_active">Active call (%1$s)</string>
|
||||
<plurals name="call_only_paused">
|
||||
|
@ -2788,11 +2791,11 @@
|
|||
<item quantity="other">1 active call (%1$s) · %2$d paused calls</item>
|
||||
</plurals>
|
||||
|
||||
|
||||
<string name="call_transfer_consult_first">Consult first</string>
|
||||
<string name="call_transfer_connect_action">Connect</string>
|
||||
<string name="call_transfer_title">Transfer</string>
|
||||
<string name="call_transfer_failure">An error occured while transfering call</string>
|
||||
<string name="call_transfer_failure">An error occurred while transferring call</string>
|
||||
<string name="call_transfer_users_tab_title">Users</string>
|
||||
|
||||
|
||||
</resources>
|
||||
|
|
12
vector/src/main/res/values/styles_dial_pad.xml
Normal file
12
vector/src/main/res/values/styles_dial_pad.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="DialpadKeyNumberStyle">
|
||||
<item name="android:textColor">?attr/riotx_text_primary</item>
|
||||
<item name="android:textSize">@dimen/dialpad_key_numbers_default_size</item>
|
||||
<item name="android:fontFamily">sans-serif-light</item>
|
||||
<item name="android:layout_width">wrap_content</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
<item name="android:layout_marginBottom">@dimen/dialpad_key_number_default_margin_bottom</item>
|
||||
<item name="android:gravity">center</item>
|
||||
</style>
|
||||
</resources>
|
Loading…
Reference in a new issue