Merge pull request #3452 from vector-im/feature/fga/voip_asserted_identity

Feature/fga/voip asserted identity
This commit is contained in:
Benoit Marty 2021-06-11 12:22:17 +02:00 committed by GitHub
commit 75ee5d38fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 327 additions and 51 deletions

View File

@ -17,6 +17,7 @@
package org.matrix.android.sdk.api.session.call 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.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.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent 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 * Called when the call has been managed by an other session
*/ */
fun onCallManagedByOtherSession(callId: String) fun onCallManagedByOtherSession(callId: String)
/**
* Called when an asserted identity event is received
*/
fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent)
} }

View File

@ -76,6 +76,8 @@ object EventType {
const val CALL_NEGOTIATE = "m.call.negotiate" const val CALL_NEGOTIATE = "m.call.negotiate"
const val CALL_REJECT = "m.call.reject" const val CALL_REJECT = "m.call.reject"
const val CALL_HANGUP = "m.call.hangup" 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 // This type is not processed by the client, just sent to the server
const val CALL_REPLACES = "m.call.replaces" const val CALL_REPLACES = "m.call.replaces"

View File

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

View File

@ -37,7 +37,9 @@ internal class CallEventProcessor @Inject constructor(private val callSignalingH
EventType.CALL_CANDIDATES, EventType.CALL_CANDIDATES,
EventType.CALL_INVITE, EventType.CALL_INVITE,
EventType.CALL_HANGUP, EventType.CALL_HANGUP,
EventType.ENCRYPTED EventType.ENCRYPTED,
EventType.CALL_ASSERTED_IDENTITY,
EventType.CALL_ASSERTED_IDENTITY_PREFIX
) )
private val eventsToPostProcess = mutableListOf<Event>() private val eventsToPostProcess = mutableListOf<Event>()

View File

