Merge branch 'develop' into feature/fga/timeline_virtual_room

This commit is contained in:
ganfra 2021-06-11 16:35:45 +02:00
commit 6d9c49462a
42 changed files with 620 additions and 247 deletions

View file

@ -15,7 +15,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:4.2.1' classpath 'com.android.tools.build:gradle:4.2.1'
classpath 'com.google.gms:google-services:4.3.8' classpath 'com.google.gms:google-services:4.3.8'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.2.0' classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.3'
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.4' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.4'
classpath "com.likethesalad.android:string-reference:1.2.2" classpath "com.likethesalad.android:string-reference:1.2.2"

View file

@ -112,7 +112,7 @@ dependencies {
def lifecycle_version = '2.2.0' def lifecycle_version = '2.2.0'
def arch_version = '2.1.0' def arch_version = '2.1.0'
def markwon_version = '3.1.0' def markwon_version = '3.1.0'
def daggerVersion = '2.36' def daggerVersion = '2.37'
def work_version = '2.5.0' def work_version = '2.5.0'
def retrofit_version = '2.9.0' def retrofit_version = '2.9.0'

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

@ -51,7 +51,7 @@ internal class SessionManager @Inject constructor(private val matrixComponent: M
} }
} }
private fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent { fun getOrCreateSessionComponent(sessionParams: SessionParams): SessionComponent {
return sessionComponents.getOrPut(sessionParams.credentials.sessionId()) { return sessionComponents.getOrPut(sessionParams.credentials.sessionId()) {
DaggerSessionComponent DaggerSessionComponent
.factory() .factory()

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

@ -31,9 +31,11 @@ internal class DirectChatsHelper @Inject constructor(@SessionDatabase
*/ */
fun getLocalUserAccount(filterRoomId: String? = null): MutableMap<String, MutableList<String>> { fun getLocalUserAccount(filterRoomId: String? = null): MutableMap<String, MutableList<String>> {
return Realm.getInstance(realmConfiguration).use { realm -> return Realm.getInstance(realmConfiguration).use { realm ->
// Makes sure we have the latest realm updates, this is important as we sent this information to the server.
realm.refresh()
RoomSummaryEntity.getDirectRooms(realm) RoomSummaryEntity.getDirectRooms(realm)
.asSequence() .asSequence()
.filter { it.roomId != filterRoomId && it.directUserId != null } .filter { it.roomId != filterRoomId && it.directUserId != null && it.membership.isActive() }
.groupByTo(mutableMapOf(), { it.directUserId!! }, { it.roomId }) .groupByTo(mutableMapOf(), { it.directUserId!! }, { it.roomId })
} }
} }

1
newsfragment/3333.bugfix Normal file
View file

@ -0,0 +1 @@
Fix new DMs not always marked as such

View file

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

1
newsfragment/3457.misc Normal file
View file

@ -0,0 +1 @@
Move the ability to start a call from dialpad directly to a dedicated tab in the home screen.

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
@ -302,7 +306,7 @@ dependencies {
def big_image_viewer_version = '1.8.0' def big_image_viewer_version = '1.8.0'
def glide_version = '4.12.0' def glide_version = '4.12.0'
def moshi_version = '1.12.0' def moshi_version = '1.12.0'
def daggerVersion = '2.36' def daggerVersion = '2.37'
def autofill_version = "1.1.0" def autofill_version = "1.1.0"
def work_version = '2.5.0' def work_version = '2.5.0'
def arch_version = '2.1.0' def arch_version = '2.1.0'

View file

@ -116,7 +116,9 @@ class DefaultErrorFormatter @Inject constructor(
throwable.localizedMessage throwable.localizedMessage
} }
} }
is DialPadLookup.Failure -> is DialPadLookup.Failure.NumberIsYours ->
stringProvider.getString(R.string.cannot_call_yourself)
is DialPadLookup.Failure.NoResult ->
stringProvider.getString(R.string.call_dial_pad_lookup_error) stringProvider.getString(R.string.call_dial_pad_lookup_error)
else -> throwable.localizedMessage else -> throwable.localizedMessage
} }

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

@ -1,46 +0,0 @@
/*
* 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

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

@ -26,9 +26,9 @@ import androidx.core.widget.ImageViewCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.android.dialer.dialpadview.DialpadView import com.android.dialer.dialpadview.DialpadView
import com.android.dialer.dialpadview.DigitsEditText import com.android.dialer.dialpadview.DigitsEditText
import com.android.dialer.dialpadview.R
import com.google.i18n.phonenumbers.AsYouTypeFormatter import com.google.i18n.phonenumbers.AsYouTypeFormatter
import com.google.i18n.phonenumbers.PhoneNumberUtil import com.google.i18n.phonenumbers.PhoneNumberUtil
import im.vector.app.R
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
class DialPadFragment : Fragment() { class DialPadFragment : Fragment() {
@ -57,7 +57,7 @@ class DialPadFragment : Fragment() {
dialpadView.findViewById<View>(R.id.dialpad_key_voicemail).isVisible = false dialpadView.findViewById<View>(R.id.dialpad_key_voicemail).isVisible = false
digits = dialpadView.digits as? DigitsEditText digits = dialpadView.digits as? DigitsEditText
digits?.isCursorVisible = cursorVisible digits?.isCursorVisible = cursorVisible
digits?.setTextColor(ThemeUtils.getColor(requireContext(), im.vector.app.R.attr.vctr_content_primary)) digits?.setTextColor(ThemeUtils.getColor(requireContext(), R.attr.vctr_content_primary))
dialpadView.findViewById<View>(R.id.zero).setOnClickListener { append('0') } dialpadView.findViewById<View>(R.id.zero).setOnClickListener { append('0') }
if (enablePlus) { if (enablePlus) {
dialpadView.findViewById<View>(R.id.zero).setOnLongClickListener { dialpadView.findViewById<View>(R.id.zero).setOnLongClickListener {

View file

@ -17,10 +17,11 @@
package im.vector.app.features.call.dialpad package im.vector.app.features.call.dialpad
import im.vector.app.features.call.lookup.pstnLookup import im.vector.app.features.call.lookup.pstnLookup
import im.vector.app.features.call.lookup.sipNativeLookup
import im.vector.app.features.call.vectorCallService
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.createdirect.DirectRoomHelper
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import java.lang.IllegalStateException
import javax.inject.Inject import javax.inject.Inject
class DialPadLookup @Inject constructor( class DialPadLookup @Inject constructor(
@ -28,13 +29,25 @@ class DialPadLookup @Inject constructor(
private val webRtcCallManager: WebRtcCallManager, private val webRtcCallManager: WebRtcCallManager,
private val directRoomHelper: DirectRoomHelper private val directRoomHelper: DirectRoomHelper
) { ) {
class Failure : Throwable() sealed class Failure : Throwable() {
object NoResult: Failure()
object NumberIsYours: Failure()
}
data class Result(val userId: String, val roomId: String) data class Result(val userId: String, val roomId: String)
suspend fun lookupPhoneNumber(phoneNumber: String): Result { suspend fun lookupPhoneNumber(phoneNumber: String): Result {
val thirdPartyUser = session.pstnLookup(phoneNumber, webRtcCallManager.supportedPSTNProtocol).firstOrNull() ?: throw IllegalStateException() session.vectorCallService.protocolChecker.awaitCheckProtocols()
val roomId = directRoomHelper.ensureDMExists(thirdPartyUser.userId) val thirdPartyUser = session.pstnLookup(phoneNumber, webRtcCallManager.supportedPSTNProtocol).firstOrNull() ?: throw Failure.NoResult
return Result(userId = thirdPartyUser.userId, roomId = roomId) // check to see if this is a virtual user, in which case we should find the native user
val nativeUserId = if (webRtcCallManager.supportsVirtualRooms) {
val nativeLookupResults = session.sipNativeLookup(thirdPartyUser.userId)
nativeLookupResults.firstOrNull()?.userId ?: thirdPartyUser.userId
} else {
thirdPartyUser.userId
}
if (nativeUserId == session.myUserId) throw Failure.NumberIsYours
val roomId = directRoomHelper.ensureDMExists(nativeUserId)
return Result(userId = nativeUserId, roomId = roomId)
} }
} }

View file

@ -16,10 +16,13 @@
package im.vector.app.features.call.lookup package im.vector.app.features.call.lookup
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser import org.matrix.android.sdk.api.session.thirdparty.model.ThirdPartyUser
private const val LOOKUP_SUCCESS_FIELD = "lookup_success"
suspend fun Session.pstnLookup(phoneNumber: String, protocol: String?): List<ThirdPartyUser> { suspend fun Session.pstnLookup(phoneNumber: String, protocol: String?): List<ThirdPartyUser> {
if (protocol == null) return emptyList() if (protocol == null) return emptyList()
return tryOrNull { return tryOrNull {
@ -36,7 +39,11 @@ suspend fun Session.sipVirtualLookup(nativeMxid: String): List<ThirdPartyUser> {
protocol = PROTOCOL_SIP_VIRTUAL, protocol = PROTOCOL_SIP_VIRTUAL,
fields = mapOf("native_mxid" to nativeMxid) fields = mapOf("native_mxid" to nativeMxid)
) )
}.orEmpty() }
.orEmpty()
.filter {
(it.fields[LOOKUP_SUCCESS_FIELD] as? Boolean).orFalse()
}
} }
suspend fun Session.sipNativeLookup(virtualMxid: String): List<ThirdPartyUser> { suspend fun Session.sipNativeLookup(virtualMxid: String): List<ThirdPartyUser> {
@ -45,5 +52,9 @@ suspend fun Session.sipNativeLookup(virtualMxid: String): List<ThirdPartyUser> {
protocol = PROTOCOL_SIP_NATIVE, protocol = PROTOCOL_SIP_NATIVE,
fields = mapOf("virtual_mxid" to virtualMxid) fields = mapOf("virtual_mxid" to virtualMxid)
) )
}.orEmpty() }
.orEmpty()
.filter {
(it.fields[LOOKUP_SUCCESS_FIELD] as? Boolean).orFalse()
}
} }

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

@ -19,6 +19,7 @@ package im.vector.app.features.home
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
sealed class HomeDetailAction : VectorViewModelAction { sealed class HomeDetailAction : VectorViewModelAction {
data class SwitchDisplayMode(val displayMode: RoomListDisplayMode) : HomeDetailAction() data class SwitchTab(val tab: HomeTab) : HomeDetailAction()
object MarkAllRoomsRead : HomeDetailAction() object MarkAllRoomsRead : HomeDetailAction()
data class StartCallWithPhoneNumber(val phoneNumber: String): HomeDetailAction()
} }

View file

@ -23,6 +23,8 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.iterator
import androidx.fragment.app.Fragment
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
@ -41,12 +43,14 @@ import im.vector.app.core.ui.views.KnownCallsViewHolder
import im.vector.app.databinding.FragmentHomeDetailBinding import im.vector.app.databinding.FragmentHomeDetailBinding
import im.vector.app.features.call.SharedKnownCallsViewModel import im.vector.app.features.call.SharedKnownCallsViewModel
import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.dialpad.DialPadFragment
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.list.RoomListFragment import im.vector.app.features.home.room.list.RoomListFragment
import im.vector.app.features.home.room.list.RoomListParams import im.vector.app.features.home.room.list.RoomListParams
import im.vector.app.features.home.room.list.UnreadCounterBadgeView import im.vector.app.features.home.room.list.UnreadCounterBadgeView
import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.PopupAlertManager
import im.vector.app.features.popup.VerificationVectorAlert import im.vector.app.features.popup.VerificationVectorAlert
import im.vector.app.features.settings.VectorLocale
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS import im.vector.app.features.settings.VectorSettingsActivity.Companion.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
@ -101,6 +105,9 @@ class HomeDetailFragment @Inject constructor(
} }
override fun onPrepareOptionsMenu(menu: Menu) { override fun onPrepareOptionsMenu(menu: Menu) {
withState(viewModel) { state ->
menu.iterator().forEach { it.isVisible = state.currentTab is HomeTab.RoomList }
}
menu.findItem(R.id.menu_home_mark_all_as_read).isVisible = hasUnreadRooms menu.findItem(R.id.menu_home_mark_all_as_read).isVisible = hasUnreadRooms
super.onPrepareOptionsMenu(menu) super.onPrepareOptionsMenu(menu)
} }
@ -123,7 +130,7 @@ class HomeDetailFragment @Inject constructor(
withState(viewModel) { withState(viewModel) {
// Update the navigation view if needed (for when we restore the tabs) // Update the navigation view if needed (for when we restore the tabs)
views.bottomNavigationView.selectedItemId = it.displayMode.toMenuId() views.bottomNavigationView.selectedItemId = it.currentTab.toMenuId()
} }
viewModel.selectSubscribe(this, HomeDetailViewState::roomGroupingMethod) { roomGroupingMethod -> viewModel.selectSubscribe(this, HomeDetailViewState::roomGroupingMethod) { roomGroupingMethod ->
@ -137,8 +144,20 @@ class HomeDetailFragment @Inject constructor(
} }
} }
viewModel.selectSubscribe(this, HomeDetailViewState::displayMode) { displayMode -> viewModel.selectSubscribe(this, HomeDetailViewState::currentTab) { currentTab ->
switchDisplayMode(displayMode) updateUIForTab(currentTab)
}
viewModel.selectSubscribe(this, HomeDetailViewState::showDialPadTab) { showDialPadTab ->
updateTabVisibilitySafely(R.id.bottom_action_dial_pad, showDialPadTab)
}
viewModel.observeViewEvents { viewEvent ->
when (viewEvent) {
HomeDetailViewEvents.CallStarted -> dismissLoadingDialog()
is HomeDetailViewEvents.FailToCall -> showFailure(viewEvent.failure)
HomeDetailViewEvents.Loading -> showLoadingDialog()
}
} }
unknownDeviceDetectorSharedViewModel.subscribe { state -> unknownDeviceDetectorSharedViewModel.subscribe { state ->
@ -179,20 +198,8 @@ class HomeDetailFragment @Inject constructor(
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// update notification tab if needed // update notification tab if needed
checkNotificationTabStatus() updateTabVisibilitySafely(R.id.bottom_action_notification, vectorPreferences.labAddNotificationTab())
} callManager.checkForProtocolsSupportIfNeeded()
private fun checkNotificationTabStatus() {
val wasVisible = views.bottomNavigationView.menu.findItem(R.id.bottom_action_notification).isVisible
views.bottomNavigationView.menu.findItem(R.id.bottom_action_notification).isVisible = vectorPreferences.labAddNotificationTab()
if (wasVisible && !vectorPreferences.labAddNotificationTab()) {
// As we hide it check if it's not the current item!
withState(viewModel) {
if (it.displayMode.toMenuId() == R.id.bottom_action_notification) {
viewModel.handle(HomeDetailAction.SwitchDisplayMode(RoomListDisplayMode.PEOPLE))
}
}
}
} }
private fun promptForNewUnknownDevices(uid: String, state: UnknownDevicesState, newest: DeviceInfo) { private fun promptForNewUnknownDevices(uid: String, state: UnknownDevicesState, newest: DeviceInfo) {
@ -321,12 +328,13 @@ class HomeDetailFragment @Inject constructor(
private fun setupBottomNavigationView() { private fun setupBottomNavigationView() {
views.bottomNavigationView.menu.findItem(R.id.bottom_action_notification).isVisible = vectorPreferences.labAddNotificationTab() views.bottomNavigationView.menu.findItem(R.id.bottom_action_notification).isVisible = vectorPreferences.labAddNotificationTab()
views.bottomNavigationView.setOnNavigationItemSelectedListener { views.bottomNavigationView.setOnNavigationItemSelectedListener {
val displayMode = when (it.itemId) { val tab = when (it.itemId) {
R.id.bottom_action_people -> RoomListDisplayMode.PEOPLE R.id.bottom_action_people -> HomeTab.RoomList(RoomListDisplayMode.PEOPLE)
R.id.bottom_action_rooms -> RoomListDisplayMode.ROOMS R.id.bottom_action_rooms -> HomeTab.RoomList(RoomListDisplayMode.ROOMS)
else -> RoomListDisplayMode.NOTIFICATIONS R.id.bottom_action_notification -> HomeTab.RoomList(RoomListDisplayMode.NOTIFICATIONS)
else -> HomeTab.DialPad
} }
viewModel.handle(HomeDetailAction.SwitchDisplayMode(displayMode)) viewModel.handle(HomeDetailAction.SwitchTab(tab))
true true
} }
@ -342,13 +350,14 @@ class HomeDetailFragment @Inject constructor(
// } // }
} }
private fun switchDisplayMode(displayMode: RoomListDisplayMode) { private fun updateUIForTab(tab: HomeTab) {
views.groupToolbarTitleView.setText(displayMode.titleRes) views.groupToolbarTitleView.setText(tab.titleRes)
updateSelectedFragment(displayMode) updateSelectedFragment(tab)
invalidateOptionsMenu()
} }
private fun updateSelectedFragment(displayMode: RoomListDisplayMode) { private fun updateSelectedFragment(tab: HomeTab) {
val fragmentTag = "FRAGMENT_TAG_${displayMode.name}" val fragmentTag = "FRAGMENT_TAG_$tab"
val fragmentToShow = childFragmentManager.findFragmentByTag(fragmentTag) val fragmentToShow = childFragmentManager.findFragmentByTag(fragmentTag)
childFragmentManager.commitTransaction { childFragmentManager.commitTransaction {
childFragmentManager.fragments childFragmentManager.fragments
@ -357,14 +366,49 @@ class HomeDetailFragment @Inject constructor(
detach(it) detach(it)
} }
if (fragmentToShow == null) { if (fragmentToShow == null) {
val params = RoomListParams(displayMode) when (tab) {
add(R.id.roomListContainer, RoomListFragment::class.java, params.toMvRxBundle(), fragmentTag) is HomeTab.RoomList -> {
val params = RoomListParams(tab.displayMode)
add(R.id.roomListContainer, RoomListFragment::class.java, params.toMvRxBundle(), fragmentTag)
}
is HomeTab.DialPad -> {
add(R.id.roomListContainer, createDialPadFragment())
}
}
} else { } else {
if (tab is HomeTab.DialPad) {
(fragmentToShow as? DialPadFragment)?.applyCallback()
}
attach(fragmentToShow) attach(fragmentToShow)
} }
} }
} }
private fun createDialPadFragment(): Fragment {
val fragment = childFragmentManager.fragmentFactory.instantiate(vectorBaseActivity.classLoader, DialPadFragment::class.java.name)
return (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()
}
}
private fun updateTabVisibilitySafely(tabId: Int, isVisible: Boolean) {
val wasVisible = views.bottomNavigationView.menu.findItem(tabId).isVisible
views.bottomNavigationView.menu.findItem(tabId).isVisible = isVisible
if (wasVisible && !isVisible) {
// As we hide it check if it's not the current item!
withState(viewModel) {
if (it.currentTab.toMenuId() == tabId) {
viewModel.handle(HomeDetailAction.SwitchTab(HomeTab.RoomList(RoomListDisplayMode.PEOPLE)))
}
}
}
}
/* ========================================================================================== /* ==========================================================================================
* KeysBackupBanner Listener * KeysBackupBanner Listener
* ========================================================================================== */ * ========================================================================================== */
@ -399,10 +443,13 @@ class HomeDetailFragment @Inject constructor(
} }
} }
private fun RoomListDisplayMode.toMenuId() = when (this) { private fun HomeTab.toMenuId() = when (this) {
RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people is HomeTab.DialPad -> R.id.bottom_action_dial_pad
RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms is HomeTab.RoomList -> when (displayMode) {
else -> R.id.bottom_action_notification RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people
RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms
else -> R.id.bottom_action_notification
}
} }
override fun onTapToReturnToCall() { override fun onTapToReturnToCall() {
@ -421,6 +468,16 @@ class HomeDetailFragment @Inject constructor(
} }
} }
private fun DialPadFragment.applyCallback(): DialPadFragment {
callback = object : DialPadFragment.Callback {
override fun onOkClicked(formatted: String?, raw: String?) {
if (raw.isNullOrEmpty()) return
viewModel.handle(HomeDetailAction.StartCallWithPhoneNumber(raw))
}
}
return this
}
override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel { override fun create(initialState: ServerBackupStatusViewState): ServerBackupStatusViewModel {
return serverBackupStatusViewModelFactory.create(initialState) return serverBackupStatusViewModelFactory.create(initialState)
} }

