Merge branch 'feature/fga/voip_dial_pad' into feature/fga/voip_v1_start

This commit is contained in:
ganfra 2021-01-29 16:37:46 +01:00
commit 940563f663
52 changed files with 1432 additions and 342 deletions

View file

@ -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.

View file

@ -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
*/

View file

@ -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>
}

View file

@ -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
)

View file

@ -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()
}

View file

@ -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

View file

@ -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))
}

View file

@ -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.

View file

@ -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

View file

@ -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)
}
}

View file

@ -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()
}
}
}

View file

@ -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)
}
}
}

View file

@ -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>>
}

View file

@ -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
}

View file

@ -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"

View file

@ -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

View file

@ -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)
}

View file

@ -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()

View file

@ -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()
}
}
}

View file

@ -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"

View file

@ -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()

View file

@ -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()

View file

@ -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

View file

@ -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()
}
}
}

View file

@ -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"
}
}

View file

@ -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)
}
}

View file

@ -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()
}

View file

@ -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 {

View file

@ -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
}
}

View file

@ -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)
}
}
}
}

View file

@ -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
}
}

View file

@ -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()) {

View file

@ -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

View file

@ -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
}
}

View file

@ -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()

View file

@ -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() {

View file

@ -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()
}
}

View file

@ -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(

View file

@ -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()
}
}
}

View file

@ -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))
}
}
}

View file

@ -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) {

View file

@ -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))
}
}

View file

@ -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 {

View file

@ -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

View file

@ -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> {

View 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>

View file

@ -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" />

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -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>

View 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>