@ -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.CallListener
import org.matrix.android.sdk.api.session.call.MxCall 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.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.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
@ -64,6 +65,10 @@ internal class CallListenersDispatcher(private val listeners: Set<CallListener>)
it.onCallNegotiateReceived(callNegotiateContent) it.onCallNegotiateReceived(callNegotiateContent)
} }
override fun onCallAssertedIdentityReceived(callAssertedIdentityContent: CallAssertedIdentityContent) = dispatch {
it.onCallAssertedIdentityReceived(callAssertedIdentityContent)
}
private fun dispatch(lambda: (CallListener) -> Unit) { private fun dispatch(lambda: (CallListener) -> Unit) {
listeners.toList().forEach { listeners.toList().forEach {
tryOrNull { tryOrNull {

View File

@ -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.EventType
import org.matrix.android.sdk.api.session.events.model.toModel 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.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.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent 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) { fun onCallEvent(event: Event) {
when (event.getClearType()) { when (event.getClearType()) {
EventType.CALL_ANSWER -> { EventType.CALL_ANSWER -> {
handleCallAnswerEvent(event) handleCallAnswerEvent(event)
} }
EventType.CALL_INVITE -> { EventType.CALL_INVITE -> {
handleCallInviteEvent(event) handleCallInviteEvent(event)
} }
EventType.CALL_HANGUP -> { EventType.CALL_HANGUP -> {
handleCallHangupEvent(event) handleCallHangupEvent(event)
} }
EventType.CALL_REJECT -> { EventType.CALL_REJECT -> {
handleCallRejectEvent(event) handleCallRejectEvent(event)
} }
EventType.CALL_CANDIDATES -> { EventType.CALL_CANDIDATES -> {
handleCallCandidatesEvent(event) handleCallCandidatesEvent(event)
} }
EventType.CALL_SELECT_ANSWER -> { EventType.CALL_SELECT_ANSWER -> {
handleCallSelectAnswerEvent(event) handleCallSelectAnswerEvent(event)
} }
EventType.CALL_NEGOTIATE -> { EventType.CALL_NEGOTIATE -> {
handleCallNegotiateEvent(event) handleCallNegotiateEvent(event)
} }
EventType.CALL_ASSERTED_IDENTITY,
EventType.CALL_ASSERTED_IDENTITY_PREFIX -> {
handleCallAssertedIdentityEvent(event)
}
} }
} }
private fun handleCallAssertedIdentityEvent(event: Event) {
val content = event.getClearContent().toModel<CallAssertedIdentityContent>() ?: 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) { private fun handleCallNegotiateEvent(event: Event) {
val content = event.getClearContent().toModel<CallNegotiateContent>() ?: return val content = event.getClearContent().toModel<CallNegotiateContent>() ?: return
val call = content.getCall() ?: return val call = content.getCall() ?: return

View File

@ -0,0 +1 @@
Adds support for receiving MSC3086 Asserted Identity events.

View File

@ -144,6 +144,10 @@ android {
buildConfigField "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy", "outboundSessionKeySharingStrategy", "im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy.WhenTyping" 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" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// Keep abiFilter for the universalApk // Keep abiFilter for the universalApk

View File

@ -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<AvatarPlaceholder, Drawable> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<AvatarPlaceholder, Drawable> {
return AvatarPlaceholderModelLoader(context)
}
override fun teardown() {
// Is there something to do here?
}
}
class AvatarPlaceholderModelLoader(private val context: Context)
: ModelLoader<AvatarPlaceholder, Drawable> {
override fun buildLoadData(model: AvatarPlaceholder, width: Int, height: Int, options: Options): ModelLoader.LoadData<Drawable>? {
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<Drawable> {
private val avatarRenderer = context.vectorComponent().avatarRenderer()
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in Drawable>) {
val avatarPlaceholder = avatarRenderer.getPlaceholderDrawable(data.matrixItem)
callback.onDataReady(avatarPlaceholder)
}
override fun cleanup() {
// NOOP
}
override fun cancel() {
// NOOP
}
override fun getDataClass(): Class<Drawable> {
return Drawable::class.java
}
override fun getDataSource(): DataSource {
return DataSource.LOCAL
}
}

View File

@ -17,6 +17,7 @@
package im.vector.app.core.glide package im.vector.app.core.glide
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable
import android.util.Log import android.util.Log
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
@ -40,5 +41,10 @@ class MyAppGlideModule : AppGlideModule() {
InputStream::class.java, InputStream::class.java,
VectorGlideModelLoaderFactory(context) VectorGlideModelLoaderFactory(context)
) )
registry.append(
AvatarPlaceholder::class.java,
Drawable::class.java,
AvatarPlaceholderModelLoaderFactory(context)
)
} }
} }

View File

@ -198,18 +198,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
} }
is CallState.Connected -> { is CallState.Connected -> {
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
if (state.transferee !is VectorCallViewState.TransfereeState.NoTransferee) { if (state.isLocalOnHold || state.isRemoteOnHold) {
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) {
views.smallIsHeldIcon.isVisible = true views.smallIsHeldIcon.isVisible = true
views.callVideoGroup.isInvisible = true views.callVideoGroup.isInvisible = true
views.callInfoGroup.isVisible = true views.callInfoGroup.isVisible = true
@ -221,10 +210,21 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
views.callStatusText.setText(R.string.call_held_by_you) views.callStatusText.setText(R.string.call_held_by_you)
} else { } else {
views.callActionText.isInvisible = true views.callActionText.isInvisible = true
state.callInfo.otherUserItem?.let { state.callInfo?.opponentUserItem?.let {
views.callStatusText.text = getString(R.string.call_held_by_user, it.getBestName()) 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 { } else {
views.callStatusText.text = state.formattedDuration views.callStatusText.text = state.formattedDuration
configureCallInfo(state) configureCallInfo(state)
@ -255,31 +255,32 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
} }
private fun configureCallInfo(state: VectorCallViewState, blurAvatar: Boolean = false) { private fun configureCallInfo(state: VectorCallViewState, blurAvatar: Boolean = false) {
state.callInfo.otherUserItem?.let { state.callInfo?.opponentUserItem?.let {
val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen) val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen_blur)
avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter) avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter, addPlaceholder = false)
if (state.transferee is VectorCallViewState.TransfereeState.NoTransferee) { if (state.transferee is VectorCallViewState.TransfereeState.NoTransferee) {
views.participantNameText.text = it.getBestName() views.participantNameText.text = it.getBestName()
} else { } else {
views.participantNameText.text = getString(R.string.call_transfer_consulting_with, it.getBestName()) views.participantNameText.text = getString(R.string.call_transfer_consulting_with, it.getBestName())
} }
if (blurAvatar) { 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 { } else {
avatarRenderer.render(it, views.otherMemberAvatar) avatarRenderer.render(it, views.otherMemberAvatar)
} }
} }
if (state.otherKnownCallInfo?.otherUserItem == null) { if (state.otherKnownCallInfo?.opponentUserItem == null) {
views.otherKnownCallLayout.isVisible = false views.otherKnownCallLayout.isVisible = false
} else { } else {
val otherCall = callManager.getCallById(state.otherKnownCallInfo.callId) 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( avatarRenderer.renderBlur(
matrixItem = state.otherKnownCallInfo.otherUserItem, matrixItem = state.otherKnownCallInfo.opponentUserItem,
imageView = views.otherKnownCallAvatarView, imageView = views.otherKnownCallAvatarView,
sampling = 20, sampling = 20,
rounded = false, rounded = true,
colorFilter = colorFilter colorFilter = colorFilter,
addPlaceholder = true
) )
views.otherKnownCallLayout.isVisible = true views.otherKnownCallLayout.isVisible = true
views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold || it.remoteOnHold }.orFalse() views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold || it.remoteOnHold }.orFalse()
@ -288,7 +289,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
private fun configureCallViews() { private fun configureCallViews() {
views.callControlsView.interactionListener = this views.callControlsView.interactionListener = this
views.otherKnownCallAvatarView.setOnClickListener { views.otherKnownCallLayout.setOnClickListener {
withState(callViewModel) { withState(callViewModel) {
val otherCall = callManager.getCallById(it.otherKnownCallInfo?.callId ?: "") ?: return@withState val otherCall = callManager.getCallById(it.otherKnownCallInfo?.callId ?: "") ?: return@withState
startActivity(newIntent(this, otherCall, null)) startActivity(newIntent(this, otherCall, null))

View File

@ -34,11 +34,13 @@ import im.vector.app.features.call.webrtc.getOpponentAsMatrixItem
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch 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.Session
import org.matrix.android.sdk.api.session.call.CallState 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.MxCall
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState 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.session.room.model.call.supportCallTransfer
import org.matrix.android.sdk.api.util.MatrixItem
class VectorCallViewModel @AssistedInject constructor( class VectorCallViewModel @AssistedInject constructor(
@Assisted initialState: VectorCallViewState, @Assisted initialState: VectorCallViewState,
@ -87,6 +89,12 @@ class VectorCallViewModel @AssistedInject constructor(
} }
} }
override fun assertedIdentityChanged() {
setState {
copy(callInfo = call?.extractCallInfo())
}
}
override fun onStateUpdate(call: MxCall) { override fun onStateUpdate(call: MxCall) {
val callState = call.state val callState = call.state
if (callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { if (callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
@ -160,8 +168,7 @@ class VectorCallViewModel @AssistedInject constructor(
if (otherCall == null) { if (otherCall == null) {
copy(otherKnownCallInfo = null) copy(otherKnownCallInfo = null)
} else { } else {
val otherUserItem = otherCall.getOpponentAsMatrixItem(session) copy(otherKnownCallInfo = otherCall.extractCallInfo())
copy(otherKnownCallInfo = VectorCallViewState.CallInfo(otherCall.callId, otherUserItem))
} }
} }
} }
@ -175,7 +182,6 @@ class VectorCallViewModel @AssistedInject constructor(
} else { } else {
call = webRtcCall call = webRtcCall
callManager.addCurrentCallListener(currentCallListener) callManager.addCurrentCallListener(currentCallListener)
val item = webRtcCall.getOpponentAsMatrixItem(session)
webRtcCall.addListener(callListener) webRtcCall.addListener(callListener)
val currentSoundDevice = callManager.audioManager.selectedDevice val currentSoundDevice = callManager.audioManager.selectedDevice
if (currentSoundDevice == CallAudioManager.Device.PHONE) { if (currentSoundDevice == CallAudioManager.Device.PHONE) {
@ -185,7 +191,7 @@ class VectorCallViewModel @AssistedInject constructor(
copy( copy(
isVideoCall = webRtcCall.mxCall.isVideoCall, isVideoCall = webRtcCall.mxCall.isVideoCall,
callState = Success(webRtcCall.mxCall.state), callState = Success(webRtcCall.mxCall.state),
callInfo = VectorCallViewState.CallInfo(callId, item), callInfo = webRtcCall.extractCallInfo(),
device = currentSoundDevice ?: CallAudioManager.Device.PHONE, device = currentSoundDevice ?: CallAudioManager.Device.PHONE,
isLocalOnHold = webRtcCall.isLocalOnHold, isLocalOnHold = webRtcCall.isLocalOnHold,
isRemoteOnHold = webRtcCall.remoteOnHold, 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() { override fun onCleared() {
callManager.removeCurrentCallListener(currentCallListener) callManager.removeCurrentCallListener(currentCallListener)
call?.removeListener(callListener) call?.removeListener(callListener)

View File

@ -39,7 +39,7 @@ data class VectorCallViewState(
val availableDevices: Set<CallAudioManager.Device> = emptySet(), val availableDevices: Set<CallAudioManager.Device> = emptySet(),
val callState: Async<CallState> = Uninitialized, val callState: Async<CallState> = Uninitialized,
val otherKnownCallInfo: CallInfo? = null, val otherKnownCallInfo: CallInfo? = null,
val callInfo: CallInfo = CallInfo(callId), val callInfo: CallInfo? = null,
val formattedDuration: String = "", val formattedDuration: String = "",
val canOpponentBeTransferred: Boolean = false, val canOpponentBeTransferred: Boolean = false,
val transferee: TransfereeState = TransfereeState.NoTransferee val transferee: TransfereeState = TransfereeState.NoTransferee
@ -53,7 +53,7 @@ data class VectorCallViewState(
data class CallInfo( data class CallInfo(
val callId: String, val callId: String,
val otherUserItem: MatrixItem? = null val opponentUserItem: MatrixItem? = null
) )
constructor(callArgs: CallArgs) : this( constructor(callArgs: CallArgs) : this(

View File

@ -27,6 +27,7 @@ import im.vector.app.features.call.CameraProxy
import im.vector.app.features.call.CameraType import im.vector.app.features.call.CameraType
import im.vector.app.features.call.CaptureFormat import im.vector.app.features.call.CaptureFormat
import im.vector.app.features.call.VectorCallActivity 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.asWebRTC
import im.vector.app.features.call.utils.awaitCreateAnswer import im.vector.app.features.call.utils.awaitCreateAnswer
import im.vector.app.features.call.utils.awaitCreateOffer 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.MxPeerConnectionState
import org.matrix.android.sdk.api.session.call.TurnServerResponse 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.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.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
@ -104,6 +106,7 @@ class WebRtcCall(
fun onCaptureStateChanged() {} fun onCaptureStateChanged() {}
fun onCameraChanged() {} fun onCameraChanged() {}
fun onHoldUnhold() {} fun onHoldUnhold() {}
fun assertedIdentityChanged() {}
fun onTick(formattedDuration: String) {} fun onTick(formattedDuration: String) {}
override fun onStateUpdate(call: MxCall) {} override fun onStateUpdate(call: MxCall) {}
} }
@ -168,6 +171,8 @@ class WebRtcCall(
// This value is used to track localOnHold when changing remoteOnHold value // This value is used to track localOnHold when changing remoteOnHold value
private var wasLocalOnHold = false private var wasLocalOnHold = false
var remoteAssertedIdentity: CallAssertedIdentityContent.AssertedIdentity? = null
private set
var offerSdp: CallInviteContent.Offer? = null 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 // MxCall.StateListener
override fun onStateUpdate(call: MxCall) { override fun onStateUpdate(call: MxCall) {

View File

@ -21,6 +21,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.OnLifecycleEvent
import im.vector.app.ActiveSessionDataSource import im.vector.app.ActiveSessionDataSource
import im.vector.app.BuildConfig
import im.vector.app.core.services.CallService import im.vector.app.core.services.CallService
import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.audio.CallAudioManager 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.CallState
import org.matrix.android.sdk.api.session.call.MxCall 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.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.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
@ -420,4 +422,15 @@ class WebRtcCallManager @Inject constructor(
Timber.v("## VOIP onCallManagedByOtherSession: $callId") Timber.v("## VOIP onCallManagedByOtherSession: $callId")
onCallEnded(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)
}
} }

View File

@ -34,6 +34,7 @@ import com.bumptech.glide.request.target.DrawableImageViewTarget
import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.target.Target
import im.vector.app.core.contacts.MappedContact import im.vector.app.core.contacts.MappedContact
import im.vector.app.core.di.ActiveSessionHolder 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.GlideApp
import im.vector.app.core.glide.GlideRequest import im.vector.app.core.glide.GlideRequest
import im.vector.app.core.glide.GlideRequests import im.vector.app.core.glide.GlideRequests
@ -136,7 +137,7 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
matrixItem: MatrixItem, matrixItem: MatrixItem,
target: Target<Drawable>) { target: Target<Drawable>) {
val placeholder = getPlaceholderDrawable(matrixItem) val placeholder = getPlaceholderDrawable(matrixItem)
buildGlideRequest(glideRequests, matrixItem.avatarUrl) glideRequests.loadResolvedUrl(matrixItem.avatarUrl)
.apply { .apply {
when (matrixItem) { when (matrixItem) {
is MatrixItem.SpaceItem -> { is MatrixItem.SpaceItem -> {
@ -175,7 +176,12 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
} }
@UiThread @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<Transformation<Bitmap>>( val transformations = mutableListOf<Transformation<Bitmap>>(
BlurTransformation(20, sampling) BlurTransformation(20, sampling)
) )
@ -185,14 +191,26 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
if (rounded) { if (rounded) {
transformations.add(CircleCrop()) transformations.add(CircleCrop())
} }
buildGlideRequest(GlideApp.with(imageView), matrixItem.avatarUrl) val bitmapTransform = RequestOptions.bitmapTransform(MultiTransformation(transformations))
.apply(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) .into(imageView)
} }
@AnyThread @AnyThread
fun getCachedDrawable(glideRequests: GlideRequests, matrixItem: MatrixItem): Drawable { fun getCachedDrawable(glideRequests: GlideRequests, matrixItem: MatrixItem): Drawable {
return buildGlideRequest(glideRequests, matrixItem.avatarUrl) return glideRequests.loadResolvedUrl(matrixItem.avatarUrl)
.onlyRetrieveFromCache(true) .onlyRetrieveFromCache(true)
.apply(RequestOptions.circleCropTransform()) .apply(RequestOptions.circleCropTransform())
.submit() .submit()
@ -220,9 +238,9 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
// PRIVATE API ********************************************************************************* // PRIVATE API *********************************************************************************
private fun buildGlideRequest(glideRequests: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> { private fun GlideRequests.loadResolvedUrl(avatarUrl: String?): GlideRequest<Drawable> {
val resolvedUrl = resolvedUrl(avatarUrl) val resolvedUrl = resolvedUrl(avatarUrl)
return glideRequests.load(resolvedUrl) return load(resolvedUrl)
} }
private fun resolvedUrl(avatarUrl: String?): String? { private fun resolvedUrl(avatarUrl: String?): String? {

View File

@ -9,7 +9,7 @@
android:id="@+id/constraintLayout" android:id="@+id/constraintLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/bg_call_screen" android:background="@color/bg_call_screen_blur"
tools:ignore="MergeRootFrame"> tools:ignore="MergeRootFrame">
<ImageView <ImageView
@ -37,7 +37,7 @@
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<FrameLayout <com.google.android.material.card.MaterialCardView
android:id="@+id/otherKnownCallLayout" android:id="@+id/otherKnownCallLayout"
android:layout_width="80dp" android:layout_width="80dp"
android:layout_height="144dp" android:layout_height="144dp"
@ -45,15 +45,19 @@
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:background="@color/element_background_light" android:background="@color/element_background_light"
android:visibility="gone" android:visibility="gone"
app:cardCornerRadius="4dp"
app:cardElevation="4dp"
android:foreground="?attr/selectableItemBackground"
app:cardBackgroundColor="@color/bg_call_screen"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible"> tools:visibility="visible">
<ImageView <ImageView
android:id="@+id/otherKnownCallAvatarView" android:id="@+id/otherKnownCallAvatarView"
android:layout_width="match_parent" android:layout_width="64dp"
android:layout_height="match_parent" android:layout_height="64dp"
android:foreground="?attr/selectableItemBackground" android:layout_gravity="center"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:scaleType="centerCrop" android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" /> tools:src="@tools:sample/avatars" />
@ -66,7 +70,7 @@
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:src="@drawable/ic_call_small_pause" /> android:src="@drawable/ic_call_small_pause" />
</FrameLayout> </com.google.android.material.card.MaterialCardView>
<ImageView <ImageView
android:id="@+id/otherMemberAvatar" android:id="@+id/otherMemberAvatar"

View File

@ -19,7 +19,8 @@
<!-- Source: https://zpl.io/aBKw9Mk --> <!-- Source: https://zpl.io/aBKw9Mk -->
<color name="bg_call_screen">#99000000</color> <color name="bg_call_screen_blur">#99000000</color>
<color name="bg_call_screen">#27303A</color>
<color name="vctr_notice_secondary">#FF61708B</color> <color name="vctr_notice_secondary">#FF61708B</color>
<color name="vctr_notice_secondary_alpha12">#1E61708B</color> <color name="vctr_notice_secondary_alpha12">#1E61708B</color>