View file

@ -0,0 +1,25 @@
/*
* 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.home
import im.vector.app.core.platform.VectorViewEvents
sealed class HomeDetailViewEvents : VectorViewEvents {
object Loading : HomeDetailViewEvents()
object CallStarted : HomeDetailViewEvents()
data class FailToCall(val failure: Throwable) : HomeDetailViewEvents()
}

View file

@ -26,8 +26,11 @@ import dagger.assisted.AssistedInject
import im.vector.app.AppStateHandler import im.vector.app.AppStateHandler
import im.vector.app.RoomGroupingMethod import im.vector.app.RoomGroupingMethod
import im.vector.app.core.di.HasScreenInjector import im.vector.app.core.di.HasScreenInjector
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.call.dialpad.DialPadLookup
import im.vector.app.features.call.lookup.CallProtocolsChecker
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.createdirect.DirectRoomHelper
import im.vector.app.features.ui.UiStateRepository import im.vector.app.features.ui.UiStateRepository
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -51,8 +54,11 @@ import java.util.concurrent.TimeUnit
class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: HomeDetailViewState, class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: HomeDetailViewState,
private val session: Session, private val session: Session,
private val uiStateRepository: UiStateRepository, private val uiStateRepository: UiStateRepository,
private val callManager: WebRtcCallManager,
private val directRoomHelper: DirectRoomHelper,
private val appStateHandler: AppStateHandler) private val appStateHandler: AppStateHandler)
: VectorViewModel<HomeDetailViewState, HomeDetailAction, EmptyViewEvents>(initialState) { : VectorViewModel<HomeDetailViewState, HomeDetailAction, HomeDetailViewEvents>(initialState),
CallProtocolsChecker.Listener {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
@ -64,7 +70,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
override fun initialState(viewModelContext: ViewModelContext): HomeDetailViewState? { override fun initialState(viewModelContext: ViewModelContext): HomeDetailViewState? {
val uiStateRepository = (viewModelContext.activity as HasScreenInjector).injector().uiStateRepository() val uiStateRepository = (viewModelContext.activity as HasScreenInjector).injector().uiStateRepository()
return HomeDetailViewState( return HomeDetailViewState(
displayMode = uiStateRepository.getDisplayMode() currentTab = HomeTab.RoomList(uiStateRepository.getDisplayMode())
) )
} }
@ -79,7 +85,8 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
observeSyncState() observeSyncState()
observeRoomGroupingMethod() observeRoomGroupingMethod()
observeRoomSummaries() observeRoomSummaries()
updateShowDialPadTab()
callManager.addProtocolsCheckerListener(this)
session.rx().liveUser(session.myUserId).execute { session.rx().liveUser(session.myUserId).execute {
copy( copy(
myMatrixItem = it.invoke()?.getOrNull()?.toMatrixItem() myMatrixItem = it.invoke()?.getOrNull()?.toMatrixItem()
@ -89,18 +96,48 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
override fun handle(action: HomeDetailAction) { override fun handle(action: HomeDetailAction) {
when (action) { when (action) {
is HomeDetailAction.SwitchDisplayMode -> handleSwitchDisplayMode(action) is HomeDetailAction.SwitchTab -> handleSwitchTab(action)
HomeDetailAction.MarkAllRoomsRead -> handleMarkAllRoomsRead() HomeDetailAction.MarkAllRoomsRead -> handleMarkAllRoomsRead()
is HomeDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action)
} }
} }
private fun handleSwitchDisplayMode(action: HomeDetailAction.SwitchDisplayMode) = withState { state -> private fun handleStartCallWithPhoneNumber(action: HomeDetailAction.StartCallWithPhoneNumber) {
if (state.displayMode != action.displayMode) { viewModelScope.launch {
setState { try {
copy(displayMode = action.displayMode) _viewEvents.post(HomeDetailViewEvents.Loading)
val result = DialPadLookup(session, callManager, directRoomHelper).lookupPhoneNumber(action.phoneNumber)
callManager.startOutgoingCall(result.roomId, result.userId, isVideoCall = false)
_viewEvents.post(HomeDetailViewEvents.CallStarted)
} catch (failure: Throwable) {
_viewEvents.post(HomeDetailViewEvents.FailToCall(failure))
} }
}
}
uiStateRepository.storeDisplayMode(action.displayMode) private fun handleSwitchTab(action: HomeDetailAction.SwitchTab) = withState { state ->
if (state.currentTab != action.tab) {
setState {
copy(currentTab = action.tab)
}
if (action.tab is HomeTab.RoomList) {
uiStateRepository.storeDisplayMode(action.tab.displayMode)
}
}
}
override fun onCleared() {
super.onCleared()
callManager.removeProtocolsCheckerListener(this)
}
override fun onPSTNSupportUpdated() {
updateShowDialPadTab()
}
private fun updateShowDialPadTab() {
setState {
copy(showDialPadTab = callManager.supportsPSTNProtocol)
} }
} }
@ -138,11 +175,11 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
private fun observeRoomGroupingMethod() { private fun observeRoomGroupingMethod() {
appStateHandler.selectedRoomGroupingObservable appStateHandler.selectedRoomGroupingObservable
.subscribe { .subscribe {
setState { setState {
copy( copy(
roomGroupingMethod = it.orNull() ?: RoomGroupingMethod.BySpace(null) roomGroupingMethod = it.orNull() ?: RoomGroupingMethod.BySpace(null)
) )
} }
} }
.disposeOnClear() .disposeOnClear()
} }
@ -165,7 +202,7 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
is RoomGroupingMethod.ByLegacyGroup -> { is RoomGroupingMethod.ByLegacyGroup -> {
// TODO!! // TODO!!
} }
is RoomGroupingMethod.BySpace -> { is RoomGroupingMethod.BySpace -> {
val activeSpaceRoomId = groupingMethod.spaceSummary?.roomId val activeSpaceRoomId = groupingMethod.spaceSummary?.roomId
val dmInvites = session.getRoomSummaries( val dmInvites = session.getRoomSummaries(
roomSummaryQueryParams { roomSummaryQueryParams {

View file

@ -16,9 +16,11 @@
package im.vector.app.features.home package im.vector.app.features.home
import androidx.annotation.StringRes
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.app.R
import im.vector.app.RoomGroupingMethod import im.vector.app.RoomGroupingMethod
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.session.sync.SyncState
@ -28,7 +30,7 @@ data class HomeDetailViewState(
val roomGroupingMethod: RoomGroupingMethod = RoomGroupingMethod.BySpace(null), val roomGroupingMethod: RoomGroupingMethod = RoomGroupingMethod.BySpace(null),
val myMatrixItem: MatrixItem? = null, val myMatrixItem: MatrixItem? = null,
val asyncRooms: Async<List<RoomSummary>> = Uninitialized, val asyncRooms: Async<List<RoomSummary>> = Uninitialized,
val displayMode: RoomListDisplayMode = RoomListDisplayMode.PEOPLE, val currentTab: HomeTab = HomeTab.RoomList(RoomListDisplayMode.PEOPLE),
val notificationCountCatchup: Int = 0, val notificationCountCatchup: Int = 0,
val notificationHighlightCatchup: Boolean = false, val notificationHighlightCatchup: Boolean = false,
val notificationCountPeople: Int = 0, val notificationCountPeople: Int = 0,
@ -36,5 +38,11 @@ data class HomeDetailViewState(
val notificationCountRooms: Int = 0, val notificationCountRooms: Int = 0,
val notificationHighlightRooms: Boolean = false, val notificationHighlightRooms: Boolean = false,
val hasUnreadMessages: Boolean = false, val hasUnreadMessages: Boolean = false,
val syncState: SyncState = SyncState.Idle val syncState: SyncState = SyncState.Idle,
val showDialPadTab: Boolean = false
) : MvRxState ) : MvRxState
sealed class HomeTab(@StringRes val titleRes: Int) {
data class RoomList(val displayMode: RoomListDisplayMode) : HomeTab(displayMode.titleRes)
object DialPad : HomeTab(R.string.call_dial_pad_title)
}

View file

@ -73,7 +73,6 @@ sealed class RoomDetailAction : VectorViewModelAction {
object ResendAll : RoomDetailAction() object ResendAll : RoomDetailAction()
data class StartCallWithPhoneNumber(val phoneNumber: String, val videoCall: Boolean): RoomDetailAction()
data class StartCall(val isVideo: Boolean) : RoomDetailAction() data class StartCall(val isVideo: Boolean) : RoomDetailAction()
data class AcceptCall(val callId: String): RoomDetailAction() data class AcceptCall(val callId: String): RoomDetailAction()
object EndCall : RoomDetailAction() object EndCall : RoomDetailAction()

View file

@ -320,7 +320,7 @@ class RoomDetailFragment @Inject constructor(
startCallActivityResultLauncher = startCallActivityResultLauncher, startCallActivityResultLauncher = startCallActivityResultLauncher,
showDialogWithMessage = ::showDialogWithMessage, showDialogWithMessage = ::showDialogWithMessage,
onTapToReturnToCall = ::onTapToReturnToCall onTapToReturnToCall = ::onTapToReturnToCall
).register() )
keyboardStateUtils = KeyboardStateUtils(requireActivity()) keyboardStateUtils = KeyboardStateUtils(requireActivity())
setupToolbar(views.roomToolbar) setupToolbar(views.roomToolbar)
setupRecyclerView() setupRecyclerView()

View file

@ -39,7 +39,6 @@ import im.vector.app.core.mvrx.runCatchingToAsync
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.call.conference.JitsiService import im.vector.app.features.call.conference.JitsiService
import im.vector.app.features.call.dialpad.DialPadLookup
import im.vector.app.features.call.lookup.CallProtocolsChecker import im.vector.app.features.call.lookup.CallProtocolsChecker
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.command.CommandParser import im.vector.app.features.command.CommandParser
@ -176,7 +175,6 @@ class RoomDetailViewModel @AssistedInject constructor(
observeMyRoomMember() observeMyRoomMember()
observeActiveRoomWidgets() observeActiveRoomWidgets()
observePowerLevel() observePowerLevel()
updateShowDialerOptionState()
room.getRoomSummaryLive() room.getRoomSummaryLive()
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
tryOrNull { room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) } tryOrNull { room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) }
@ -301,7 +299,6 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action)
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
is RoomDetailAction.StartCallWithPhoneNumber -> handleStartCallWithPhoneNumber(action)
is RoomDetailAction.StartCall -> handleStartCall(action) is RoomDetailAction.StartCall -> handleStartCall(action)
is RoomDetailAction.AcceptCall -> handleAcceptCall(action) is RoomDetailAction.AcceptCall -> handleAcceptCall(action)
is RoomDetailAction.EndCall -> handleEndCall() is RoomDetailAction.EndCall -> handleEndCall()
@ -327,17 +324,6 @@ class RoomDetailViewModel @AssistedInject constructor(
}.exhaustive }.exhaustive
} }
private fun handleStartCallWithPhoneNumber(action: RoomDetailAction.StartCallWithPhoneNumber) {
viewModelScope.launch {
try {
val result = DialPadLookup(session, callManager, directRoomHelper).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) { private fun handleAcceptCall(action: RoomDetailAction.AcceptCall) {
callManager.getCallById(action.callId)?.also { callManager.getCallById(action.callId)?.also {
_viewEvents.post(RoomDetailViewEvents.DisplayAndAcceptCall(it)) _viewEvents.post(RoomDetailViewEvents.DisplayAndAcceptCall(it))
@ -1491,16 +1477,6 @@ class RoomDetailViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.OnNewTimelineEvents(eventIds)) _viewEvents.post(RoomDetailViewEvents.OnNewTimelineEvents(eventIds))
} }
override fun onPSTNSupportUpdated() {
updateShowDialerOptionState()
}
private fun updateShowDialerOptionState() {
setState {
copy(showDialerOption = callManager.supportsPSTNProtocol)
}
}
override fun onCleared() { override fun onCleared() {
roomSummariesHolder.remove(room.roomId) roomSummariesHolder.remove(room.roomId)
timeline.dispose() timeline.dispose()

View file

@ -75,7 +75,6 @@ data class RoomDetailViewState(
val canInvite: Boolean = true, val canInvite: Boolean = true,
val isAllowedToManageWidgets: Boolean = false, val isAllowedToManageWidgets: Boolean = false,
val isAllowedToStartWebRTCCall: Boolean = true, val isAllowedToStartWebRTCCall: Boolean = true,
val showDialerOption: Boolean = false,
val hasFailedSending: Boolean = false val hasFailedSending: Boolean = false
) : MvRxState { ) : MvRxState {

View file

@ -16,26 +16,18 @@
package im.vector.app.features.home.room.detail package im.vector.app.features.home.room.detail
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R 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_AUDIO_IP_CALL
import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL
import im.vector.app.core.utils.checkPermissions 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.call.webrtc.WebRtcCallManager
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.session.widgets.model.WidgetType 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( class StartCallActionsHandler(
private val roomId: String, private val roomId: String,
private val fragment: Fragment, private val fragment: Fragment,
@ -44,52 +36,20 @@ class StartCallActionsHandler(
private val roomDetailViewModel: RoomDetailViewModel, private val roomDetailViewModel: RoomDetailViewModel,
private val startCallActivityResultLauncher: ActivityResultLauncher<Array<String>>, private val startCallActivityResultLauncher: ActivityResultLauncher<Array<String>>,
private val showDialogWithMessage: (String) -> Unit, private val showDialogWithMessage: (String) -> Unit,
private val onTapToReturnToCall: () -> Unit): Restorable { private val onTapToReturnToCall: () -> Unit) {
fun onVideoCallClicked() { fun onVideoCallClicked() {
handleCallRequest(true) handleCallRequest(true)
} }
fun onVoiceCallClicked() = withState(roomDetailViewModel) { fun onVoiceCallClicked() {
if (it.showDialerOption) { handleCallRequest(false)
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 -> private fun handleCallRequest(isVideoCall: Boolean) = withState(roomDetailViewModel) { state ->
val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState
when (roomSummary.joinedMembersCount) { when (roomSummary.joinedMembersCount) {
1 -> { 1 -> {
val pendingInvite = roomSummary.invitedMembersCount ?: 0 > 0 val pendingInvite = roomSummary.invitedMembersCount ?: 0 > 0
if (pendingInvite) { if (pendingInvite) {
// wait for other to join // wait for other to join
@ -99,7 +59,7 @@ class StartCallActionsHandler(
showDialogWithMessage(fragment.getString(R.string.cannot_call_yourself)) showDialogWithMessage(fragment.getString(R.string.cannot_call_yourself))
} }
} }
2 -> { 2 -> {
val currentCall = callManager.getCurrentCall() val currentCall = callManager.getCurrentCall()
if (currentCall != null) { if (currentCall != null) {
// resume existing if same room, if not prompt to kill and then restart new call? // resume existing if same room, if not prompt to kill and then restart new call?
@ -190,13 +150,4 @@ class StartCallActionsHandler(
} }
} }
} }
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

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

@ -20,4 +20,11 @@
android:title="@string/bottom_action_notification" android:title="@string/bottom_action_notification"
android:visible="false" /> android:visible="false" />
<item
android:id="@+id/bottom_action_dial_pad"
android:enabled="true"
android:icon="@drawable/ic_call_dial_pad"
android:title="@string/call_dial_pad_title"
android:visible="false" />
</menu> </menu>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="DialpadKeyNumberStyle">
<item name="android:textColor">?attr/vctr_content_primary</item>
<item name="android:textSize">@dimen/dialpad_key_numbers_default_size</item>
<item name="android:fontFamily">sans-serif</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>
<style name="DialpadKeyLettersStyle">
<item name="android:textColor">?attr/vctr_content_secondary</item>
<item name="android:textSize">@dimen/dialpad_key_letters_size</item>
<item name="android:fontFamily">sans-serif-regular</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:gravity">center_horizontal</item>
</style>
<style name="DialpadKeyPoundStyle" parent="DialpadKeyNumberStyle">
<item name="android:textSize">@dimen/dialpad_key_pound_size</item>
<item name="android:layout_marginBottom">@dimen/dialpad_symbol_margin_bottom</item>
</style>
<style name="DialpadKeyStarStyle" parent="DialpadKeyNumberStyle">
<item name="android:textSize">@dimen/dialpad_key_star_size</item>
<item name="android:layout_marginBottom">@dimen/dialpad_symbol_margin_bottom</item>
</style>
</resources>

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>

View file

@ -1,12 +1,33 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="DialpadKeyNumberStyle"> <style name="DialpadKeyNumberStyle">
<item name="android:textColor">?vctr_content_primary</item> <item name="android:textColor">?vctr_content_primary</item>
<item name="android:textSize">@dimen/dialpad_key_numbers_default_size</item> <item name="android:textSize">@dimen/dialpad_key_numbers_default_size</item>
<item name="android:fontFamily">sans-serif-light</item> <item name="android:fontFamily">sans-serif</item>
<item name="android:layout_width">wrap_content</item> <item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">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:layout_marginBottom">@dimen/dialpad_key_number_default_margin_bottom</item>
<item name="android:gravity">center</item> <item name="android:gravity">center</item>
</style> </style>
<style name="DialpadKeyLettersStyle">
<item name="android:textColor">?attr/vctr_content_secondary</item>
<item name="android:textSize">@dimen/dialpad_key_letters_size</item>
<item name="android:fontFamily">sans-serif-regular</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:gravity">center_horizontal</item>
</style>
<style name="DialpadKeyPoundStyle" parent="DialpadKeyNumberStyle">
<item name="android:textSize">@dimen/dialpad_key_pound_size</item>
<item name="android:layout_marginBottom">@dimen/dialpad_symbol_margin_bottom</item>
</style>
<style name="DialpadKeyStarStyle" parent="DialpadKeyNumberStyle">
<item name="android:textSize">@dimen/dialpad_key_star_size</item>
<item name="android:layout_marginBottom">@dimen/dialpad_symbol_margin_bottom</item>
</style>
</resources> </resources>