diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallListener.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallListener.kt index 303add747f..d17be59cd4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallListener.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/call/CallListener.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.api.session.call import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallAssertedIdentityContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent @@ -61,4 +62,9 @@ interface CallListener { * Called when the call has been managed by an other session */ fun onCallManagedByOtherSession(callId: String) + + /** + * Called when an asserted identity event is received + */ + fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 229a53fa9d..9c3fdd57da 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -76,6 +76,8 @@ object EventType { const val CALL_NEGOTIATE = "m.call.negotiate" const val CALL_REJECT = "m.call.reject" const val CALL_HANGUP = "m.call.hangup" + const val CALL_ASSERTED_IDENTITY = "m.call.asserted_identity" + const val CALL_ASSERTED_IDENTITY_PREFIX = "org.matrix.call.asserted_identity" // This type is not processed by the client, just sent to the server const val CALL_REPLACES = "m.call.replaces" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAssertedIdentityContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAssertedIdentityContent.kt new file mode 100644 index 0000000000..4c5413425f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/call/CallAssertedIdentityContent.kt @@ -0,0 +1,57 @@ +/* + * Copyright 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.room.model.call + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * This event is sent by the callee when they wish to answer the call. + */ +@JsonClass(generateAdapter = true) +data class CallAssertedIdentityContent( + /** + * Required. The ID of the call this event relates to. + */ + @Json(name = "call_id") override val callId: String, + /** + * Required. ID to let user identify remote echo of their own events + */ + @Json(name = "party_id") override val partyId: String? = null, + /** + * Required. The version of the VoIP specification this messages adheres to. + */ + @Json(name = "version") override val version: String?, + + /** + * Optional. Used to inform the transferee who they're now speaking to. + */ + @Json(name = "asserted_identity") val assertedIdentity: AssertedIdentity? = null +) : CallSignalingContent { + + /** + * A user ID may be included if relevant, but unlike target_user, it is purely informational. + * The asserted identity may not represent a matrix user at all, + * in which case just a display_name may be given, or a perhaps a display_name and avatar_url. + */ + @JsonClass(generateAdapter = true) + data class AssertedIdentity( + @Json(name = "id") val id: String? = null, + @Json(name = "display_name") val displayName: String? = null, + @Json(name = "avatar_url") val avatarUrl: String? = null + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt index a190ff62ac..8bf2014639 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallEventProcessor.kt @@ -37,7 +37,9 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH EventType.CALL_CANDIDATES, EventType.CALL_INVITE, EventType.CALL_HANGUP, - EventType.ENCRYPTED + EventType.ENCRYPTED, + EventType.CALL_ASSERTED_IDENTITY, + EventType.CALL_ASSERTED_IDENTITY_PREFIX ) private val eventsToPostProcess = mutableListOf() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallListenersDispatcher.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallListenersDispatcher.kt index 1de2d8a106..dad17f4ac9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallListenersDispatcher.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallListenersDispatcher.kt @@ -20,6 +20,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.call.CallListener import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallAssertedIdentityContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent @@ -64,6 +65,10 @@ internal class CallListenersDispatcher(private val listeners: Set) it.onCallNegotiateReceived(callNegotiateContent) } + override fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) = dispatch { + it.onCallAssertedIdentityReceived(callAssertedIdentityContent) + } + private fun dispatch(lambda: (CallListener) -> Unit) { listeners.toList().forEach { tryOrNull { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt index 61ea660b60..d18737d31b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/call/CallSignalingHandler.kt @@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallAssertedIdentityContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent @@ -53,30 +54,44 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa fun onCallEvent(event: Event) { when (event.getClearType()) { - EventType.CALL_ANSWER -> { + EventType.CALL_ANSWER -> { handleCallAnswerEvent(event) } - EventType.CALL_INVITE -> { + EventType.CALL_INVITE -> { handleCallInviteEvent(event) } - EventType.CALL_HANGUP -> { + EventType.CALL_HANGUP -> { handleCallHangupEvent(event) } - EventType.CALL_REJECT -> { + EventType.CALL_REJECT -> { handleCallRejectEvent(event) } - EventType.CALL_CANDIDATES -> { + EventType.CALL_CANDIDATES -> { handleCallCandidatesEvent(event) } - EventType.CALL_SELECT_ANSWER -> { + EventType.CALL_SELECT_ANSWER -> { handleCallSelectAnswerEvent(event) } - EventType.CALL_NEGOTIATE -> { + EventType.CALL_NEGOTIATE -> { handleCallNegotiateEvent(event) } + EventType.CALL_ASSERTED_IDENTITY, + EventType.CALL_ASSERTED_IDENTITY_PREFIX -> { + handleCallAssertedIdentityEvent(event) + } } } + private fun handleCallAssertedIdentityEvent(event: Event) { + val content = event.getClearContent().toModel() ?: return + val call = content.getCall() ?: return + if (call.ourPartyId == content.partyId) { + // Ignore remote echo (not that we send asserted identity, but still...) + return + } + callListenersDispatcher.onCallAssertedIdentityReceived(content) + } + private fun handleCallNegotiateEvent(event: Event) { val content = event.getClearContent().toModel() ?: return val call = content.getCall() ?: return diff --git a/newsfragment/3451.feature b/newsfragment/3451.feature new file mode 100644 index 0000000000..dbf1a9b02c --- /dev/null +++ b/newsfragment/3451.feature @@ -0,0 +1 @@ +Adds support for receiving MSC3086 Asserted Identity events. \ No newline at end of file diff --git a/vector/build.gradle b/vector/build.gradle index a94f796a90..32707da935 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -144,6 +144,10 @@ android { buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping" + // If set, MSC3086 asserted identity messages sent on VoIP calls will cause the call to appear in the room corresponding to the asserted identity. + // This *must* only be set in trusted environments. + buildConfigField "Boolean", "handleCallAssertedIdentityEvents", "false" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // Keep abiFilter for the universalApk diff --git a/vector/src/main/java/im/vector/app/core/glide/AvatarPlaceholder.kt b/vector/src/main/java/im/vector/app/core/glide/AvatarPlaceholder.kt new file mode 100644 index 0000000000..32968c4f80 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/glide/AvatarPlaceholder.kt @@ -0,0 +1,82 @@ +/* + * 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.core.glide + +import android.content.Context +import android.graphics.drawable.Drawable +import com.bumptech.glide.Priority +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.Options +import com.bumptech.glide.load.data.DataFetcher +import com.bumptech.glide.load.model.ModelLoader +import com.bumptech.glide.load.model.ModelLoaderFactory +import com.bumptech.glide.load.model.MultiModelLoaderFactory +import com.bumptech.glide.signature.ObjectKey +import im.vector.app.core.extensions.vectorComponent +import org.matrix.android.sdk.api.util.MatrixItem + +data class AvatarPlaceholder(val matrixItem: MatrixItem) + +class AvatarPlaceholderModelLoaderFactory(private val context: Context) : ModelLoaderFactory { + + override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader { + return AvatarPlaceholderModelLoader(context) + } + + override fun teardown() { + // Is there something to do here? + } +} + +class AvatarPlaceholderModelLoader(private val context: Context) + : ModelLoader { + + override fun buildLoadData(model: AvatarPlaceholder, width: Int, height: Int, options: Options): ModelLoader.LoadData? { + return ModelLoader.LoadData(ObjectKey(model), AvatarPlaceholderDataFetcher(context, model)) + } + + override fun handles(model: AvatarPlaceholder): Boolean { + return true + } +} + +class AvatarPlaceholderDataFetcher(context: Context, private val data: AvatarPlaceholder) + : DataFetcher { + + private val avatarRenderer = context.vectorComponent().avatarRenderer() + + override fun loadData(priority: Priority, callback: DataFetcher.DataCallback) { + val avatarPlaceholder = avatarRenderer.getPlaceholderDrawable(data.matrixItem) + callback.onDataReady(avatarPlaceholder) + } + + override fun cleanup() { + // NOOP + } + + override fun cancel() { + // NOOP + } + + override fun getDataClass(): Class { + return Drawable::class.java + } + + override fun getDataSource(): DataSource { + return DataSource.LOCAL + } +} diff --git a/vector/src/main/java/im/vector/app/core/glide/MyAppGlideModule.kt b/vector/src/main/java/im/vector/app/core/glide/MyAppGlideModule.kt index 6ded33f823..74c9d4f0f6 100644 --- a/vector/src/main/java/im/vector/app/core/glide/MyAppGlideModule.kt +++ b/vector/src/main/java/im/vector/app/core/glide/MyAppGlideModule.kt @@ -17,6 +17,7 @@ package im.vector.app.core.glide import android.content.Context +import android.graphics.drawable.Drawable import android.util.Log import com.bumptech.glide.Glide @@ -40,5 +41,10 @@ class MyAppGlideModule : AppGlideModule() { InputStream::class.java, VectorGlideModelLoaderFactory(context) ) + registry.append( + AvatarPlaceholder::class.java, + Drawable::class.java, + AvatarPlaceholderModelLoaderFactory(context) + ) } } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt index 11b3a51c6f..21939bd42b 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt @@ -198,18 +198,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } is CallState.Connected -> { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { - if (state.transferee !is VectorCallViewState.TransfereeState.NoTransferee) { - val transfereeName = if (state.transferee is VectorCallViewState.TransfereeState.KnownTransferee) { - state.transferee.name - } else { - getString(R.string.call_transfer_unknown_person) - } - views.callActionText.text = getString(R.string.call_transfer_transfer_to_title, transfereeName) - views.callActionText.isVisible = true - views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.TransferCall) } - views.callStatusText.text = state.formattedDuration - configureCallInfo(state) - } else if (state.isLocalOnHold || state.isRemoteOnHold) { + if (state.isLocalOnHold || state.isRemoteOnHold) { views.smallIsHeldIcon.isVisible = true views.callVideoGroup.isInvisible = true views.callInfoGroup.isVisible = true @@ -221,10 +210,21 @@ class VectorCallActivity : VectorBaseActivity(), CallContro views.callStatusText.setText(R.string.call_held_by_you) } else { views.callActionText.isInvisible = true - state.callInfo.otherUserItem?.let { + state.callInfo?.opponentUserItem?.let { views.callStatusText.text = getString(R.string.call_held_by_user, it.getBestName()) } } + } else if (state.transferee !is VectorCallViewState.TransfereeState.NoTransferee) { + val transfereeName = if (state.transferee is VectorCallViewState.TransfereeState.KnownTransferee) { + state.transferee.name + } else { + getString(R.string.call_transfer_unknown_person) + } + views.callActionText.text = getString(R.string.call_transfer_transfer_to_title, transfereeName) + views.callActionText.isVisible = true + views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.TransferCall) } + views.callStatusText.text = state.formattedDuration + configureCallInfo(state) } else { views.callStatusText.text = state.formattedDuration configureCallInfo(state) @@ -255,31 +255,32 @@ class VectorCallActivity : VectorBaseActivity(), CallContro } private fun configureCallInfo(state: VectorCallViewState, blurAvatar: Boolean = false) { - state.callInfo.otherUserItem?.let { - val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen) - avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter) + state.callInfo?.opponentUserItem?.let { + val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen_blur) + avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter, addPlaceholder = false) if (state.transferee is VectorCallViewState.TransfereeState.NoTransferee) { views.participantNameText.text = it.getBestName() } else { views.participantNameText.text = getString(R.string.call_transfer_consulting_with, it.getBestName()) } if (blurAvatar) { - avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter) + avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter, addPlaceholder = true) } else { avatarRenderer.render(it, views.otherMemberAvatar) } } - if (state.otherKnownCallInfo?.otherUserItem == null) { + if (state.otherKnownCallInfo?.opponentUserItem == null) { views.otherKnownCallLayout.isVisible = false } else { val otherCall = callManager.getCallById(state.otherKnownCallInfo.callId) - val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen) + val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen_blur) avatarRenderer.renderBlur( - matrixItem = state.otherKnownCallInfo.otherUserItem, + matrixItem = state.otherKnownCallInfo.opponentUserItem, imageView = views.otherKnownCallAvatarView, sampling = 20, - rounded = false, - colorFilter = colorFilter + rounded = true, + colorFilter = colorFilter, + addPlaceholder = true ) views.otherKnownCallLayout.isVisible = true views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold || it.remoteOnHold }.orFalse() @@ -288,7 +289,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro private fun configureCallViews() { views.callControlsView.interactionListener = this - views.otherKnownCallAvatarView.setOnClickListener { + views.otherKnownCallLayout.setOnClickListener { withState(callViewModel) { val otherCall = callManager.getCallById(it.otherKnownCallInfo?.callId ?: "") ?: return@withState startActivity(newIntent(this, otherCall, null)) diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index 18eda0fd6f..e7b2b629e1 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -34,11 +34,13 @@ import im.vector.app.features.call.webrtc.getOpponentAsMatrixItem import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxPeerConnectionState import org.matrix.android.sdk.api.session.room.model.call.supportCallTransfer +import org.matrix.android.sdk.api.util.MatrixItem class VectorCallViewModel @AssistedInject constructor( @Assisted initialState: VectorCallViewState, @@ -87,6 +89,12 @@ class VectorCallViewModel @AssistedInject constructor( } } + override fun assertedIdentityChanged() { + setState { + copy(callInfo = call?.extractCallInfo()) + } + } + override fun onStateUpdate(call: MxCall) { val callState = call.state if (callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { @@ -160,8 +168,7 @@ class VectorCallViewModel @AssistedInject constructor( if (otherCall == null) { copy(otherKnownCallInfo = null) } else { - val otherUserItem = otherCall.getOpponentAsMatrixItem(session) - copy(otherKnownCallInfo = VectorCallViewState.CallInfo(otherCall.callId, otherUserItem)) + copy(otherKnownCallInfo = otherCall.extractCallInfo()) } } } @@ -175,7 +182,6 @@ class VectorCallViewModel @AssistedInject constructor( } else { call = webRtcCall callManager.addCurrentCallListener(currentCallListener) - val item = webRtcCall.getOpponentAsMatrixItem(session) webRtcCall.addListener(callListener) val currentSoundDevice = callManager.audioManager.selectedDevice if (currentSoundDevice == CallAudioManager.Device.PHONE) { @@ -185,7 +191,7 @@ class VectorCallViewModel @AssistedInject constructor( copy( isVideoCall = webRtcCall.mxCall.isVideoCall, callState = Success(webRtcCall.mxCall.state), - callInfo = VectorCallViewState.CallInfo(callId, item), + callInfo = webRtcCall.extractCallInfo(), device = currentSoundDevice ?: CallAudioManager.Device.PHONE, isLocalOnHold = webRtcCall.isLocalOnHold, isRemoteOnHold = webRtcCall.remoteOnHold, @@ -202,6 +208,22 @@ class VectorCallViewModel @AssistedInject constructor( } } + private fun WebRtcCall.extractCallInfo(): VectorCallViewState.CallInfo { + val assertedIdentity = this.remoteAssertedIdentity + val matrixItem = if (assertedIdentity != null) { + val userId = if (MatrixPatterns.isUserId(assertedIdentity.id)) { + assertedIdentity.id!! + } else { + // Need an id starting with @ + "@${assertedIdentity.displayName}" + } + MatrixItem.UserItem(userId, assertedIdentity.displayName, assertedIdentity.avatarUrl) + } else { + getOpponentAsMatrixItem(session) + } + return VectorCallViewState.CallInfo(callId, matrixItem) + } + override fun onCleared() { callManager.removeCurrentCallListener(currentCallListener) call?.removeListener(callListener) diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt index c5ae61cf60..3e7791cc08 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt @@ -39,7 +39,7 @@ data class VectorCallViewState( val availableDevices: Set = emptySet(), val callState: Async = Uninitialized, val otherKnownCallInfo: CallInfo? = null, - val callInfo: CallInfo = CallInfo(callId), + val callInfo: CallInfo? = null, val formattedDuration: String = "", val canOpponentBeTransferred: Boolean = false, val transferee: TransfereeState = TransfereeState.NoTransferee @@ -53,7 +53,7 @@ data class VectorCallViewState( data class CallInfo( val callId: String, - val otherUserItem: MatrixItem? = null + val opponentUserItem: MatrixItem? = null ) constructor(callArgs: CallArgs) : this( diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index c688c48429..d3a9dd7edf 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -27,6 +27,7 @@ import im.vector.app.features.call.CameraProxy import im.vector.app.features.call.CameraType import im.vector.app.features.call.CaptureFormat import im.vector.app.features.call.VectorCallActivity +import im.vector.app.features.call.lookup.sipNativeLookup import im.vector.app.features.call.utils.asWebRTC import im.vector.app.features.call.utils.awaitCreateAnswer import im.vector.app.features.call.utils.awaitCreateOffer @@ -51,6 +52,7 @@ import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxPeerConnectionState import org.matrix.android.sdk.api.session.call.TurnServerResponse import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallAssertedIdentityContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent @@ -104,6 +106,7 @@ class WebRtcCall( fun onCaptureStateChanged() {} fun onCameraChanged() {} fun onHoldUnhold() {} + fun assertedIdentityChanged() {} fun onTick(formattedDuration: String) {} override fun onStateUpdate(call: MxCall) {} } @@ -168,6 +171,8 @@ class WebRtcCall( // This value is used to track localOnHold when changing remoteOnHold value private var wasLocalOnHold = false + var remoteAssertedIdentity: CallAssertedIdentityContent.AssertedIdentity? = null + private set var offerSdp: CallInviteContent.Offer? = null @@ -877,6 +882,38 @@ class WebRtcCall( } } + fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) { + sessionScope?.launch(dispatcher) { + val session = sessionProvider.get() ?: return@launch + val newAssertedIdentity = callAssertedIdentityContent.assertedIdentity ?: return@launch + if (newAssertedIdentity.id == null && newAssertedIdentity.displayName == null) { + Timber.v("Asserted identity received with no relevant information, skip") + return@launch + } + remoteAssertedIdentity = newAssertedIdentity + if (newAssertedIdentity.id != null) { + val nativeUserId = session.sipNativeLookup(newAssertedIdentity.id!!).firstOrNull()?.userId + if (nativeUserId != null) { + val resolvedUser = tryOrNull { + session.resolveUser(nativeUserId) + } + if (resolvedUser != null) { + remoteAssertedIdentity = newAssertedIdentity.copy( + id = nativeUserId, + avatarUrl = resolvedUser.avatarUrl, + displayName = resolvedUser.displayName + ) + } else { + remoteAssertedIdentity = newAssertedIdentity.copy(id = nativeUserId) + } + } + } + listeners.forEach { + tryOrNull { it.assertedIdentityChanged() } + } + } + } + // MxCall.StateListener override fun onStateUpdate(call: MxCall) { diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt index 3c18d97937..25463428e9 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallManager.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.OnLifecycleEvent import im.vector.app.ActiveSessionDataSource +import im.vector.app.BuildConfig import im.vector.app.core.services.CallService import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.audio.CallAudioManager @@ -37,6 +38,7 @@ import org.matrix.android.sdk.api.session.call.CallListener import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent +import org.matrix.android.sdk.api.session.room.model.call.CallAssertedIdentityContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent @@ -420,4 +422,15 @@ class WebRtcCallManager @Inject constructor( Timber.v("## VOIP onCallManagedByOtherSession: $callId") onCallEnded(callId) } + + override fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) { + if (!BuildConfig.handleCallAssertedIdentityEvents) { + return + } + val call = callsByCallId[callAssertedIdentityContent.callId] + ?: return Unit.also { + Timber.w("onCallAssertedIdentityReceived for non active call? ${callAssertedIdentityContent.callId}") + } + call.onCallAssertedIdentityReceived(callAssertedIdentityContent) + } } diff --git a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt index c6cceee3b9..787027e0e2 100644 --- a/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt +++ b/vector/src/main/java/im/vector/app/features/home/AvatarRenderer.kt @@ -34,6 +34,7 @@ import com.bumptech.glide.request.target.DrawableImageViewTarget import com.bumptech.glide.request.target.Target import im.vector.app.core.contacts.MappedContact import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.core.glide.AvatarPlaceholder import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideRequest import im.vector.app.core.glide.GlideRequests @@ -136,7 +137,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active matrixItem: MatrixItem, target: Target) { val placeholder = getPlaceholderDrawable(matrixItem) - buildGlideRequest(glideRequests, matrixItem.avatarUrl) + glideRequests.loadResolvedUrl(matrixItem.avatarUrl) .apply { when (matrixItem) { is MatrixItem.SpaceItem -> { @@ -175,7 +176,12 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active } @UiThread - fun renderBlur(matrixItem: MatrixItem, imageView: ImageView, sampling: Int, rounded: Boolean, @ColorInt colorFilter: Int? = null) { + fun renderBlur(matrixItem: MatrixItem, + imageView: ImageView, + sampling: Int, + rounded: Boolean, + @ColorInt colorFilter: Int? = null, + addPlaceholder: Boolean) { val transformations = mutableListOf>( BlurTransformation(20, sampling) ) @@ -185,14 +191,26 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active if (rounded) { transformations.add(CircleCrop()) } - buildGlideRequest(GlideApp.with(imageView), matrixItem.avatarUrl) - .apply(RequestOptions.bitmapTransform(MultiTransformation(transformations))) + val bitmapTransform = RequestOptions.bitmapTransform(MultiTransformation(transformations)) + val glideRequests = GlideApp.with(imageView) + val placeholderRequest = if (addPlaceholder) { + glideRequests + .load(AvatarPlaceholder(matrixItem)) + .apply(bitmapTransform) + } else { + null + } + glideRequests.loadResolvedUrl(matrixItem.avatarUrl) + .apply(bitmapTransform) + // We are using thumbnail and error API so we can have blur transformation on it... + .thumbnail(placeholderRequest) + .error(placeholderRequest) .into(imageView) } @AnyThread fun getCachedDrawable(glideRequests: GlideRequests, matrixItem: MatrixItem): Drawable { - return buildGlideRequest(glideRequests, matrixItem.avatarUrl) + return glideRequests.loadResolvedUrl(matrixItem.avatarUrl) .onlyRetrieveFromCache(true) .apply(RequestOptions.circleCropTransform()) .submit() @@ -220,9 +238,9 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active // PRIVATE API ********************************************************************************* - private fun buildGlideRequest(glideRequests: GlideRequests, avatarUrl: String?): GlideRequest { + private fun GlideRequests.loadResolvedUrl(avatarUrl: String?): GlideRequest { val resolvedUrl = resolvedUrl(avatarUrl) - return glideRequests.load(resolvedUrl) + return load(resolvedUrl) } private fun resolvedUrl(avatarUrl: String?): String? { diff --git a/vector/src/main/res/layout/activity_call.xml b/vector/src/main/res/layout/activity_call.xml index 7179fc7896..46b65ad746 100644 --- a/vector/src/main/res/layout/activity_call.xml +++ b/vector/src/main/res/layout/activity_call.xml @@ -9,7 +9,7 @@ android:id="@+id/constraintLayout" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/bg_call_screen" + android:background="@color/bg_call_screen_blur" tools:ignore="MergeRootFrame"> - @@ -66,7 +70,7 @@ android:importantForAccessibility="no" android:src="@drawable/ic_call_small_pause" /> - + - #99000000 + #99000000 + #27303A #FF61708B #1E61708B