mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 05:31:21 +03:00
Merge branch 'develop' into feature/fga/timeline_virtual_room
This commit is contained in:
commit
6d9c49462a
42 changed files with 620 additions and 247 deletions
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
|
@ -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>()
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
1
newsfragment/3333.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Fix new DMs not always marked as such
|
1
newsfragment/3451.feature
Normal file
1
newsfragment/3451.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Adds support for receiving MSC3086 Asserted Identity events.
|
1
newsfragment/3457.misc
Normal file
1
newsfragment/3457.misc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Move the ability to start a call from dialpad directly to a dedicated tab in the home screen.
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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? {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
32
vector/src/main/res/values-land/styles_dial_pad.xml
Normal file
32
vector/src/main/res/values-land/styles_dial_pad.xml
Normal 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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
Loading…
Reference in a new issue