mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 13:38:49 +03:00
Merge pull request #2575 from vector-im/feature/fga/voip_switch_call_and_transfer_init
Feature/fga/voip switch call and transfer init
This commit is contained in:
commit
19fc4419c3
85 changed files with 1465 additions and 480 deletions
|
@ -35,7 +35,11 @@ data class MatrixConfiguration(
|
|||
* Optional proxy to connect to the matrix servers
|
||||
* You can create one using for instance Proxy(proxyType, InetSocketAddress.createUnresolved(hostname, port)
|
||||
*/
|
||||
val proxy: Proxy? = null
|
||||
val proxy: Proxy? = null,
|
||||
/**
|
||||
* True to advertise support for call transfers to other parties on Matrix calls.
|
||||
*/
|
||||
val supportsCallTransfer: Boolean = false
|
||||
) {
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package org.matrix.android.sdk.api.session.call
|
||||
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallCandidate
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.SdpType
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
|
@ -42,6 +43,8 @@ interface MxCall : MxCallDetail {
|
|||
var opponentPartyId: Optional<String>?
|
||||
var opponentVersion: Int
|
||||
|
||||
var capabilities: CallCapabilities?
|
||||
|
||||
var state: CallState
|
||||
|
||||
/**
|
||||
|
@ -86,6 +89,11 @@ interface MxCall : MxCallDetail {
|
|||
*/
|
||||
fun sendLocalIceCandidateRemovals(candidates: List<CallCandidate>)
|
||||
|
||||
/**
|
||||
* Send a m.call.replaces event to initiate call transfer.
|
||||
*/
|
||||
suspend fun transfer(targetUserId: String, targetRoomId: String?)
|
||||
|
||||
fun addListener(listener: StateListener)
|
||||
fun removeListener(listener: StateListener)
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ object EventType {
|
|||
const val CALL_NEGOTIATE = "m.call.negotiate"
|
||||
const val CALL_REJECT = "m.call.reject"
|
||||
const val CALL_HANGUP = "m.call.hangup"
|
||||
const val CALL_REPLACES = "m.call.replaces"
|
||||
|
||||
// Key share events
|
||||
const val ROOM_KEY_REQUEST = "m.room_key_request"
|
||||
|
|
|
@ -39,7 +39,11 @@ data class CallAnswerContent(
|
|||
/**
|
||||
* Required. The version of the VoIP specification this messages adheres to.
|
||||
*/
|
||||
@Json(name = "version") override val version: String?
|
||||
@Json(name = "version") override val version: String?,
|
||||
/**
|
||||
* Capability advertisement.
|
||||
*/
|
||||
@Json(name = "capabilities") val capabilities: CallCapabilities? = null
|
||||
): CallSignallingContent {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.room.model.call
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CallCapabilities(
|
||||
/**
|
||||
* If set to true, states that the sender of the event supports the m.call.replaces event and therefore supports
|
||||
* being transferred to another destination
|
||||
*/
|
||||
@Json(name = "m.call.transferee") val transferee: Boolean? = null
|
||||
)
|
||||
|
||||
fun CallCapabilities?.supportCallTransfer() = this?.transferee.orFalse()
|
|
@ -49,7 +49,11 @@ data class CallInviteContent(
|
|||
/**
|
||||
* The field should be added for all invites where the target is a specific user
|
||||
*/
|
||||
@Json(name = "invitee") val invitee: String? = null
|
||||
@Json(name = "invitee") val invitee: String? = null,
|
||||
/**
|
||||
* Capability advertisement.
|
||||
*/
|
||||
@Json(name = "capabilities") val capabilities: CallCapabilities? = null
|
||||
|
||||
): CallSignallingContent {
|
||||
@JsonClass(generateAdapter = true)
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.matrix.android.sdk.api.session.room.model.call
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
|
||||
/**
|
||||
* This event is sent to signal the intent of a participant in a call to replace the call with another,
|
||||
* such that the other participant ends up in a call with a new user.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class CallReplacesContent(
|
||||
/**
|
||||
* 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,
|
||||
/**
|
||||
* An identifier for the call replacement itself, generated by the transferor.
|
||||
*/
|
||||
@Json(name = "replacement_id") val replacementId: String? = null,
|
||||
/**
|
||||
* Optional. If specified, the transferee client waits for an invite to this room and joins it
|
||||
* (possibly waiting for user confirmation) and then continues the transfer in this room.
|
||||
* If absent, the transferee contacts the Matrix User ID given in the target_user field in a room of its choosing.
|
||||
*/
|
||||
@Json(name = "target_room") val targerRoomId: String? = null,
|
||||
/**
|
||||
* An object giving information about the transfer target
|
||||
*/
|
||||
@Json(name = "target_user") val targetUser: TargetUser? = null,
|
||||
/**
|
||||
* If specified, gives the call ID for the transferee's client to use when placing the replacement call.
|
||||
* Mutually exclusive with await_call
|
||||
*/
|
||||
@Json(name = "create_call") val createCall: String? = null,
|
||||
/**
|
||||
* If specified, gives the call ID that the transferee's client should wait for.
|
||||
* Mutually exclusive with create_call.
|
||||
*/
|
||||
@Json(name = "await_call") val awaitCall: String? = null,
|
||||
/**
|
||||
* Required. The version of the VoIP specification this messages adheres to.
|
||||
*/
|
||||
@Json(name = "version") override val version: String?
|
||||
): CallSignallingContent {
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class TargetUser(
|
||||
/**
|
||||
* Required. The matrix user ID of the transfer target
|
||||
*/
|
||||
@Json(name = "id") val id: String,
|
||||
/**
|
||||
* Optional. The display name of the transfer target.
|
||||
*/
|
||||
@Json(name = "display_name") val displayName: String?,
|
||||
/**
|
||||
* Optional. The avatar URL of the transfer target.
|
||||
*/
|
||||
@Json(name = "avatar_url") val avatarUrl: String?
|
||||
|
||||
)
|
||||
}
|
|
@ -52,6 +52,8 @@ data class TimelineEvent(
|
|||
}
|
||||
}
|
||||
|
||||
val roomId = root.roomId ?: ""
|
||||
|
||||
val metadata = HashMap<String, Any>()
|
||||
|
||||
/**
|
||||
|
|
|
@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType
|
|||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities
|
||||
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.CallNegotiateContent
|
||||
|
@ -185,6 +186,7 @@ internal class CallSignalingHandler @Inject constructor(private val activeCallHa
|
|||
call.apply {
|
||||
opponentPartyId = Optional.from(content.partyId)
|
||||
opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION
|
||||
capabilities = content.capabilities ?: CallCapabilities()
|
||||
}
|
||||
callListenersDispatcher.onCallAnswerReceived(content)
|
||||
}
|
||||
|
|
|
@ -16,12 +16,15 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.call
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||
import org.matrix.android.sdk.api.session.call.MxCall
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.internal.di.DeviceId
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.session.call.model.MxCallImpl
|
||||
import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||
import java.math.BigDecimal
|
||||
|
@ -32,6 +35,8 @@ internal class MxCallFactory @Inject constructor(
|
|||
@DeviceId private val deviceId: String?,
|
||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val eventSenderProcessor: EventSenderProcessor,
|
||||
private val matrixConfiguration: MatrixConfiguration,
|
||||
private val getProfileInfoTask: GetProfileInfoTask,
|
||||
@UserId private val userId: String
|
||||
) {
|
||||
|
||||
|
@ -46,10 +51,13 @@ internal class MxCallFactory @Inject constructor(
|
|||
opponentUserId = opponentUserId,
|
||||
isVideoCall = content.isVideo(),
|
||||
localEchoEventFactory = localEchoEventFactory,
|
||||
eventSenderProcessor = eventSenderProcessor
|
||||
eventSenderProcessor = eventSenderProcessor,
|
||||
matrixConfiguration = matrixConfiguration,
|
||||
getProfileInfoTask = getProfileInfoTask
|
||||
).apply {
|
||||
opponentPartyId = Optional.from(content.partyId)
|
||||
opponentVersion = content.version?.let { BigDecimal(it).intValueExact() } ?: MxCall.VOIP_PROTO_VERSION
|
||||
capabilities = content.capabilities ?: CallCapabilities()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,7 +71,9 @@ internal class MxCallFactory @Inject constructor(
|
|||
opponentUserId = opponentUserId,
|
||||
isVideoCall = isVideoCall,
|
||||
localEchoEventFactory = localEchoEventFactory,
|
||||
eventSenderProcessor = eventSenderProcessor
|
||||
eventSenderProcessor = eventSenderProcessor,
|
||||
matrixConfiguration = matrixConfiguration,
|
||||
getProfileInfoTask = getProfileInfoTask
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.call.model
|
||||
|
||||
import org.matrix.android.sdk.api.MatrixConfiguration
|
||||
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.events.model.Content
|
||||
|
@ -24,20 +25,25 @@ import org.matrix.android.sdk.api.session.events.model.EventType
|
|||
import org.matrix.android.sdk.api.session.events.model.LocalEcho
|
||||
import org.matrix.android.sdk.api.session.events.model.UnsignedData
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.profile.ProfileService
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallCandidate
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallCapabilities
|
||||
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.CallNegotiateContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallReplacesContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallSelectAnswerContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.SdpType
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.internal.session.call.DefaultCallSignalingService
|
||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||
import org.matrix.android.sdk.internal.session.profile.GetProfileInfoTask
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||
import timber.log.Timber
|
||||
import java.util.UUID
|
||||
|
||||
internal class MxCallImpl(
|
||||
override val callId: String,
|
||||
|
@ -48,11 +54,14 @@ internal class MxCallImpl(
|
|||
override val isVideoCall: Boolean,
|
||||
override val ourPartyId: String,
|
||||
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||
private val eventSenderProcessor: EventSenderProcessor
|
||||
private val eventSenderProcessor: EventSenderProcessor,
|
||||
private val matrixConfiguration: MatrixConfiguration,
|
||||
private val getProfileInfoTask: GetProfileInfoTask
|
||||
) : MxCall {
|
||||
|
||||
override var opponentPartyId: Optional<String>? = null
|
||||
override var opponentVersion: Int = MxCall.VOIP_PROTO_VERSION
|
||||
override var capabilities: CallCapabilities? = null
|
||||
|
||||
override var state: CallState = CallState.Idle
|
||||
set(value) {
|
||||
|
@ -98,7 +107,8 @@ internal class MxCallImpl(
|
|||
partyId = ourPartyId,
|
||||
lifetime = DefaultCallSignalingService.CALL_TIMEOUT_MS,
|
||||
offer = CallInviteContent.Offer(sdp = sdpString),
|
||||
version = MxCall.VOIP_PROTO_VERSION.toString()
|
||||
version = MxCall.VOIP_PROTO_VERSION.toString(),
|
||||
capabilities = buildCapabilities()
|
||||
)
|
||||
.let { createEventAndLocalEcho(type = EventType.CALL_INVITE, roomId = roomId, content = it.toContent()) }
|
||||
.also { eventSenderProcessor.postEvent(it) }
|
||||
|
@ -158,7 +168,8 @@ internal class MxCallImpl(
|
|||
callId = callId,
|
||||
partyId = ourPartyId,
|
||||
answer = CallAnswerContent.Answer(sdp = sdpString),
|
||||
version = MxCall.VOIP_PROTO_VERSION.toString()
|
||||
version = MxCall.VOIP_PROTO_VERSION.toString(),
|
||||
capabilities = buildCapabilities()
|
||||
)
|
||||
.let { createEventAndLocalEcho(type = EventType.CALL_ANSWER, roomId = roomId, content = it.toContent()) }
|
||||
.also { eventSenderProcessor.postEvent(it) }
|
||||
|
@ -191,6 +202,31 @@ internal class MxCallImpl(
|
|||
.also { eventSenderProcessor.postEvent(it) }
|
||||
}
|
||||
|
||||
override suspend fun transfer(targetUserId: String, targetRoomId: String?) {
|
||||
val profileInfoParams = GetProfileInfoTask.Params(targetUserId)
|
||||
val profileInfo = try {
|
||||
getProfileInfoTask.execute(profileInfoParams)
|
||||
} catch (failure: Throwable) {
|
||||
Timber.v("Fail fetching profile info of $targetUserId while transferring call")
|
||||
null
|
||||
}
|
||||
CallReplacesContent(
|
||||
callId = callId,
|
||||
partyId = ourPartyId,
|
||||
replacementId = UUID.randomUUID().toString(),
|
||||
version = MxCall.VOIP_PROTO_VERSION.toString(),
|
||||
targetUser = CallReplacesContent.TargetUser(
|
||||
id = targetUserId,
|
||||
displayName = profileInfo?.get(ProfileService.DISPLAY_NAME_KEY) as? String,
|
||||
avatarUrl = profileInfo?.get(ProfileService.AVATAR_URL_KEY) as? String
|
||||
),
|
||||
targerRoomId = targetRoomId,
|
||||
createCall = UUID.randomUUID().toString()
|
||||
)
|
||||
.let { createEventAndLocalEcho(type = EventType.CALL_REPLACES, roomId = roomId, content = it.toContent()) }
|
||||
.also { eventSenderProcessor.postEvent(it) }
|
||||
}
|
||||
|
||||
private fun createEventAndLocalEcho(localId: String = LocalEcho.createLocalEchoId(), type: String, roomId: String, content: Content): Event {
|
||||
return Event(
|
||||
roomId = roomId,
|
||||
|
@ -203,4 +239,12 @@ internal class MxCallImpl(
|
|||
)
|
||||
.also { localEchoEventFactory.createLocalEcho(it) }
|
||||
}
|
||||
|
||||
private fun buildCapabilities(): CallCapabilities? {
|
||||
return if (matrixConfiguration.supportsCallTransfer) {
|
||||
CallCapabilities(true)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,8 +49,10 @@ internal class QueueMemento @Inject constructor(context: Context,
|
|||
}
|
||||
|
||||
fun unTrack(task: QueuedTask) {
|
||||
managedTaskInfos.remove(task)
|
||||
persist()
|
||||
synchronized(managedTaskInfos) {
|
||||
managedTaskInfos.remove(task)
|
||||
persist()
|
||||
}
|
||||
}
|
||||
|
||||
private fun persist() {
|
||||
|
@ -64,19 +66,17 @@ internal class QueueMemento @Inject constructor(context: Context,
|
|||
}
|
||||
|
||||
private fun toTaskInfo(task: QueuedTask, order: Int): TaskInfo? {
|
||||
synchronized(managedTaskInfos) {
|
||||
return when (task) {
|
||||
is SendEventQueuedTask -> SendEventTaskInfo(
|
||||
localEchoId = task.event.eventId ?: "",
|
||||
encrypt = task.encrypt,
|
||||
order = order
|
||||
)
|
||||
is RedactQueuedTask -> RedactEventTaskInfo(
|
||||
redactionLocalEcho = task.redactionLocalEchoId,
|
||||
order = order
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
return when (task) {
|
||||
is SendEventQueuedTask -> SendEventTaskInfo(
|
||||
localEchoId = task.event.eventId ?: "",
|
||||
encrypt = task.encrypt,
|
||||
order = order
|
||||
)
|
||||
is RedactQueuedTask -> RedactEventTaskInfo(
|
||||
redactionLocalEcho = task.redactionLocalEchoId,
|
||||
order = order
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,7 +90,7 @@ internal class QueueMemento @Inject constructor(context: Context,
|
|||
?.forEach { info ->
|
||||
try {
|
||||
when (info) {
|
||||
is SendEventTaskInfo -> {
|
||||
is SendEventTaskInfo -> {
|
||||
localEchoRepository.getUpToDateEcho(info.localEchoId)?.let {
|
||||
if (it.sendState.isSending() && it.eventId != null && it.roomId != null) {
|
||||
localEchoRepository.updateSendState(it.eventId, it.roomId, SendState.UNSENT)
|
||||
|
|
|
@ -243,6 +243,7 @@
|
|||
<activity android:name=".features.pin.PinActivity" />
|
||||
<activity android:name=".features.home.room.detail.search.SearchActivity" />
|
||||
<activity android:name=".features.usercode.UserCodeActivity" />
|
||||
<activity android:name=".features.call.transfer.CallTransferActivity" />
|
||||
|
||||
<!-- Services -->
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import im.vector.app.features.MainActivity
|
|||
import im.vector.app.features.call.CallControlsBottomSheet
|
||||
import im.vector.app.features.call.VectorCallActivity
|
||||
import im.vector.app.features.call.conference.VectorJitsiActivity
|
||||
import im.vector.app.features.call.transfer.CallTransferActivity
|
||||
import im.vector.app.features.createdirect.CreateDirectRoomActivity
|
||||
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
|
||||
|
@ -146,6 +147,7 @@ interface ScreenComponent {
|
|||
fun inject(activity: VectorJitsiActivity)
|
||||
fun inject(activity: SearchActivity)
|
||||
fun inject(activity: UserCodeActivity)
|
||||
fun inject(activity: CallTransferActivity)
|
||||
|
||||
/* ==========================================================================================
|
||||
* BottomSheets
|
||||
|
|
|
@ -38,6 +38,7 @@ import im.vector.app.features.home.AvatarRenderer
|
|||
import im.vector.app.features.home.HomeRoomListDataSource
|
||||
import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import im.vector.app.features.html.EventHtmlRenderer
|
||||
import im.vector.app.features.html.VectorHtmlCompressor
|
||||
import im.vector.app.features.login.ReAuthHelper
|
||||
|
@ -158,6 +159,8 @@ interface VectorComponent {
|
|||
|
||||
fun webRtcCallManager(): WebRtcCallManager
|
||||
|
||||
fun roomSummaryHolder(): RoomSummariesHolder
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
fun create(@BindsInstance context: Context): VectorComponent
|
||||
|
|
|
@ -22,7 +22,7 @@ import dagger.Binds
|
|||
import dagger.Module
|
||||
import dagger.multibindings.IntoMap
|
||||
import im.vector.app.core.platform.ConfigurationViewModel
|
||||
import im.vector.app.features.call.SharedActiveCallViewModel
|
||||
import im.vector.app.features.call.SharedKnownCallsViewModel
|
||||
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreFromKeyViewModel
|
||||
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreFromPassphraseViewModel
|
||||
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreSharedViewModel
|
||||
|
@ -85,8 +85,8 @@ interface ViewModelModule {
|
|||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(SharedActiveCallViewModel::class)
|
||||
fun bindSharedActiveCallViewModel(viewModel: SharedActiveCallViewModel): ViewModel
|
||||
@ViewModelKey(SharedKnownCallsViewModel::class)
|
||||
fun bindSharedActiveCallViewModel(viewModel: SharedKnownCallsViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
|
|
|
@ -17,10 +17,18 @@
|
|||
package im.vector.app.core.extensions
|
||||
|
||||
// Create a new Set including the provided element if not already present, or removing the element if already present
|
||||
fun <T> Set<T>.toggle(element: T): Set<T> {
|
||||
fun <T> Set<T>.toggle(element: T, singleElement: Boolean = false): Set<T> {
|
||||
return if (contains(element)) {
|
||||
minus(element)
|
||||
if (singleElement) {
|
||||
emptySet()
|
||||
} else {
|
||||
minus(element)
|
||||
}
|
||||
} else {
|
||||
plus(element)
|
||||
if (singleElement) {
|
||||
setOf(element)
|
||||
} else {
|
||||
plus(element)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,7 @@ import androidx.lifecycle.ViewModelProvider
|
|||
import androidx.viewbinding.ViewBinding
|
||||
import com.bumptech.glide.util.Util
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.jakewharton.rxbinding3.view.clicks
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
|
@ -84,6 +85,7 @@ import io.reactivex.disposables.Disposable
|
|||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.failure.GlobalError
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
abstract class VectorBaseActivity<VB: ViewBinding> : AppCompatActivity(), HasScreenInjector {
|
||||
|
@ -113,6 +115,18 @@ abstract class VectorBaseActivity<VB: ViewBinding> : AppCompatActivity(), HasScr
|
|||
.disposeOnDestroy()
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Views
|
||||
* ========================================================================================== */
|
||||
|
||||
protected fun View.debouncedClicks(onClicked: () -> Unit) {
|
||||
clicks()
|
||||
.throttleFirst(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { onClicked() }
|
||||
.disposeOnDestroy()
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* DATA
|
||||
* ========================================================================================== */
|
||||
|
|
|
@ -22,6 +22,7 @@ import android.content.Intent
|
|||
import android.os.Binder
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.view.KeyEvent
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media.session.MediaButtonReceiver
|
||||
import com.airbnb.mvrx.MvRx
|
||||
|
@ -46,7 +47,9 @@ import timber.log.Timber
|
|||
class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListener, BluetoothHeadsetReceiver.EventListener {
|
||||
|
||||
private val connections = mutableMapOf<String, CallConnection>()
|
||||
private val knownCalls = mutableSetOf<String>()
|
||||
|
||||
private lateinit var notificationManager: NotificationManagerCompat
|
||||
private lateinit var notificationUtils: NotificationUtils
|
||||
private lateinit var callManager: WebRtcCallManager
|
||||
private lateinit var avatarRenderer: AvatarRenderer
|
||||
|
@ -74,6 +77,7 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
notificationManager = NotificationManagerCompat.from(this)
|
||||
notificationUtils = vectorComponent().notificationUtils()
|
||||
callManager = vectorComponent().webRtcCallManager()
|
||||
avatarRenderer = vectorComponent().avatarRenderer()
|
||||
|
@ -130,7 +134,6 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
callRingPlayerOutgoing?.stop()
|
||||
displayCallInProgressNotification(intent)
|
||||
}
|
||||
ACTION_NO_ACTIVE_CALL -> hideCallNotifications()
|
||||
ACTION_CALL_CONNECTING -> {
|
||||
// lower notification priority
|
||||
displayCallInProgressNotification(intent)
|
||||
|
@ -138,9 +141,8 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
callRingPlayerIncoming?.stop()
|
||||
callRingPlayerOutgoing?.stop()
|
||||
}
|
||||
ACTION_ONGOING_CALL_BG -> {
|
||||
// there is an ongoing call but call activity is in background
|
||||
displayCallOnGoingInBackground(intent)
|
||||
ACTION_CALL_TERMINATED -> {
|
||||
handleCallTerminated(intent)
|
||||
}
|
||||
else -> {
|
||||
// Should not happen
|
||||
|
@ -166,11 +168,15 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
Timber.v("## VOIP displayIncomingCallNotification $intent")
|
||||
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
||||
val call = callManager.getCallById(callId) ?: return
|
||||
if (knownCalls.contains(callId)) {
|
||||
Timber.v("Call already notified $callId$")
|
||||
return
|
||||
}
|
||||
val isVideoCall = call.mxCall.isVideoCall
|
||||
val fromBg = intent.getBooleanExtra(EXTRA_IS_IN_BG, false)
|
||||
val opponentMatrixItem = getOpponentMatrixItem(call)
|
||||
Timber.v("displayIncomingCallNotification : display the dedicated notification")
|
||||
val incomingCallAlert = IncomingCallAlert(INCOMING_CALL_ALERT_UID,
|
||||
val incomingCallAlert = IncomingCallAlert(callId,
|
||||
shouldBeDisplayedIn = { activity ->
|
||||
if (activity is RoomDetailActivity) {
|
||||
call.roomId != activity.currentRoomId
|
||||
|
@ -195,7 +201,27 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId,
|
||||
fromBg = fromBg
|
||||
)
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
if (knownCalls.isEmpty()) {
|
||||
startForeground(callId.hashCode(), notification)
|
||||
} else {
|
||||
notificationManager.notify(callId.hashCode(), notification)
|
||||
}
|
||||
knownCalls.add(callId)
|
||||
}
|
||||
|
||||
private fun handleCallTerminated(intent: Intent) {
|
||||
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
||||
if (!knownCalls.remove(callId)) {
|
||||
Timber.v("Call terminated for unknown call $callId$")
|
||||
return
|
||||
}
|
||||
val notification = notificationUtils.buildCallEndedNotification()
|
||||
notificationManager.notify(callId.hashCode(), notification)
|
||||
alertManager.cancelAlert(callId)
|
||||
if (knownCalls.isEmpty()) {
|
||||
mediaSession?.isActive = false
|
||||
myStopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCallScreen(call: WebRtcCall, mode: String) {
|
||||
|
@ -210,13 +236,22 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
private fun displayOutgoingRingingCallNotification(intent: Intent) {
|
||||
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: return
|
||||
val call = callManager.getCallById(callId) ?: return
|
||||
if (knownCalls.contains(callId)) {
|
||||
Timber.v("Call already notified $callId$")
|
||||
return
|
||||
}
|
||||
val opponentMatrixItem = getOpponentMatrixItem(call)
|
||||
Timber.v("displayOutgoingCallNotification : display the dedicated notification")
|
||||
val notification = notificationUtils.buildOutgoingRingingCallNotification(
|
||||
mxCall = call.mxCall,
|
||||
title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId
|
||||
)
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
if (knownCalls.isEmpty()) {
|
||||
startForeground(callId.hashCode(), notification)
|
||||
} else {
|
||||
notificationManager.notify(callId.hashCode(), notification)
|
||||
}
|
||||
knownCalls.add(callId)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -226,44 +261,17 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
Timber.v("## VOIP displayCallInProgressNotification")
|
||||
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: ""
|
||||
val call = callManager.getCallById(callId) ?: return
|
||||
if (!knownCalls.contains(callId)) {
|
||||
Timber.v("Call in progress for unknown call $callId$")
|
||||
return
|
||||
}
|
||||
val opponentMatrixItem = getOpponentMatrixItem(call)
|
||||
alertManager.cancelAlert(INCOMING_CALL_ALERT_UID)
|
||||
alertManager.cancelAlert(callId)
|
||||
val notification = notificationUtils.buildPendingCallNotification(
|
||||
mxCall = call.mxCall,
|
||||
title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId
|
||||
)
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
// mCallIdInProgress = callId
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a call in progress notification.
|
||||
*/
|
||||
private fun displayCallOnGoingInBackground(intent: Intent) {
|
||||
Timber.v("## VOIP displayCallInProgressNotification")
|
||||
val callId = intent.getStringExtra(EXTRA_CALL_ID) ?: return
|
||||
val call = callManager.getCallById(callId) ?: return
|
||||
val opponentMatrixItem = getOpponentMatrixItem(call)
|
||||
|
||||
val notification = notificationUtils.buildPendingCallNotification(
|
||||
mxCall = call.mxCall,
|
||||
title = opponentMatrixItem?.getBestName() ?: call.mxCall.opponentUserId,
|
||||
fromBg = true)
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
// mCallIdInProgress = callId
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the permanent call notifications
|
||||
*/
|
||||
private fun hideCallNotifications() {
|
||||
val notification = notificationUtils.buildCallEndedNotification()
|
||||
alertManager.cancelAlert(INCOMING_CALL_ALERT_UID)
|
||||
mediaSession?.isActive = false
|
||||
// It's mandatory to startForeground to avoid crash
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
|
||||
myStopSelf()
|
||||
notificationManager.notify(callId.hashCode(), notification)
|
||||
}
|
||||
|
||||
fun addConnection(callConnection: CallConnection) {
|
||||
|
@ -277,12 +285,11 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
companion object {
|
||||
private const val NOTIFICATION_ID = 6480
|
||||
|
||||
private const val INCOMING_CALL_ALERT_UID = "INCOMING_CALL_ALERT_UID"
|
||||
private const val ACTION_INCOMING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_INCOMING_RINGING_CALL"
|
||||
private const val ACTION_OUTGOING_RINGING_CALL = "im.vector.app.core.services.CallService.ACTION_OUTGOING_RINGING_CALL"
|
||||
private const val ACTION_CALL_CONNECTING = "im.vector.app.core.services.CallService.ACTION_CALL_CONNECTING"
|
||||
private const val ACTION_ONGOING_CALL = "im.vector.app.core.services.CallService.ACTION_ONGOING_CALL"
|
||||
private const val ACTION_ONGOING_CALL_BG = "im.vector.app.core.services.CallService.ACTION_ONGOING_CALL_BG"
|
||||
private const val ACTION_CALL_TERMINATED = "im.vector.app.core.services.CallService.ACTION_CALL_TERMINATED"
|
||||
private const val ACTION_NO_ACTIVE_CALL = "im.vector.app.core.services.CallService.NO_ACTIVE_CALL"
|
||||
// private const val ACTION_ACTIVITY_VISIBLE = "im.vector.app.core.services.CallService.ACTION_ACTIVITY_VISIBLE"
|
||||
// private const val ACTION_STOP_RINGING = "im.vector.app.core.services.CallService.ACTION_STOP_RINGING"
|
||||
|
@ -302,17 +309,6 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
|
||||
fun onOnGoingCallBackground(context: Context,
|
||||
callId: String) {
|
||||
val intent = Intent(context, CallService::class.java)
|
||||
.apply {
|
||||
action = ACTION_ONGOING_CALL_BG
|
||||
putExtra(EXTRA_CALL_ID, callId)
|
||||
}
|
||||
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
|
||||
fun onOutgoingCallRinging(context: Context,
|
||||
callId: String) {
|
||||
val intent = Intent(context, CallService::class.java)
|
||||
|
@ -335,10 +331,11 @@ class CallService : VectorService(), WiredHeadsetStateReceiver.HeadsetEventListe
|
|||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
|
||||
fun onNoActiveCall(context: Context) {
|
||||
fun onCallTerminated(context: Context, callId: String) {
|
||||
val intent = Intent(context, CallService::class.java)
|
||||
.apply {
|
||||
action = ACTION_NO_ACTIVE_CALL
|
||||
action = ACTION_CALL_TERMINATED
|
||||
putExtra(EXTRA_CALL_ID, callId)
|
||||
}
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
|
|
|
@ -20,9 +20,12 @@ import android.content.Context
|
|||
import android.util.AttributeSet
|
||||
import android.widget.RelativeLayout
|
||||
import im.vector.app.R
|
||||
import im.vector.app.databinding.ViewCurrentCallsBinding
|
||||
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||
import im.vector.app.features.themes.ThemeUtils
|
||||
import org.matrix.android.sdk.api.session.call.CallState
|
||||
|
||||
class ActiveCallView @JvmOverloads constructor(
|
||||
class CurrentCallsView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
|
@ -32,15 +35,32 @@ class ActiveCallView @JvmOverloads constructor(
|
|||
fun onTapToReturnToCall()
|
||||
}
|
||||
|
||||
val views: ViewCurrentCallsBinding
|
||||
var callback: Callback? = null
|
||||
|
||||
init {
|
||||
setupView()
|
||||
}
|
||||
|
||||
private fun setupView() {
|
||||
inflate(context, R.layout.view_active_call_view, this)
|
||||
inflate(context, R.layout.view_current_calls, this)
|
||||
views = ViewCurrentCallsBinding.bind(this)
|
||||
setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary))
|
||||
setOnClickListener { callback?.onTapToReturnToCall() }
|
||||
}
|
||||
|
||||
fun render(calls: List<WebRtcCall>, formattedDuration: String) {
|
||||
val connectedCalls = calls.filter {
|
||||
it.mxCall.state is CallState.Connected
|
||||
}
|
||||
val heldCalls = connectedCalls.filter {
|
||||
it.isLocalOnHold || it.remoteOnHold
|
||||
}
|
||||
if (connectedCalls.isEmpty()) return
|
||||
views.currentCallsInfo.text = if (connectedCalls.size == heldCalls.size) {
|
||||
resources.getQuantityString(R.plurals.call_only_paused, heldCalls.size, heldCalls.size)
|
||||
} else {
|
||||
if (heldCalls.isEmpty()) {
|
||||
resources.getString(R.string.call_only_active, formattedDuration)
|
||||
} else {
|
||||
resources.getQuantityString(R.plurals.call_one_active_and_other_paused, heldCalls.size, formattedDuration, heldCalls.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package im.vector.app.core.ui.views
|
||||
|
||||
import android.view.View
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.view.isVisible
|
||||
import im.vector.app.core.utils.DebouncedClickListener
|
||||
|
@ -26,33 +25,47 @@ import im.vector.app.features.call.webrtc.WebRtcCall
|
|||
import org.webrtc.RendererCommon
|
||||
import org.webrtc.SurfaceViewRenderer
|
||||
|
||||
class ActiveCallViewHolder {
|
||||
class KnownCallsViewHolder {
|
||||
|
||||
private var activeCallPiP: SurfaceViewRenderer? = null
|
||||
private var activeCallView: ActiveCallView? = null
|
||||
private var currentCallsView: CurrentCallsView? = null
|
||||
private var pipWrapper: CardView? = null
|
||||
private var activeCall: WebRtcCall? = null
|
||||
private var currentCall: WebRtcCall? = null
|
||||
private var calls: List<WebRtcCall> = emptyList()
|
||||
|
||||
private var activeCallPipInitialized = false
|
||||
|
||||
fun updateCall(activeCall: WebRtcCall?) {
|
||||
this.activeCall = activeCall
|
||||
val hasActiveCall = activeCall?.mxCall?.state is CallState.Connected
|
||||
private val tickListener = object : WebRtcCall.Listener {
|
||||
override fun onTick(formattedDuration: String) {
|
||||
currentCallsView?.render(calls, formattedDuration)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCall(currentCall: WebRtcCall?, calls: List<WebRtcCall>) {
|
||||
activeCallPiP?.let {
|
||||
this.currentCall?.detachRenderers(listOf(it))
|
||||
}
|
||||
this.currentCall?.removeListener(tickListener)
|
||||
this.currentCall = currentCall
|
||||
this.currentCall?.addListener(tickListener)
|
||||
this.calls = calls
|
||||
val hasActiveCall = currentCall?.mxCall?.state is CallState.Connected
|
||||
if (hasActiveCall) {
|
||||
val isVideoCall = activeCall?.mxCall?.isVideoCall == true
|
||||
val isVideoCall = currentCall?.mxCall?.isVideoCall == true
|
||||
if (isVideoCall) initIfNeeded()
|
||||
activeCallView?.isVisible = !isVideoCall
|
||||
currentCallsView?.isVisible = !isVideoCall
|
||||
currentCallsView?.render(calls, currentCall?.formattedDuration() ?: "")
|
||||
pipWrapper?.isVisible = isVideoCall
|
||||
activeCallPiP?.isVisible = isVideoCall
|
||||
activeCallPiP?.let {
|
||||
activeCall?.attachViewRenderers(null, it, null)
|
||||
currentCall?.attachViewRenderers(null, it, null)
|
||||
}
|
||||
} else {
|
||||
activeCallView?.isVisible = false
|
||||
currentCallsView?.isVisible = false
|
||||
activeCallPiP?.isVisible = false
|
||||
pipWrapper?.isVisible = false
|
||||
activeCallPiP?.let {
|
||||
activeCall?.detachRenderers(listOf(it))
|
||||
currentCall?.detachRenderers(listOf(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,30 +83,31 @@ class ActiveCallViewHolder {
|
|||
}
|
||||
}
|
||||
|
||||
fun bind(activeCallPiP: SurfaceViewRenderer, activeCallView: ActiveCallView, pipWrapper: CardView, interactionListener: ActiveCallView.Callback) {
|
||||
fun bind(activeCallPiP: SurfaceViewRenderer, activeCallView: CurrentCallsView, pipWrapper: CardView, interactionListener: CurrentCallsView.Callback) {
|
||||
this.activeCallPiP = activeCallPiP
|
||||
this.activeCallView = activeCallView
|
||||
this.currentCallsView = activeCallView
|
||||
this.pipWrapper = pipWrapper
|
||||
|
||||
this.activeCallView?.callback = interactionListener
|
||||
this.currentCallsView?.callback = interactionListener
|
||||
pipWrapper.setOnClickListener(
|
||||
DebouncedClickListener(View.OnClickListener { _ ->
|
||||
DebouncedClickListener({ _ ->
|
||||
interactionListener.onTapToReturnToCall()
|
||||
})
|
||||
)
|
||||
this.currentCall?.addListener(tickListener)
|
||||
}
|
||||
|
||||
fun unBind() {
|
||||
activeCallPiP?.let {
|
||||
activeCall?.detachRenderers(listOf(it))
|
||||
currentCall?.detachRenderers(listOf(it))
|
||||
}
|
||||
if (activeCallPipInitialized) {
|
||||
activeCallPiP?.release()
|
||||
}
|
||||
this.activeCallView?.callback = null
|
||||
this.currentCallsView?.callback = null
|
||||
this.currentCall?.removeListener(tickListener)
|
||||
pipWrapper?.setOnClickListener(null)
|
||||
activeCallPiP = null
|
||||
activeCallView = null
|
||||
currentCallsView = null
|
||||
pipWrapper = null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright 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.core.utils
|
||||
|
||||
import io.reactivex.Observable
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
|
||||
class CountUpTimer(private val intervalInMs: Long) {
|
||||
|
||||
private val elapsedTime: AtomicLong = AtomicLong()
|
||||
private val resumed: AtomicBoolean = AtomicBoolean(false)
|
||||
|
||||
private val disposable = Observable.interval(intervalInMs, TimeUnit.MILLISECONDS)
|
||||
.filter { _ -> resumed.get() }
|
||||
.doOnNext { _ -> elapsedTime.addAndGet(intervalInMs) }
|
||||
.subscribe {
|
||||
tickListener?.onTick(elapsedTime.get())
|
||||
}
|
||||
|
||||
var tickListener: TickListener? = null
|
||||
|
||||
fun elapsedTime(): Long {
|
||||
return elapsedTime.get()
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
resumed.set(false)
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
resumed.set(true)
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
disposable.dispose()
|
||||
}
|
||||
|
||||
interface TickListener {
|
||||
fun onTick(milliseconds: Long)
|
||||
}
|
||||
}
|
|
@ -63,6 +63,11 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
|
|||
dismiss()
|
||||
}
|
||||
|
||||
views.callControlsTransfer.views.itemVerificationClickableZone.debouncedClicks {
|
||||
callViewModel.handle(VectorCallViewActions.InitiateCallTransfer)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
callViewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is VectorCallViewEvents.ShowSoundDeviceChooser -> {
|
||||
|
@ -153,5 +158,6 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
|
|||
views.callControlsToggleHoldResume.subTitle = null
|
||||
views.callControlsToggleHoldResume.leftIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_call_hold_action)
|
||||
}
|
||||
views.callControlsTransfer.isVisible = state.canOpponentBeTransferred
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,37 +23,51 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager
|
|||
import org.matrix.android.sdk.api.session.call.MxCall
|
||||
import javax.inject.Inject
|
||||
|
||||
class SharedActiveCallViewModel @Inject constructor(
|
||||
class SharedKnownCallsViewModel @Inject constructor(
|
||||
private val callManager: WebRtcCallManager
|
||||
) : ViewModel() {
|
||||
|
||||
val activeCall: MutableLiveData<WebRtcCall?> = MutableLiveData()
|
||||
val liveKnownCalls: MutableLiveData<List<WebRtcCall>> = MutableLiveData()
|
||||
|
||||
val callStateListener = object : WebRtcCall.Listener {
|
||||
val callListener = object : WebRtcCall.Listener {
|
||||
|
||||
override fun onStateUpdate(call: MxCall) {
|
||||
if (activeCall.value?.callId == call.callId) {
|
||||
activeCall.postValue(callManager.getCallById(call.callId))
|
||||
// post it-self
|
||||
liveKnownCalls.postValue(liveKnownCalls.value)
|
||||
}
|
||||
|
||||
override fun onHoldUnhold() {
|
||||
super.onHoldUnhold()
|
||||
// post it-self
|
||||
liveKnownCalls.postValue(liveKnownCalls.value)
|
||||
}
|
||||
}
|
||||
|
||||
private val currentCallListener = object : WebRtcCallManager.CurrentCallListener {
|
||||
override fun onCurrentCallChange(call: WebRtcCall?) {
|
||||
val knownCalls = callManager.getCalls()
|
||||
liveKnownCalls.postValue(knownCalls)
|
||||
knownCalls.forEach {
|
||||
it.removeListener(callListener)
|
||||
it.addListener(callListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val listener = object : WebRtcCallManager.CurrentCallListener {
|
||||
override fun onCurrentCallChange(call: WebRtcCall?) {
|
||||
activeCall.value?.mxCall?.removeListener(callStateListener)
|
||||
activeCall.postValue(call)
|
||||
call?.addListener(callStateListener)
|
||||
init {
|
||||
val knownCalls = callManager.getCalls()
|
||||
liveKnownCalls.postValue(knownCalls)
|
||||
callManager.addCurrentCallListener(currentCallListener)
|
||||
knownCalls.forEach {
|
||||
it.addListener(callListener)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
activeCall.postValue(callManager.currentCall)
|
||||
callManager.addCurrentCallListener(listener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
activeCall.value?.removeListener(callStateListener)
|
||||
callManager.removeCurrentCallListener(listener)
|
||||
callManager.getCalls().forEach {
|
||||
it.removeListener(callListener)
|
||||
}
|
||||
callManager.removeCurrentCallListener(currentCallListener)
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
|
@ -34,10 +34,10 @@ import androidx.core.view.isVisible
|
|||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.viewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.di.ScreenComponent
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.core.services.CallService
|
||||
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.allGranted
|
||||
|
@ -50,6 +50,7 @@ import im.vector.app.features.home.room.detail.RoomDetailActivity
|
|||
import im.vector.app.features.home.room.detail.RoomDetailArgs
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.call.CallState
|
||||
import org.matrix.android.sdk.api.session.call.MxCallDetail
|
||||
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
|
||||
|
@ -106,7 +107,6 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
|||
callArgs = intent.getParcelableExtra(MvRx.KEY_ARG)!!
|
||||
} else {
|
||||
Timber.e("## VOIP missing callArgs for VectorCall Activity")
|
||||
CallService.onNoActiveCall(this)
|
||||
finish()
|
||||
}
|
||||
|
||||
|
@ -153,8 +153,6 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
|||
private fun renderState(state: VectorCallViewState) {
|
||||
Timber.v("## VOIP renderState call $state")
|
||||
if (state.callState is Fail) {
|
||||
// be sure to clear notification
|
||||
CallService.onNoActiveCall(this)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
@ -167,7 +165,8 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
|||
views.smallIsHeldIcon.isVisible = false
|
||||
when (callState) {
|
||||
is CallState.Idle,
|
||||
is CallState.Dialing -> {
|
||||
is CallState.CreateOffer,
|
||||
is CallState.Dialing -> {
|
||||
views.callVideoGroup.isInvisible = true
|
||||
views.callInfoGroup.isVisible = true
|
||||
views.callStatusText.setText(R.string.call_ring)
|
||||
|
@ -190,7 +189,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
|||
}
|
||||
is CallState.Connected -> {
|
||||
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
|
||||
if (state.isLocalOnHold) {
|
||||
if (state.isLocalOnHold || state.isRemoteOnHold) {
|
||||
views.smallIsHeldIcon.isVisible = true
|
||||
views.callVideoGroup.isInvisible = true
|
||||
views.callInfoGroup.isVisible = true
|
||||
|
@ -202,16 +201,17 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
|||
views.callStatusText.setText(R.string.call_held_by_you)
|
||||
} else {
|
||||
views.callActionText.isInvisible = true
|
||||
state.otherUserMatrixItem.invoke()?.let {
|
||||
state.callInfo.otherUserItem?.let {
|
||||
views.callStatusText.text = getString(R.string.call_held_by_user, it.getBestName())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
views.callStatusText.text = null
|
||||
views.callStatusText.text = state.formattedDuration
|
||||
if (callArgs.isVideoCall) {
|
||||
views.callVideoGroup.isVisible = true
|
||||
views.callInfoGroup.isVisible = false
|
||||
//views.pip_video_view.isVisible = !state.isVideoCaptureInError
|
||||
views.pipRenderer.isVisible = !state.isVideoCaptureInError && state.otherKnownCallInfo == null
|
||||
configureCallInfo(state)
|
||||
} else {
|
||||
views.callVideoGroup.isInvisible = true
|
||||
views.callInfoGroup.isVisible = true
|
||||
|
@ -226,8 +226,6 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
|||
views.callStatusText.setText(R.string.call_connecting)
|
||||
views.callConnectingProgress.isVisible = true
|
||||
}
|
||||
// ensure all attached?
|
||||
callManager.getCallById(callArgs.callId)?.attachViewRenderers(views.pipRenderer, views.fullscreenRenderer, null)
|
||||
}
|
||||
is CallState.Terminated -> {
|
||||
finish()
|
||||
|
@ -238,7 +236,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
|||
}
|
||||
|
||||
private fun configureCallInfo(state: VectorCallViewState, blurAvatar: Boolean = false) {
|
||||
state.otherUserMatrixItem.invoke()?.let {
|
||||
state.callInfo.otherUserItem?.let {
|
||||
val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen)
|
||||
avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter)
|
||||
views.participantNameText.text = it.getBestName()
|
||||
|
@ -248,10 +246,32 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
|||
avatarRenderer.render(it, views.otherMemberAvatar)
|
||||
}
|
||||
}
|
||||
if (state.otherKnownCallInfo?.otherUserItem == null) {
|
||||
views.otherKnownCallLayout.isVisible = false
|
||||
} else {
|
||||
val otherCall = callManager.getCallById(state.otherKnownCallInfo.callId)
|
||||
val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen)
|
||||
avatarRenderer.renderBlur(
|
||||
matrixItem = state.otherKnownCallInfo.otherUserItem,
|
||||
imageView = views.otherKnownCallAvatarView,
|
||||
sampling = 20,
|
||||
rounded = false,
|
||||
colorFilter = colorFilter
|
||||
)
|
||||
views.otherKnownCallLayout.isVisible = true
|
||||
views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold || it.remoteOnHold }.orFalse()
|
||||
}
|
||||
}
|
||||
|
||||
private fun configureCallViews() {
|
||||
views.callControlsView.interactionListener = this
|
||||
views.otherKnownCallAvatarView.setOnClickListener {
|
||||
withState(callViewModel) {
|
||||
val otherCall = callManager.getCallById(it.otherKnownCallInfo?.callId ?: "") ?: return@withState
|
||||
startActivity(newIntent(this, otherCall.mxCall, null))
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||
|
@ -295,12 +315,14 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
|||
Timber.v("## VOIP handleViewEvents $event")
|
||||
when (event) {
|
||||
VectorCallViewEvents.DismissNoCall -> {
|
||||
CallService.onNoActiveCall(this)
|
||||
finish()
|
||||
}
|
||||
is VectorCallViewEvents.ConnectionTimeout -> {
|
||||
onErrorTimoutConnect(event.turn)
|
||||
}
|
||||
is VectorCallViewEvents.ShowCallTransferScreen -> {
|
||||
navigator.openCallTransfer(this, callArgs.callId)
|
||||
}
|
||||
null -> {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,4 +30,5 @@ sealed class VectorCallViewActions : VectorViewModelAction {
|
|||
object HeadSetButtonPressed : VectorCallViewActions()
|
||||
object ToggleCamera : VectorCallViewActions()
|
||||
object ToggleHDSD : VectorCallViewActions()
|
||||
object InitiateCallTransfer : VectorCallViewActions()
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ sealed class VectorCallViewEvents : VectorViewEvents {
|
|||
val available: List<CallAudioManager.SoundDevice>,
|
||||
val current: CallAudioManager.SoundDevice
|
||||
) : VectorCallViewEvents()
|
||||
object ShowCallTransferScreen: VectorCallViewEvents()
|
||||
// data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
|
||||
// data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents()
|
||||
// object CallAccepted : VectorCallViewEvents()
|
||||
|
|
|
@ -20,7 +20,6 @@ import com.airbnb.mvrx.Fail
|
|||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.Uninitialized
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
|
@ -34,6 +33,7 @@ import org.matrix.android.sdk.api.session.call.CallState
|
|||
import org.matrix.android.sdk.api.session.call.MxCall
|
||||
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
|
||||
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
||||
import org.matrix.android.sdk.api.session.room.model.call.supportCallTransfer
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import java.util.Timer
|
||||
|
@ -56,7 +56,7 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||
override fun onHoldUnhold() {
|
||||
setState {
|
||||
copy(
|
||||
isLocalOnHold = call?.isLocalOnHold() ?: false,
|
||||
isLocalOnHold = call?.isLocalOnHold ?: false,
|
||||
isRemoteOnHold = call?.remoteOnHold ?: false
|
||||
)
|
||||
}
|
||||
|
@ -80,6 +80,12 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onTick(formattedDuration: String) {
|
||||
setState {
|
||||
copy(formattedDuration = formattedDuration)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStateUpdate(call: MxCall) {
|
||||
val callState = call.state
|
||||
if (callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
|
||||
|
@ -109,7 +115,8 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||
}
|
||||
setState {
|
||||
copy(
|
||||
callState = Success(callState)
|
||||
callState = Success(callState),
|
||||
canOpponentBeTransferred = call.capabilities.supportCallTransfer()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -118,10 +125,10 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||
private val currentCallListener = object : WebRtcCallManager.CurrentCallListener {
|
||||
|
||||
override fun onCurrentCallChange(call: WebRtcCall?) {
|
||||
// we need to check the state
|
||||
if (call == null) {
|
||||
// we should dismiss, e.g handled by other session?
|
||||
_viewEvents.post(VectorCallViewEvents.DismissNoCall)
|
||||
} else {
|
||||
updateOtherKnownCall(call)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,6 +149,20 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateOtherKnownCall(currentCall: WebRtcCall) {
|
||||
val otherCall = callManager.getCalls().firstOrNull {
|
||||
it.callId != currentCall.callId && it.mxCall.state is CallState.Connected
|
||||
}
|
||||
setState {
|
||||
if (otherCall == null) {
|
||||
copy(otherKnownCallInfo = null)
|
||||
} else {
|
||||
val otherUserItem: MatrixItem? = session.getUser(otherCall.mxCall.opponentUserId)?.toMatrixItem()
|
||||
copy(otherKnownCallInfo = VectorCallViewState.CallInfo(otherCall.callId, otherUserItem))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
val webRtcCall = callManager.getCallById(initialState.callId)
|
||||
if (webRtcCall == null) {
|
||||
|
@ -161,14 +182,19 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||
copy(
|
||||
isVideoCall = webRtcCall.mxCall.isVideoCall,
|
||||
callState = Success(webRtcCall.mxCall.state),
|
||||
otherUserMatrixItem = item?.let { Success(it) } ?: Uninitialized,
|
||||
callInfo = VectorCallViewState.CallInfo(callId, item),
|
||||
soundDevice = currentSoundDevice,
|
||||
isLocalOnHold = webRtcCall.isLocalOnHold,
|
||||
isRemoteOnHold = webRtcCall.remoteOnHold,
|
||||
availableSoundDevices = callManager.callAudioManager.getAvailableSoundDevices(),
|
||||
isFrontCamera = call?.currentCameraType() == CameraType.FRONT,
|
||||
canSwitchCamera = call?.canSwitchCamera() ?: false,
|
||||
isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD
|
||||
isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT,
|
||||
canSwitchCamera = webRtcCall.canSwitchCamera(),
|
||||
formattedDuration = webRtcCall.formattedDuration(),
|
||||
isHD = webRtcCall.mxCall.isVideoCall && webRtcCall.currentCaptureFormat() is CaptureFormat.HD,
|
||||
canOpponentBeTransferred = webRtcCall.mxCall.capabilities.supportCallTransfer()
|
||||
)
|
||||
}
|
||||
updateOtherKnownCall(webRtcCall)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -246,6 +272,11 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||
if (!state.isVideoCall) return@withState
|
||||
call?.setCaptureFormat(if (state.isHD) CaptureFormat.SD else CaptureFormat.HD)
|
||||
}
|
||||
VectorCallViewActions.InitiateCallTransfer -> {
|
||||
_viewEvents.post(
|
||||
VectorCallViewEvents.ShowCallTransferScreen
|
||||
)
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
|
|
|
@ -36,10 +36,18 @@ data class VectorCallViewState(
|
|||
val canSwitchCamera: Boolean = true,
|
||||
val soundDevice: CallAudioManager.SoundDevice = CallAudioManager.SoundDevice.PHONE,
|
||||
val availableSoundDevices: List<CallAudioManager.SoundDevice> = emptyList(),
|
||||
val otherUserMatrixItem: Async<MatrixItem> = Uninitialized,
|
||||
val callState: Async<CallState> = Uninitialized
|
||||
val callState: Async<CallState> = Uninitialized,
|
||||
val otherKnownCallInfo: CallInfo? = null,
|
||||
val callInfo: CallInfo = CallInfo(callId),
|
||||
val formattedDuration: String = "",
|
||||
val canOpponentBeTransferred: Boolean = false
|
||||
) : MvRxState {
|
||||
|
||||
data class CallInfo(
|
||||
val callId: String,
|
||||
val otherUserItem: MatrixItem? = null
|
||||
)
|
||||
|
||||
constructor(callArgs: CallArgs): this(
|
||||
callId = callArgs.callId,
|
||||
roomId = callArgs.roomId,
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.call.transfer
|
||||
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
|
||||
sealed class CallTransferAction : VectorViewModelAction {
|
||||
data class Connect(val consultFirst: Boolean, val selectedUserId: String) : CallTransferAction()
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.call.transfer
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.widget.Toast
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.airbnb.mvrx.viewModel
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.di.ScreenComponent
|
||||
import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.core.extensions.addFragment
|
||||
import im.vector.app.core.extensions.addFragmentToBackstack
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.core.utils.PERMISSIONS_FOR_MEMBERS_SEARCH
|
||||
import im.vector.app.core.utils.PERMISSION_REQUEST_CODE_READ_CONTACTS
|
||||
import im.vector.app.core.utils.allGranted
|
||||
import im.vector.app.core.utils.checkPermissions
|
||||
import im.vector.app.databinding.ActivityCallTransferBinding
|
||||
import im.vector.app.features.contactsbook.ContactsBookFragment
|
||||
import im.vector.app.features.contactsbook.ContactsBookViewModel
|
||||
import im.vector.app.features.contactsbook.ContactsBookViewState
|
||||
import im.vector.app.features.userdirectory.UserListFragment
|
||||
import im.vector.app.features.userdirectory.UserListFragmentArgs
|
||||
import im.vector.app.features.userdirectory.UserListSharedAction
|
||||
import im.vector.app.features.userdirectory.UserListSharedActionViewModel
|
||||
import im.vector.app.features.userdirectory.UserListViewModel
|
||||
import im.vector.app.features.userdirectory.UserListViewState
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
@Parcelize
|
||||
data class CallTransferArgs(val callId: String) : Parcelable
|
||||
|
||||
private const val USER_LIST_FRAGMENT_TAG = "USER_LIST_FRAGMENT_TAG"
|
||||
|
||||
class CallTransferActivity : VectorBaseActivity<ActivityCallTransferBinding>(),
|
||||
CallTransferViewModel.Factory,
|
||||
UserListViewModel.Factory,
|
||||
ContactsBookViewModel.Factory {
|
||||
|
||||
private lateinit var sharedActionViewModel: UserListSharedActionViewModel
|
||||
@Inject lateinit var userListViewModelFactory: UserListViewModel.Factory
|
||||
@Inject lateinit var callTransferViewModelFactory: CallTransferViewModel.Factory
|
||||
@Inject lateinit var contactsBookViewModelFactory: ContactsBookViewModel.Factory
|
||||
@Inject lateinit var errorFormatter: ErrorFormatter
|
||||
|
||||
private val callTransferViewModel: CallTransferViewModel by viewModel()
|
||||
|
||||
override fun getBinding() = ActivityCallTransferBinding.inflate(layoutInflater)
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
super.injectWith(injector)
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override fun create(initialState: UserListViewState): UserListViewModel {
|
||||
return userListViewModelFactory.create(initialState)
|
||||
}
|
||||
|
||||
override fun create(initialState: CallTransferViewState): CallTransferViewModel {
|
||||
return callTransferViewModelFactory.create(initialState)
|
||||
}
|
||||
|
||||
override fun create(initialState: ContactsBookViewState): ContactsBookViewModel {
|
||||
return contactsBookViewModelFactory.create(initialState)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
waitingView = views.waitingView.waitingView
|
||||
sharedActionViewModel = viewModelProvider.get(UserListSharedActionViewModel::class.java)
|
||||
sharedActionViewModel
|
||||
.observe()
|
||||
.subscribe { sharedAction ->
|
||||
when (sharedAction) {
|
||||
UserListSharedAction.OpenPhoneBook -> openPhoneBook()
|
||||
// not exhaustive because it's a sharedAction
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
.disposeOnDestroy()
|
||||
if (isFirstCreation()) {
|
||||
addFragment(
|
||||
R.id.callTransferFragmentContainer,
|
||||
UserListFragment::class.java,
|
||||
UserListFragmentArgs(
|
||||
title = "",
|
||||
menuResId = -1,
|
||||
singleSelection = true,
|
||||
showInviteActions = false,
|
||||
showToolbar = false
|
||||
),
|
||||
USER_LIST_FRAGMENT_TAG
|
||||
)
|
||||
}
|
||||
callTransferViewModel.observeViewEvents {
|
||||
when (it) {
|
||||
is CallTransferViewEvents.Dismiss -> finish()
|
||||
CallTransferViewEvents.Loading -> showWaitingView()
|
||||
is CallTransferViewEvents.FailToTransfer -> showSnackbar(getString(R.string.call_transfer_failure))
|
||||
}
|
||||
}
|
||||
configureToolbar(views.callTransferToolbar)
|
||||
views.callTransferToolbar.title = getString(R.string.call_transfer_title)
|
||||
setupConnectAction()
|
||||
}
|
||||
|
||||
private fun setupConnectAction() {
|
||||
views.callTransferConnectAction.debouncedClicks {
|
||||
val userListFragment = supportFragmentManager.findFragmentByTag(USER_LIST_FRAGMENT_TAG) as? UserListFragment
|
||||
val selectedUser = userListFragment?.getCurrentState()?.getSelectedMatrixId()?.firstOrNull()
|
||||
if (selectedUser != null) {
|
||||
val action = CallTransferAction.Connect(views.callTransferConsultCheckBox.isChecked, selectedUser)
|
||||
callTransferViewModel.handle(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openPhoneBook() {
|
||||
// Check permission first
|
||||
if (checkPermissions(PERMISSIONS_FOR_MEMBERS_SEARCH,
|
||||
this,
|
||||
PERMISSION_REQUEST_CODE_READ_CONTACTS,
|
||||
0)) {
|
||||
addFragmentToBackstack(R.id.callTransferFragmentContainer, ContactsBookFragment::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (allGranted(grantResults)) {
|
||||
if (requestCode == PERMISSION_REQUEST_CODE_READ_CONTACTS) {
|
||||
doOnPostResume { addFragmentToBackstack(R.id.callTransferFragmentContainer, ContactsBookFragment::class.java) }
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(baseContext, R.string.missing_permissions_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun newIntent(context: Context, callId: String): Intent {
|
||||
return Intent(context, CallTransferActivity::class.java).also {
|
||||
it.putExtra(MvRx.KEY_ARG, CallTransferArgs(callId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.call.transfer
|
||||
|
||||
import im.vector.app.core.platform.VectorViewEvents
|
||||
|
||||
sealed class CallTransferViewEvents : VectorViewEvents {
|
||||
object Dismiss : CallTransferViewEvents()
|
||||
object Loading: CallTransferViewEvents()
|
||||
object FailToTransfer : CallTransferViewEvents()
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.call.transfer
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.airbnb.mvrx.ActivityViewModelContext
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.call.CallState
|
||||
import org.matrix.android.sdk.api.session.call.MxCall
|
||||
|
||||
class CallTransferViewModel @AssistedInject constructor(@Assisted initialState: CallTransferViewState,
|
||||
callManager: WebRtcCallManager)
|
||||
: VectorViewModel<CallTransferViewState, CallTransferAction, CallTransferViewEvents>(initialState) {
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: CallTransferViewState): CallTransferViewModel
|
||||
}
|
||||
|
||||
companion object : MvRxViewModelFactory<CallTransferViewModel, CallTransferViewState> {
|
||||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: CallTransferViewState): CallTransferViewModel? {
|
||||
val activity: CallTransferActivity = (viewModelContext as ActivityViewModelContext).activity()
|
||||
return activity.callTransferViewModelFactory.create(state)
|
||||
}
|
||||
}
|
||||
|
||||
private val call = callManager.getCallById(initialState.callId)
|
||||
private val callListener = object : WebRtcCall.Listener {
|
||||
override fun onStateUpdate(call: MxCall) {
|
||||
if (call.state == CallState.Terminated) {
|
||||
_viewEvents.post(CallTransferViewEvents.Dismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
if (call == null) {
|
||||
_viewEvents.post(CallTransferViewEvents.Dismiss)
|
||||
} else {
|
||||
call.addListener(callListener)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
call?.removeListener(callListener)
|
||||
}
|
||||
|
||||
override fun handle(action: CallTransferAction) {
|
||||
when (action) {
|
||||
is CallTransferAction.Connect -> transferCall(action)
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
private fun transferCall(action: CallTransferAction.Connect) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
_viewEvents.post(CallTransferViewEvents.Loading)
|
||||
call?.mxCall?.transfer(action.selectedUserId, null)
|
||||
_viewEvents.post(CallTransferViewEvents.Dismiss)
|
||||
} catch (failure: Throwable) {
|
||||
_viewEvents.post(CallTransferViewEvents.FailToTransfer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.call.transfer
|
||||
|
||||
import com.airbnb.mvrx.MvRxState
|
||||
|
||||
data class CallTransferViewState(
|
||||
val callId: String
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: CallTransferArgs) : this(callId = args.callId)
|
||||
}
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
|||
import android.hardware.camera2.CameraManager
|
||||
import androidx.core.content.getSystemService
|
||||
import im.vector.app.core.services.CallService
|
||||
import im.vector.app.core.utils.CountUpTimer
|
||||
import im.vector.app.features.call.CallAudioManager
|
||||
import im.vector.app.features.call.CameraEventsHandlerAdapter
|
||||
import im.vector.app.features.call.CameraProxy
|
||||
|
@ -45,6 +46,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
|
|||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.call.CallState
|
||||
import org.matrix.android.sdk.api.session.call.MxCall
|
||||
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
|
||||
import org.matrix.android.sdk.api.session.call.TurnServerResponse
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
|
||||
|
@ -53,6 +55,7 @@ import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
|
|||
import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent
|
||||
import org.matrix.android.sdk.api.session.room.model.call.SdpType
|
||||
import org.matrix.android.sdk.internal.util.awaitCallback
|
||||
import org.threeten.bp.Duration
|
||||
import org.webrtc.AudioSource
|
||||
import org.webrtc.AudioTrack
|
||||
import org.webrtc.Camera1Enumerator
|
||||
|
@ -72,6 +75,7 @@ import org.webrtc.VideoSource
|
|||
import org.webrtc.VideoTrack
|
||||
import timber.log.Timber
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Provider
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
@ -88,15 +92,18 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
private val dispatcher: CoroutineContext,
|
||||
private val sessionProvider: Provider<Session?>,
|
||||
private val peerConnectionFactoryProvider: Provider<PeerConnectionFactory?>,
|
||||
private val onCallBecomeActive: (WebRtcCall) -> Unit,
|
||||
private val onCallEnded: (WebRtcCall) -> Unit) : MxCall.StateListener {
|
||||
|
||||
interface Listener : MxCall.StateListener {
|
||||
fun onCaptureStateChanged() {}
|
||||
fun onCameraChanged() {}
|
||||
fun onHoldUnhold() {}
|
||||
fun onTick(formattedDuration: String) {}
|
||||
override fun onStateUpdate(call: MxCall) {}
|
||||
}
|
||||
|
||||
private val listeners = ArrayList<Listener>()
|
||||
private val listeners = CopyOnWriteArrayList<Listener>()
|
||||
|
||||
fun addListener(listener: Listener) {
|
||||
listeners.add(listener)
|
||||
|
@ -128,10 +135,29 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
private var currentCaptureFormat: CaptureFormat = CaptureFormat.HD
|
||||
private var cameraAvailabilityCallback: CameraManager.AvailabilityCallback? = null
|
||||
|
||||
private val timer = CountUpTimer(Duration.ofSeconds(1).toMillis()).apply {
|
||||
tickListener = object : CountUpTimer.TickListener {
|
||||
override fun onTick(milliseconds: Long) {
|
||||
val formattedDuration = formatDuration(Duration.ofMillis(milliseconds))
|
||||
listeners.forEach {
|
||||
tryOrNull { it.onTick(formattedDuration) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mute status
|
||||
var micMuted = false
|
||||
private set
|
||||
var videoMuted = false
|
||||
private set
|
||||
var remoteOnHold = false
|
||||
private set
|
||||
var isLocalOnHold = false
|
||||
private set
|
||||
|
||||
// This value is used to track localOnHold when changing remoteOnHold value
|
||||
private var wasLocalOnHold = false
|
||||
|
||||
var offerSdp: CallInviteContent.Offer? = null
|
||||
|
||||
|
@ -204,6 +230,12 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
}
|
||||
}
|
||||
|
||||
fun formattedDuration(): String {
|
||||
return formatDuration(
|
||||
Duration.ofMillis(timer.elapsedTime())
|
||||
)
|
||||
}
|
||||
|
||||
private fun createPeerConnection(turnServerResponse: TurnServerResponse?) {
|
||||
val peerConnectionFactory = peerConnectionFactoryProvider.get() ?: return
|
||||
val iceServers = mutableListOf<PeerConnection.IceServer>().apply {
|
||||
|
@ -229,21 +261,9 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
|
||||
fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
|
||||
Timber.v("## VOIP attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer")
|
||||
// this.localSurfaceRenderer = WeakReference(localViewRenderer)
|
||||
// this.remoteSurfaceRenderer = WeakReference(remoteViewRenderer)
|
||||
localSurfaceRenderers.addIfNeeded(localViewRenderer)
|
||||
remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer)
|
||||
|
||||
// The call is going to resume from background, we can reduce notif
|
||||
mxCall
|
||||
.takeIf { it.state is CallState.Connected }
|
||||
?.let { mxCall ->
|
||||
// Start background service with notification
|
||||
CallService.onPendingCall(
|
||||
context = context,
|
||||
callId = mxCall.callId)
|
||||
}
|
||||
|
||||
GlobalScope.launch(dispatcher) {
|
||||
when (mode) {
|
||||
VectorCallActivity.INCOMING_ACCEPT -> {
|
||||
|
@ -275,7 +295,6 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
|
||||
fun detachRenderers(renderers: List<SurfaceViewRenderer>?) {
|
||||
Timber.v("## VOIP detachRenderers")
|
||||
// currentCall?.localMediaStream?.let { currentCall?.peerConnection?.removeStream(it) }
|
||||
if (renderers.isNullOrEmpty()) {
|
||||
// remove all sinks
|
||||
localSurfaceRenderers.forEach {
|
||||
|
@ -295,18 +314,6 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
remoteVideoTrack?.removeSink(it)
|
||||
}
|
||||
}
|
||||
if (remoteSurfaceRenderers.isEmpty()) {
|
||||
// The call is going to continue in background, so ensure notification is visible
|
||||
mxCall
|
||||
.takeIf { it.state is CallState.Connected }
|
||||
?.let { mxCall ->
|
||||
// Start background service with notification
|
||||
CallService.onOnGoingCallBackground(
|
||||
context = context,
|
||||
callId = mxCall.callId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setupOutgoingCall() = withContext(dispatcher) {
|
||||
|
@ -328,6 +335,9 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
}
|
||||
|
||||
private suspend fun internalAcceptIncomingCall() = withContext(dispatcher) {
|
||||
tryOrNull {
|
||||
onCallBecomeActive(this@WebRtcCall)
|
||||
}
|
||||
val turnServerResponse = getTurnServer()
|
||||
// Update service state
|
||||
withContext(Dispatchers.Main) {
|
||||
|
@ -526,7 +536,7 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
* rather than 'sendonly')
|
||||
* @returns true if the other party has put us on hold
|
||||
*/
|
||||
fun isLocalOnHold(): Boolean {
|
||||
private fun computeIsLocalOnHold(): Boolean {
|
||||
if (mxCall.state !is CallState.Connected) return false
|
||||
var callOnHold = true
|
||||
// We consider a call to be on hold only if *all* the tracks are on hold
|
||||
|
@ -540,17 +550,32 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
}
|
||||
|
||||
fun updateRemoteOnHold(onHold: Boolean) {
|
||||
if (remoteOnHold == onHold) return
|
||||
remoteOnHold = onHold
|
||||
val direction = if (onHold) {
|
||||
RtpTransceiver.RtpTransceiverDirection.INACTIVE
|
||||
} else {
|
||||
RtpTransceiver.RtpTransceiverDirection.SEND_RECV
|
||||
GlobalScope.launch(dispatcher) {
|
||||
if (remoteOnHold == onHold) return@launch
|
||||
val direction: RtpTransceiver.RtpTransceiverDirection
|
||||
if (onHold) {
|
||||
wasLocalOnHold = isLocalOnHold
|
||||
remoteOnHold = true
|
||||
isLocalOnHold = true
|
||||
direction = RtpTransceiver.RtpTransceiverDirection.INACTIVE
|
||||
timer.pause()
|
||||
} else {
|
||||
remoteOnHold = false
|
||||
isLocalOnHold = wasLocalOnHold
|
||||
onCallBecomeActive(this@WebRtcCall)
|
||||
direction = RtpTransceiver.RtpTransceiverDirection.SEND_RECV
|
||||
if (!isLocalOnHold) {
|
||||
timer.resume()
|
||||
}
|
||||
}
|
||||
for (transceiver in peerConnection?.transceivers ?: emptyList()) {
|
||||
transceiver.direction = direction
|
||||
}
|
||||
updateMuteStatus()
|
||||
listeners.forEach {
|
||||
tryOrNull { it.onHoldUnhold() }
|
||||
}
|
||||
}
|
||||
for (transceiver in peerConnection?.transceivers ?: emptyList()) {
|
||||
transceiver.direction = direction
|
||||
}
|
||||
updateMuteStatus()
|
||||
}
|
||||
|
||||
fun muteCall(muted: Boolean) {
|
||||
|
@ -628,7 +653,10 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
}
|
||||
|
||||
private fun release() {
|
||||
listeners.clear()
|
||||
mxCall.removeListener(this)
|
||||
timer.stop()
|
||||
timer.tickListener = null
|
||||
videoCapturer?.stopCapture()
|
||||
videoCapturer?.dispose()
|
||||
videoCapturer = null
|
||||
|
@ -683,7 +711,6 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
if (mxCall.state == CallState.Terminated) {
|
||||
return
|
||||
}
|
||||
mxCall.state = CallState.Terminated
|
||||
// Close tracks ASAP
|
||||
localVideoTrack?.setEnabled(false)
|
||||
localVideoTrack?.setEnabled(false)
|
||||
|
@ -691,14 +718,17 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
val cameraManager = context.getSystemService<CameraManager>()!!
|
||||
cameraManager.unregisterAvailabilityCallback(cameraAvailabilityCallback)
|
||||
}
|
||||
release()
|
||||
val wasRinging = mxCall.state is CallState.LocalRinging
|
||||
mxCall.state = CallState.Terminated
|
||||
GlobalScope.launch(dispatcher) {
|
||||
release()
|
||||
}
|
||||
onCallEnded(this)
|
||||
if (originatedByMe) {
|
||||
// send hang up event
|
||||
if (mxCall.state is CallState.Connected) {
|
||||
mxCall.hangUp(reason)
|
||||
} else {
|
||||
if (wasRinging) {
|
||||
mxCall.reject()
|
||||
} else {
|
||||
mxCall.hangUp(reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -758,7 +788,7 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
Timber.i("Ignoring colliding negotiate event because we're impolite")
|
||||
return@launch
|
||||
}
|
||||
val prevOnHold = isLocalOnHold()
|
||||
val prevOnHold = computeIsLocalOnHold()
|
||||
try {
|
||||
val sdp = SessionDescription(type.asWebRTC(), sdpText)
|
||||
peerConnection.awaitSetRemoteDescription(sdp)
|
||||
|
@ -770,8 +800,15 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
} catch (failure: Throwable) {
|
||||
Timber.e(failure, "Failed to complete negotiation")
|
||||
}
|
||||
val nowOnHold = isLocalOnHold()
|
||||
val nowOnHold = computeIsLocalOnHold()
|
||||
wasLocalOnHold = nowOnHold
|
||||
if (prevOnHold != nowOnHold) {
|
||||
isLocalOnHold = nowOnHold
|
||||
if (nowOnHold) {
|
||||
timer.pause()
|
||||
} else {
|
||||
timer.resume()
|
||||
}
|
||||
listeners.forEach {
|
||||
tryOrNull { it.onHoldUnhold() }
|
||||
}
|
||||
|
@ -779,9 +816,26 @@ class WebRtcCall(val mxCall: MxCall,
|
|||
}
|
||||
}
|
||||
|
||||
private fun formatDuration(duration: Duration): String {
|
||||
val hours = duration.seconds / 3600
|
||||
val minutes = (duration.seconds % 3600) / 60
|
||||
val seconds = duration.seconds % 60
|
||||
return if (hours > 0) {
|
||||
String.format("%d:%02d:%02d", hours, minutes, seconds)
|
||||
} else {
|
||||
String.format("%02d:%02d", minutes, seconds)
|
||||
}
|
||||
}
|
||||
|
||||
// MxCall.StateListener
|
||||
|
||||
override fun onStateUpdate(call: MxCall) {
|
||||
val state = call.state
|
||||
if (state is CallState.Connected && state.iceConnectionState == MxPeerConnectionState.CONNECTED) {
|
||||
timer.resume()
|
||||
} else {
|
||||
timer.pause()
|
||||
}
|
||||
listeners.forEach {
|
||||
tryOrNull { it.onStateUpdate(call) }
|
||||
}
|
||||
|
|
|
@ -45,7 +45,10 @@ import org.webrtc.DefaultVideoDecoderFactory
|
|||
import org.webrtc.DefaultVideoEncoderFactory
|
||||
import org.webrtc.PeerConnectionFactory
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
@ -63,11 +66,11 @@ class WebRtcCallManager @Inject constructor(
|
|||
get() = activeSessionDataSource.currentValue?.orNull()
|
||||
|
||||
interface CurrentCallListener {
|
||||
fun onCurrentCallChange(call: WebRtcCall?)
|
||||
fun onCurrentCallChange(call: WebRtcCall?) {}
|
||||
fun onAudioDevicesChange() {}
|
||||
}
|
||||
|
||||
private val currentCallsListeners = emptyList<CurrentCallListener>().toMutableList()
|
||||
private val currentCallsListeners = CopyOnWriteArrayList<CurrentCallListener>()
|
||||
fun addCurrentCallListener(listener: CurrentCallListener) {
|
||||
currentCallsListeners.add(listener)
|
||||
}
|
||||
|
@ -100,16 +103,20 @@ class WebRtcCallManager @Inject constructor(
|
|||
isInBackground = true
|
||||
}
|
||||
|
||||
var currentCall: WebRtcCall? = null
|
||||
set(value) {
|
||||
field = value
|
||||
currentCallsListeners.forEach {
|
||||
tryOrNull { it.onCurrentCallChange(value) }
|
||||
}
|
||||
/**
|
||||
* The current call is the call we interacted with whatever his state (connected,resumed, held...)
|
||||
* As soon as we interact with an other call, it replaces this one and put it on held if not already.
|
||||
*/
|
||||
var currentCall: AtomicReference<WebRtcCall?> = AtomicReference(null)
|
||||
private fun AtomicReference<WebRtcCall?>.setAndNotify(newValue: WebRtcCall?) {
|
||||
set(newValue)
|
||||
currentCallsListeners.forEach {
|
||||
tryOrNull { it.onCurrentCallChange(newValue) }
|
||||
}
|
||||
}
|
||||
|
||||
private val callsByCallId = HashMap<String, WebRtcCall>()
|
||||
private val callsByRoomId = HashMap<String, ArrayList<WebRtcCall>>()
|
||||
private val callsByCallId = ConcurrentHashMap<String, WebRtcCall>()
|
||||
private val callsByRoomId = ConcurrentHashMap<String, MutableList<WebRtcCall>>()
|
||||
|
||||
fun getCallById(callId: String): WebRtcCall? {
|
||||
return callsByCallId[callId]
|
||||
|
@ -119,9 +126,17 @@ class WebRtcCallManager @Inject constructor(
|
|||
return callsByRoomId[roomId] ?: emptyList()
|
||||
}
|
||||
|
||||
fun getCurrentCall(): WebRtcCall? {
|
||||
return currentCall.get()
|
||||
}
|
||||
|
||||
fun getCalls(): List<WebRtcCall> {
|
||||
return callsByCallId.values.toList()
|
||||
}
|
||||
|
||||
fun headSetButtonTapped() {
|
||||
Timber.v("## VOIP headSetButtonTapped")
|
||||
val call = currentCall ?: return
|
||||
val call = getCurrentCall() ?: return
|
||||
if (call.mxCall.state is CallState.LocalRinging) {
|
||||
// accept call
|
||||
call.acceptIncomingCall()
|
||||
|
@ -161,16 +176,26 @@ class WebRtcCallManager @Inject constructor(
|
|||
.createPeerConnectionFactory()
|
||||
}
|
||||
|
||||
private fun onCallActive(call: WebRtcCall) {
|
||||
Timber.v("## VOIP WebRtcPeerConnectionManager onCall active: ${call.mxCall.callId}")
|
||||
val currentCall = getCurrentCall().takeIf { it != call }
|
||||
currentCall?.updateRemoteOnHold(onHold = true)
|
||||
this.currentCall.setAndNotify(call)
|
||||
}
|
||||
|
||||
private fun onCallEnded(call: WebRtcCall) {
|
||||
Timber.v("## VOIP WebRtcPeerConnectionManager onCall ended: ${call.mxCall.callId}")
|
||||
CallService.onNoActiveCall(context)
|
||||
CallService.onCallTerminated(context, call.callId)
|
||||
callAudioManager.stop()
|
||||
currentCall = null
|
||||
callsByCallId.remove(call.mxCall.callId)
|
||||
callsByRoomId[call.mxCall.roomId]?.remove(call)
|
||||
if (getCurrentCall() == call) {
|
||||
val otherCall = getCalls().lastOrNull()
|
||||
currentCall.setAndNotify(otherCall)
|
||||
}
|
||||
// This must be done in this thread
|
||||
executor.execute {
|
||||
if (currentCall == null) {
|
||||
if (getCurrentCall() == null) {
|
||||
Timber.v("## VOIP Dispose peerConnectionFactory as there is no need to keep one")
|
||||
peerConnectionFactory?.dispose()
|
||||
peerConnectionFactory = null
|
||||
|
@ -181,12 +206,22 @@ class WebRtcCallManager @Inject constructor(
|
|||
|
||||
fun startOutgoingCall(signalingRoomId: String, otherUserId: String, isVideoCall: Boolean) {
|
||||
Timber.v("## VOIP startOutgoingCall in room $signalingRoomId to $otherUserId isVideo $isVideoCall")
|
||||
if (getCallsByRoomId(signalingRoomId).isNotEmpty()) {
|
||||
Timber.w("## VOIP you already have a call in this room")
|
||||
return
|
||||
}
|
||||
if (getCurrentCall() != null && getCurrentCall()?.mxCall?.state !is CallState.Connected || getCalls().size >= 2) {
|
||||
Timber.w("## VOIP cannot start outgoing call")
|
||||
// Just ignore, maybe we could answer from other session?
|
||||
return
|
||||
}
|
||||
executor.execute {
|
||||
createPeerConnectionFactoryIfNeeded()
|
||||
}
|
||||
|
||||
getCurrentCall()?.updateRemoteOnHold(onHold = true)
|
||||
val mxCall = currentSession?.callSignalingService()?.createOutgoingCall(signalingRoomId, otherUserId, isVideoCall) ?: return
|
||||
createWebRtcCall(mxCall)
|
||||
val webRtcCall = createWebRtcCall(mxCall)
|
||||
currentCall.setAndNotify(webRtcCall)
|
||||
callAudioManager.startForCall(mxCall)
|
||||
|
||||
CallService.onOutgoingCallRinging(
|
||||
|
@ -199,10 +234,11 @@ class WebRtcCallManager @Inject constructor(
|
|||
|
||||
override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) {
|
||||
Timber.v("## VOIP onCallIceCandidateReceived for call ${mxCall.callId}")
|
||||
if (currentCall?.mxCall?.callId != mxCall.callId) return Unit.also {
|
||||
Timber.w("## VOIP ignore ice candidates from other call")
|
||||
}
|
||||
currentCall?.onCallIceCandidateReceived(iceCandidatesContent)
|
||||
val call = callsByCallId[iceCandidatesContent.callId]
|
||||
?: return Unit.also {
|
||||
Timber.w("onCallIceCandidateReceived for non active call? ${iceCandidatesContent.callId}")
|
||||
}
|
||||
call.onCallIceCandidateReceived(iceCandidatesContent)
|
||||
}
|
||||
|
||||
private fun createWebRtcCall(mxCall: MxCall): WebRtcCall {
|
||||
|
@ -217,26 +253,22 @@ class WebRtcCallManager @Inject constructor(
|
|||
peerConnectionFactory
|
||||
},
|
||||
sessionProvider = { currentSession },
|
||||
onCallBecomeActive = this::onCallActive,
|
||||
onCallEnded = this::onCallEnded
|
||||
)
|
||||
currentCall = webRtcCall
|
||||
callsByCallId[mxCall.callId] = webRtcCall
|
||||
callsByRoomId.getOrPut(mxCall.roomId) { ArrayList() }
|
||||
callsByRoomId.getOrPut(mxCall.roomId) { ArrayList(1) }
|
||||
.add(webRtcCall)
|
||||
return webRtcCall
|
||||
}
|
||||
|
||||
fun acceptIncomingCall() {
|
||||
currentCall?.acceptIncomingCall()
|
||||
}
|
||||
|
||||
fun endCall(originatedByMe: Boolean = true) {
|
||||
currentCall?.endCall(originatedByMe)
|
||||
fun endCallForRoom(roomId: String, originatedByMe: Boolean = true) {
|
||||
callsByRoomId[roomId]?.forEach { it.endCall(originatedByMe) }
|
||||
}
|
||||
|
||||
fun onWiredDeviceEvent(event: WiredHeadsetStateReceiver.HeadsetPlugEvent) {
|
||||
Timber.v("## VOIP onWiredDeviceEvent $event")
|
||||
currentCall ?: return
|
||||
getCurrentCall() ?: return
|
||||
// sometimes we received un-wanted unplugged...
|
||||
callAudioManager.wiredStateChange(event)
|
||||
}
|
||||
|
@ -248,8 +280,12 @@ class WebRtcCallManager @Inject constructor(
|
|||
|
||||
override fun onCallInviteReceived(mxCall: MxCall, callInviteContent: CallInviteContent) {
|
||||
Timber.v("## VOIP onCallInviteReceived callId ${mxCall.callId}")
|
||||
if (currentCall != null) {
|
||||
Timber.w("## VOIP receiving incoming call while already in call?")
|
||||
if (getCallsByRoomId(mxCall.roomId).isNotEmpty()) {
|
||||
Timber.w("## VOIP you already have a call in this room")
|
||||
return
|
||||
}
|
||||
if ((getCurrentCall() != null && getCurrentCall()?.mxCall?.state !is CallState.Connected) || getCalls().size >= 2) {
|
||||
Timber.w("## VOIP receiving incoming call but cannot handle it")
|
||||
// Just ignore, maybe we could answer from other session?
|
||||
return
|
||||
}
|
||||
|
@ -329,12 +365,12 @@ class WebRtcCallManager @Inject constructor(
|
|||
|
||||
override fun onCallManagedByOtherSession(callId: String) {
|
||||
Timber.v("## VOIP onCallManagedByOtherSession: $callId")
|
||||
currentCall = null
|
||||
val webRtcCall = callsByCallId.remove(callId)
|
||||
if (webRtcCall != null) {
|
||||
callsByRoomId[webRtcCall.mxCall.roomId]?.remove(webRtcCall)
|
||||
}
|
||||
CallService.onNoActiveCall(context)
|
||||
// TODO: handle this properly
|
||||
CallService.onCallTerminated(context, callId)
|
||||
|
||||
// did we start background sync? so we should stop it
|
||||
if (isInBackground) {
|
||||
|
|
|
@ -32,7 +32,7 @@ import im.vector.app.core.extensions.configureWith
|
|||
import im.vector.app.core.extensions.hideKeyboard
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.databinding.FragmentContactsBookBinding
|
||||
import im.vector.app.features.userdirectory.PendingInvitee
|
||||
import im.vector.app.features.userdirectory.PendingSelection
|
||||
import im.vector.app.features.userdirectory.UserListAction
|
||||
import im.vector.app.features.userdirectory.UserListSharedAction
|
||||
import im.vector.app.features.userdirectory.UserListSharedActionViewModel
|
||||
|
@ -44,9 +44,9 @@ import java.util.concurrent.TimeUnit
|
|||
import javax.inject.Inject
|
||||
|
||||
class ContactsBookFragment @Inject constructor(
|
||||
val contactsBookViewModelFactory: ContactsBookViewModel.Factory,
|
||||
private val contactsBookViewModelFactory: ContactsBookViewModel.Factory,
|
||||
private val contactsBookController: ContactsBookController
|
||||
) : VectorBaseFragment<FragmentContactsBookBinding>(), ContactsBookController.Callback {
|
||||
) : VectorBaseFragment<FragmentContactsBookBinding>(), ContactsBookController.Callback, ContactsBookViewModel.Factory {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentContactsBookBinding {
|
||||
return FragmentContactsBookBinding.inflate(inflater, container, false)
|
||||
|
@ -59,6 +59,10 @@ class ContactsBookFragment @Inject constructor(
|
|||
|
||||
private lateinit var sharedActionViewModel: UserListSharedActionViewModel
|
||||
|
||||
override fun create(initialState: ContactsBookViewState): ContactsBookViewModel {
|
||||
return contactsBookViewModelFactory.create(initialState)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java)
|
||||
|
@ -128,13 +132,13 @@ class ContactsBookFragment @Inject constructor(
|
|||
|
||||
override fun onMatrixIdClick(matrixId: String) {
|
||||
view?.hideKeyboard()
|
||||
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId))))
|
||||
viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.UserPendingSelection(User(matrixId))))
|
||||
sharedActionViewModel.post(UserListSharedAction.GoBack)
|
||||
}
|
||||
|
||||
override fun onThreePidClick(threePid: ThreePid) {
|
||||
view?.hideKeyboard()
|
||||
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid)))
|
||||
viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.ThreePidPendingSelection(threePid)))
|
||||
sharedActionViewModel.post(UserListSharedAction.GoBack)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
|
||||
package im.vector.app.features.contactsbook
|
||||
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.airbnb.mvrx.ActivityViewModelContext
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
|
@ -31,8 +30,6 @@ import im.vector.app.core.contacts.MappedContact
|
|||
import im.vector.app.core.extensions.exhaustive
|
||||
import im.vector.app.core.platform.EmptyViewEvents
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.features.createdirect.CreateDirectRoomActivity
|
||||
import im.vector.app.features.invite.InviteUsersToRoomActivity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.MatrixCallback
|
||||
|
@ -56,17 +53,11 @@ class ContactsBookViewModel @AssistedInject constructor(@Assisted
|
|||
companion object : MvRxViewModelFactory<ContactsBookViewModel, ContactsBookViewState> {
|
||||
|
||||
override fun create(viewModelContext: ViewModelContext, state: ContactsBookViewState): ContactsBookViewModel? {
|
||||
return when (viewModelContext) {
|
||||
is FragmentViewModelContext -> (viewModelContext.fragment() as ContactsBookFragment).contactsBookViewModelFactory.create(state)
|
||||
is ActivityViewModelContext -> {
|
||||
when (viewModelContext.activity<FragmentActivity>()) {
|
||||
is CreateDirectRoomActivity -> viewModelContext.activity<CreateDirectRoomActivity>().contactsBookViewModelFactory.create(state)
|
||||
is InviteUsersToRoomActivity -> viewModelContext.activity<InviteUsersToRoomActivity>().contactsBookViewModelFactory.create(state)
|
||||
else -> error("Wrong activity or fragment")
|
||||
}
|
||||
}
|
||||
else -> error("Wrong activity or fragment")
|
||||
val factory = when (viewModelContext) {
|
||||
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
|
||||
is ActivityViewModelContext -> viewModelContext.activity as? Factory
|
||||
}
|
||||
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
package im.vector.app.features.createdirect
|
||||
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
import im.vector.app.features.userdirectory.PendingInvitee
|
||||
import im.vector.app.features.userdirectory.PendingSelection
|
||||
|
||||
sealed class CreateDirectRoomAction : VectorViewModelAction {
|
||||
data class CreateRoomAndInviteSelectedUsers(
|
||||
val invitees: Set<PendingInvitee>,
|
||||
val selections: Set<PendingSelection>,
|
||||
val existingDmRoomId: String?
|
||||
) : CreateDirectRoomAction()
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ import im.vector.app.core.utils.checkPermissions
|
|||
import im.vector.app.core.utils.onPermissionDeniedSnackbar
|
||||
import im.vector.app.features.contactsbook.ContactsBookFragment
|
||||
import im.vector.app.features.contactsbook.ContactsBookViewModel
|
||||
import im.vector.app.features.contactsbook.ContactsBookViewState
|
||||
import im.vector.app.features.userdirectory.UserListFragment
|
||||
import im.vector.app.features.userdirectory.UserListFragmentArgs
|
||||
import im.vector.app.features.userdirectory.UserListSharedAction
|
||||
|
@ -57,7 +58,7 @@ import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
|
|||
import java.net.HttpURLConnection
|
||||
import javax.inject.Inject
|
||||
|
||||
class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory {
|
||||
class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory, CreateDirectRoomViewModel.Factory, ContactsBookViewModel.Factory {
|
||||
|
||||
private val viewModel: CreateDirectRoomViewModel by viewModel()
|
||||
private lateinit var sharedActionViewModel: UserListSharedActionViewModel
|
||||
|
@ -71,9 +72,11 @@ class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fac
|
|||
injector.inject(this)
|
||||
}
|
||||
|
||||
override fun create(initialState: UserListViewState): UserListViewModel {
|
||||
return userListViewModelFactory.create(initialState)
|
||||
}
|
||||
override fun create(initialState: UserListViewState) = userListViewModelFactory.create(initialState)
|
||||
|
||||
override fun create(initialState: CreateDirectRoomViewState) = createDirectRoomViewModelFactory.create(initialState)
|
||||
|
||||
override fun create(initialState: ContactsBookViewState) = contactsBookViewModelFactory.create(initialState)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -143,7 +146,7 @@ class CreateDirectRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fac
|
|||
private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) {
|
||||
if (action.itemId == R.id.action_create_direct_room) {
|
||||
viewModel.handle(CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(
|
||||
action.invitees,
|
||||
action.selections,
|
||||
null
|
||||
))
|
||||
}
|
||||
|
|
|
@ -29,8 +29,7 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
|
|||
import im.vector.app.core.utils.checkPermissions
|
||||
import im.vector.app.core.utils.registerForPermissionsResult
|
||||
import im.vector.app.databinding.FragmentQrCodeScannerBinding
|
||||
import im.vector.app.features.userdirectory.PendingInvitee
|
||||
|
||||
import im.vector.app.features.userdirectory.PendingSelection
|
||||
import me.dm7.barcodescanner.zxing.ZXingScannerView
|
||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
|
||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
|
||||
|
@ -107,7 +106,7 @@ class CreateDirectRoomByQrCodeFragment @Inject constructor() : VectorBaseFragmen
|
|||
val qrInvitee = if (viewModel.session.getUser(mxid) != null) viewModel.session.getUser(mxid)!! else User(mxid, null, null)
|
||||
|
||||
viewModel.handle(
|
||||
CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingInvitee.UserPendingInvitee(qrInvitee)), existingDm)
|
||||
CreateDirectRoomAction.CreateRoomAndInviteSelectedUsers(setOf(PendingSelection.UserPendingSelection(qrInvitee)), existingDm)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package im.vector.app.features.createdirect
|
|||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.airbnb.mvrx.ActivityViewModelContext
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
|
@ -27,7 +28,7 @@ import im.vector.app.core.extensions.exhaustive
|
|||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.features.raw.wellknown.getElementWellknown
|
||||
import im.vector.app.features.raw.wellknown.isE2EByDefault
|
||||
import im.vector.app.features.userdirectory.PendingInvitee
|
||||
import im.vector.app.features.userdirectory.PendingSelection
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.raw.RawService
|
||||
|
@ -50,8 +51,11 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
|
|||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: CreateDirectRoomViewState): CreateDirectRoomViewModel? {
|
||||
val activity: CreateDirectRoomActivity = (viewModelContext as ActivityViewModelContext).activity()
|
||||
return activity.createDirectRoomViewModelFactory.create(state)
|
||||
val factory = when (viewModelContext) {
|
||||
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
|
||||
is ActivityViewModelContext -> viewModelContext.activity as? Factory
|
||||
}
|
||||
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,11 +76,11 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
|
|||
}
|
||||
} else {
|
||||
// Create the DM
|
||||
createRoomAndInviteSelectedUsers(action.invitees)
|
||||
createRoomAndInviteSelectedUsers(action.selections)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createRoomAndInviteSelectedUsers(invitees: Set<PendingInvitee>) {
|
||||
private fun createRoomAndInviteSelectedUsers(selections: Set<PendingSelection>) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val adminE2EByDefault = rawService.getElementWellknown(session.myUserId)
|
||||
?.isE2EByDefault()
|
||||
|
@ -84,10 +88,10 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
|
|||
|
||||
val roomParams = CreateRoomParams()
|
||||
.apply {
|
||||
invitees.forEach {
|
||||
selections.forEach {
|
||||
when (it) {
|
||||
is PendingInvitee.UserPendingInvitee -> invitedUserIds.add(it.user.userId)
|
||||
is PendingInvitee.ThreePidPendingInvitee -> invite3pids.add(it.threePid)
|
||||
is PendingSelection.UserPendingSelection -> invitedUserIds.add(it.user.userId)
|
||||
is PendingSelection.ThreePidPendingSelection -> invite3pids.add(it.threePid)
|
||||
}.exhaustive
|
||||
}
|
||||
setDirectMessage()
|
||||
|
|
|
@ -21,7 +21,6 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.Observer
|
||||
import com.airbnb.mvrx.activityViewModel
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
|
@ -33,11 +32,11 @@ import im.vector.app.core.glide.GlideApp
|
|||
import im.vector.app.core.platform.ToolbarConfigurable
|
||||
import im.vector.app.core.platform.VectorBaseActivity
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.ui.views.ActiveCallView
|
||||
import im.vector.app.core.ui.views.ActiveCallViewHolder
|
||||
import im.vector.app.core.ui.views.CurrentCallsView
|
||||
import im.vector.app.core.ui.views.KnownCallsViewHolder
|
||||
import im.vector.app.core.ui.views.KeysBackupBanner
|
||||
import im.vector.app.databinding.FragmentHomeDetailBinding
|
||||
import im.vector.app.features.call.SharedActiveCallViewModel
|
||||
import im.vector.app.features.call.SharedKnownCallsViewModel
|
||||
import im.vector.app.features.call.VectorCallActivity
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.home.room.list.RoomListFragment
|
||||
|
@ -70,7 +69,7 @@ class HomeDetailFragment @Inject constructor(
|
|||
private val vectorPreferences: VectorPreferences
|
||||
) : VectorBaseFragment<FragmentHomeDetailBinding>(),
|
||||
KeysBackupBanner.Delegate,
|
||||
ActiveCallView.Callback,
|
||||
CurrentCallsView.Callback,
|
||||
ServerBackupStatusViewModel.Factory {
|
||||
|
||||
private val viewModel: HomeDetailViewModel by fragmentViewModel()
|
||||
|
@ -78,18 +77,18 @@ class HomeDetailFragment @Inject constructor(
|
|||
private val serverBackupStatusViewModel: ServerBackupStatusViewModel by activityViewModel()
|
||||
|
||||
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
|
||||
private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel
|
||||
private lateinit var sharedCallActionViewModel: SharedKnownCallsViewModel
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentHomeDetailBinding {
|
||||
return FragmentHomeDetailBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
private val activeCallViewHolder = ActiveCallViewHolder()
|
||||
private val activeCallViewHolder = KnownCallsViewHolder()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
sharedActionViewModel = activityViewModelProvider.get(HomeSharedActionViewModel::class.java)
|
||||
sharedCallActionViewModel = activityViewModelProvider.get(SharedActiveCallViewModel::class.java)
|
||||
sharedCallActionViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java)
|
||||
|
||||
setupBottomNavigationView()
|
||||
setupToolbar()
|
||||
|
@ -127,9 +126,9 @@ class HomeDetailFragment @Inject constructor(
|
|||
}
|
||||
|
||||
sharedCallActionViewModel
|
||||
.activeCall
|
||||
.observe(viewLifecycleOwner, Observer {
|
||||
activeCallViewHolder.updateCall(it)
|
||||
.liveKnownCalls
|
||||
.observe(viewLifecycleOwner, {
|
||||
activeCallViewHolder.updateCall(callManager.getCurrentCall(), callManager.getCalls())
|
||||
invalidateOptionsMenu()
|
||||
})
|
||||
}
|
||||
|
@ -336,7 +335,7 @@ class HomeDetailFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onTapToReturnToCall() {
|
||||
sharedCallActionViewModel.activeCall.value?.let { call ->
|
||||
callManager.getCurrentCall()?.let { call ->
|
||||
VectorCallActivity.newIntent(
|
||||
context = requireContext(),
|
||||
callId = call.callId,
|
||||
|
|
|
@ -89,8 +89,8 @@ import im.vector.app.core.intent.getFilenameFromUri
|
|||
import im.vector.app.core.intent.getMimeTypeFromUri
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.ui.views.ActiveCallView
|
||||
import im.vector.app.core.ui.views.ActiveCallViewHolder
|
||||
import im.vector.app.core.ui.views.CurrentCallsView
|
||||
import im.vector.app.core.ui.views.KnownCallsViewHolder
|
||||
import im.vector.app.core.ui.views.ActiveConferenceView
|
||||
import im.vector.app.core.ui.views.JumpToReadMarkerView
|
||||
import im.vector.app.core.ui.views.NotificationAreaView
|
||||
|
@ -120,7 +120,7 @@ import im.vector.app.features.attachments.ContactAttachment
|
|||
import im.vector.app.features.attachments.preview.AttachmentsPreviewActivity
|
||||
import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs
|
||||
import im.vector.app.features.attachments.toGroupedContentAttachmentData
|
||||
import im.vector.app.features.call.SharedActiveCallViewModel
|
||||
import im.vector.app.features.call.SharedKnownCallsViewModel
|
||||
import im.vector.app.features.call.VectorCallActivity
|
||||
import im.vector.app.features.call.conference.JitsiCallViewModel
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
|
@ -227,7 +227,8 @@ class RoomDetailFragment @Inject constructor(
|
|||
private val matrixItemColorProvider: MatrixItemColorProvider,
|
||||
private val imageContentRenderer: ImageContentRenderer,
|
||||
private val roomDetailPendingActionStore: RoomDetailPendingActionStore,
|
||||
private val pillsPostProcessorFactory: PillsPostProcessor.Factory
|
||||
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
|
||||
private val callManager: WebRtcCallManager
|
||||
) :
|
||||
VectorBaseFragment<FragmentRoomDetailBinding>(),
|
||||
TimelineEventController.Callback,
|
||||
|
@ -236,7 +237,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
AttachmentTypeSelectorView.Callback,
|
||||
AttachmentsHelper.Callback,
|
||||
GalleryOrCameraDialogHelper.Listener,
|
||||
ActiveCallView.Callback {
|
||||
CurrentCallsView.Callback {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
|
@ -282,7 +283,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
override fun getMenuRes() = R.menu.menu_timeline
|
||||
|
||||
private lateinit var sharedActionViewModel: MessageSharedActionViewModel
|
||||
private lateinit var sharedCallActionViewModel: SharedActiveCallViewModel
|
||||
private lateinit var knownCallsViewModel: SharedKnownCallsViewModel
|
||||
|
||||
private lateinit var layoutManager: LinearLayoutManager
|
||||
private lateinit var jumpToBottomViewVisibilityManager: JumpToBottomViewVisibilityManager
|
||||
|
@ -294,12 +295,12 @@ class RoomDetailFragment @Inject constructor(
|
|||
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
|
||||
|
||||
private var lockSendButton = false
|
||||
private val activeCallViewHolder = ActiveCallViewHolder()
|
||||
private val activeCallViewHolder = KnownCallsViewHolder()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
|
||||
sharedCallActionViewModel = activityViewModelProvider.get(SharedActiveCallViewModel::class.java)
|
||||
knownCallsViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java)
|
||||
attachmentsHelper = AttachmentsHelper(requireContext(), this).register()
|
||||
keyboardStateUtils = KeyboardStateUtils(requireActivity())
|
||||
setupToolbar(views.roomToolbar)
|
||||
|
@ -324,10 +325,10 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
sharedCallActionViewModel
|
||||
.activeCall
|
||||
knownCallsViewModel
|
||||
.liveKnownCalls
|
||||
.observe(viewLifecycleOwner, {
|
||||
activeCallViewHolder.updateCall(it)
|
||||
activeCallViewHolder.updateCall(callManager.getCurrentCall(), it)
|
||||
invalidateOptionsMenu()
|
||||
})
|
||||
|
||||
|
@ -799,18 +800,15 @@ class RoomDetailFragment @Inject constructor(
|
|||
showDialogWithMessage(getString(R.string.cannot_call_yourself))
|
||||
}
|
||||
}
|
||||
2 -> {
|
||||
val activeCall = sharedCallActionViewModel.activeCall.value
|
||||
if (activeCall != null) {
|
||||
2 -> {
|
||||
val currentCall = callManager.getCurrentCall()
|
||||
if (currentCall != null) {
|
||||
// resume existing if same room, if not prompt to kill and then restart new call?
|
||||
if (activeCall.roomId == roomDetailArgs.roomId) {
|
||||
if (currentCall.mxCall.roomId == roomDetailArgs.roomId) {
|
||||
onTapToReturnToCall()
|
||||
} else {
|
||||
safeStartCall(isVideoCall)
|
||||
}
|
||||
// else {
|
||||
// TODO might not work well, and should prompt
|
||||
// webRtcPeerConnectionManager.endCall()
|
||||
// safeStartCall(it, isVideoCall)
|
||||
// }
|
||||
} else if (!state.isAllowedToStartWebRTCCall) {
|
||||
showDialogWithMessage(getString(
|
||||
if (state.isDm()) {
|
||||
|
@ -2015,7 +2013,7 @@ class RoomDetailFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onTapToReturnToCall() {
|
||||
sharedCallActionViewModel.activeCall.value?.let { call ->
|
||||
callManager.getCurrentCall()?.let { call ->
|
||||
VectorCallActivity.newIntent(
|
||||
context = requireContext(),
|
||||
callId = call.callId,
|
||||
|
|
|
@ -38,7 +38,7 @@ import im.vector.app.features.command.ParsedCommand
|
|||
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
|
||||
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
|
||||
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineSettingsFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||
import im.vector.app.features.home.room.typing.TypingHelper
|
||||
|
@ -111,7 +111,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
private val rawService: RawService,
|
||||
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider,
|
||||
private val stickerPickerActionHandler: StickerPickerActionHandler,
|
||||
private val roomSummaryHolder: RoomSummaryHolder,
|
||||
private val roomSummariesHolder: RoomSummariesHolder,
|
||||
private val typingHelper: TypingHelper,
|
||||
private val callManager: WebRtcCallManager,
|
||||
private val chatEffectManager: ChatEffectManager,
|
||||
|
@ -349,7 +349,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun handleEndCall() {
|
||||
callManager.endCall()
|
||||
callManager.endCallForRoom(initialState.roomId)
|
||||
}
|
||||
|
||||
private fun handleSelectStickerAttachment() {
|
||||
|
@ -1323,6 +1323,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
}
|
||||
}
|
||||
.subscribe {
|
||||
Timber.v("Unread state: $it")
|
||||
setState { copy(unreadState = it) }
|
||||
}
|
||||
.disposeOnClear()
|
||||
|
@ -1375,7 +1376,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
|
||||
private fun observeSummaryState() {
|
||||
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
|
||||
roomSummaryHolder.set(summary)
|
||||
roomSummariesHolder.set(summary)
|
||||
setState {
|
||||
val typingMessage = typingHelper.getTypingMessage(summary.typingUsers)
|
||||
copy(typingMessage = typingMessage)
|
||||
|
@ -1420,7 +1421,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override fun onCleared() {
|
||||
roomSummaryHolder.clear()
|
||||
roomSummariesHolder.remove(room.roomId)
|
||||
timeline.dispose()
|
||||
timeline.removeAllListeners()
|
||||
if (vectorPreferences.sendTypingNotifs()) {
|
||||
|
|
|
@ -35,7 +35,6 @@ import im.vector.app.features.home.room.detail.RoomDetailAction
|
|||
import im.vector.app.features.home.room.detail.RoomDetailViewState
|
||||
import im.vector.app.features.home.room.detail.UnreadState
|
||||
import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.factory.NoticeItemFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
|
@ -77,7 +76,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
private val mergedHeaderItemFactory: MergedHeaderItemFactory,
|
||||
private val session: Session,
|
||||
private val callManager: WebRtcCallManager,
|
||||
private val noticeItemFactory: NoticeItemFactory,
|
||||
@TimelineEventControllerHandler
|
||||
private val backgroundHandler: Handler
|
||||
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
|
||||
|
@ -104,6 +102,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
// TODO move all callbacks to this?
|
||||
fun onTimelineItemAction(itemAction: RoomDetailAction)
|
||||
|
||||
// Introduce ViewModel scoped component (or Hilt?)
|
||||
fun getPreviewUrlRetriever(): PreviewUrlRetriever
|
||||
}
|
||||
|
||||
|
|
|
@ -196,7 +196,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
|
|||
EventType.CALL_CANDIDATES,
|
||||
EventType.CALL_HANGUP,
|
||||
EventType.CALL_ANSWER -> {
|
||||
noticeEventFormatter.format(timelineEvent, room?.roomSummary())
|
||||
noticeEventFormatter.format(timelineEvent)
|
||||
}
|
||||
else -> null
|
||||
} ?: ""
|
||||
|
|
|
@ -22,7 +22,7 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
|||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
|
||||
|
@ -43,7 +43,7 @@ class CallItemFactory @Inject constructor(
|
|||
private val messageInformationDataFactory: MessageInformationDataFactory,
|
||||
private val messageItemAttributesFactory: MessageItemAttributesFactory,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val roomSummaryHolder: RoomSummaryHolder,
|
||||
private val roomSummariesHolder: RoomSummariesHolder,
|
||||
private val callManager: WebRtcCallManager
|
||||
) {
|
||||
|
||||
|
@ -52,6 +52,7 @@ class CallItemFactory @Inject constructor(
|
|||
callback: TimelineEventController.Callback?
|
||||
): VectorEpoxyModel<*>? {
|
||||
if (event.root.eventId == null) return null
|
||||
val roomId = event.roomId
|
||||
val informationData = messageInformationDataFactory.create(event, null)
|
||||
val callSignalingContent = event.getCallSignallingContent() ?: return null
|
||||
val callId = callSignalingContent.callId ?: return null
|
||||
|
@ -64,6 +65,7 @@ class CallItemFactory @Inject constructor(
|
|||
return when (event.root.getClearType()) {
|
||||
EventType.CALL_ANSWER -> {
|
||||
createCallTileTimelineItem(
|
||||
roomId = roomId,
|
||||
callId = callId,
|
||||
callStatus = CallTileTimelineItem.CallStatus.IN_CALL,
|
||||
callKind = callKind,
|
||||
|
@ -75,6 +77,7 @@ class CallItemFactory @Inject constructor(
|
|||
}
|
||||
EventType.CALL_INVITE -> {
|
||||
createCallTileTimelineItem(
|
||||
roomId = roomId,
|
||||
callId = callId,
|
||||
callStatus = CallTileTimelineItem.CallStatus.INVITED,
|
||||
callKind = callKind,
|
||||
|
@ -86,6 +89,7 @@ class CallItemFactory @Inject constructor(
|
|||
}
|
||||
EventType.CALL_REJECT -> {
|
||||
createCallTileTimelineItem(
|
||||
roomId = roomId,
|
||||
callId = callId,
|
||||
callStatus = CallTileTimelineItem.CallStatus.REJECTED,
|
||||
callKind = callKind,
|
||||
|
@ -97,6 +101,7 @@ class CallItemFactory @Inject constructor(
|
|||
}
|
||||
EventType.CALL_HANGUP -> {
|
||||
createCallTileTimelineItem(
|
||||
roomId = roomId,
|
||||
callId = callId,
|
||||
callStatus = CallTileTimelineItem.CallStatus.ENDED,
|
||||
callKind = callKind,
|
||||
|
@ -121,6 +126,7 @@ class CallItemFactory @Inject constructor(
|
|||
}
|
||||
|
||||
private fun createCallTileTimelineItem(
|
||||
roomId: String,
|
||||
callId: String,
|
||||
callKind: CallTileTimelineItem.CallKind,
|
||||
callStatus: CallTileTimelineItem.CallStatus,
|
||||
|
@ -129,7 +135,7 @@ class CallItemFactory @Inject constructor(
|
|||
isStillActive: Boolean,
|
||||
callback: TimelineEventController.Callback?
|
||||
): CallTileTimelineItem? {
|
||||
val userOfInterest = roomSummaryHolder.roomSummary?.toMatrixItem() ?: return null
|
||||
val userOfInterest = roomSummariesHolder.get(roomId)?.toMatrixItem() ?: return null
|
||||
val attributes = messageItemAttributesFactory.create(null, informationData, callback).let {
|
||||
CallTileTimelineItem.Attributes(
|
||||
callId = callId,
|
||||
|
|
|
@ -22,7 +22,7 @@ import im.vector.app.features.home.AvatarRenderer
|
|||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.prevSameTypeEvents
|
||||
|
@ -47,7 +47,7 @@ import javax.inject.Inject
|
|||
class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val roomSummaryHolder: RoomSummaryHolder) {
|
||||
private val roomSummariesHolder: RoomSummariesHolder) {
|
||||
|
||||
private val collapsedEventIds = linkedSetOf<Long>()
|
||||
private val mergeItemCollapseStates = HashMap<Long, Boolean>()
|
||||
|
@ -77,7 +77,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
|||
}
|
||||
}
|
||||
|
||||
private fun isDirectRoom() = roomSummaryHolder.roomSummary?.isDirect.orFalse()
|
||||
private fun isDirectRoom(roomId: String) = roomSummariesHolder.get(roomId)?.isDirect.orFalse()
|
||||
|
||||
private fun buildMembershipEventsMergedSummary(currentPosition: Int,
|
||||
items: List<TimelineEvent>,
|
||||
|
@ -102,7 +102,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
|||
memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
|
||||
localId = mergedEvent.localId,
|
||||
eventId = mergedEvent.root.eventId ?: "",
|
||||
isDirectRoom = isDirectRoom()
|
||||
isDirectRoom = isDirectRoom(event.roomId)
|
||||
)
|
||||
mergedData.add(data)
|
||||
}
|
||||
|
@ -174,7 +174,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
|||
memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
|
||||
localId = mergedEvent.localId,
|
||||
eventId = mergedEvent.root.eventId ?: "",
|
||||
isDirectRoom = isDirectRoom()
|
||||
isDirectRoom = isDirectRoom(event.roomId)
|
||||
)
|
||||
mergedData.add(data)
|
||||
}
|
||||
|
@ -191,8 +191,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
|||
collapsedEventIds.removeAll(mergedEventIds)
|
||||
}
|
||||
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
|
||||
val powerLevelsHelper = roomSummaryHolder.roomSummary?.roomId
|
||||
?.let { activeSessionHolder.getSafeActiveSession()?.getRoom(it) }
|
||||
val powerLevelsHelper = activeSessionHolder.getSafeActiveSession()?.getRoom(event.roomId)
|
||||
?.let { it.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS, QueryStringValue.NoCondition)?.content?.toModel<PowerLevelsContent>() }
|
||||
?.let { PowerLevelsHelper(it) }
|
||||
val currentUserId = activeSessionHolder.getSafeActiveSession()?.myUserId ?: ""
|
||||
|
@ -209,7 +208,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
|||
readReceiptsCallback = callback,
|
||||
callback = callback,
|
||||
currentUserId = currentUserId,
|
||||
roomSummary = roomSummaryHolder.roomSummary,
|
||||
roomSummary = roomSummariesHolder.get(event.roomId),
|
||||
canChangeAvatar = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false,
|
||||
canChangeTopic = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false,
|
||||
canChangeName = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false
|
||||
|
|
|
@ -38,7 +38,6 @@ import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadSt
|
|||
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.MessageBlockCodeItem
|
||||
|
@ -105,15 +104,17 @@ class MessageItemFactory @Inject constructor(
|
|||
private val messageItemAttributesFactory: MessageItemAttributesFactory,
|
||||
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
|
||||
private val contentDownloadStateTrackerBinder: ContentDownloadStateTrackerBinder,
|
||||
private val roomSummaryHolder: RoomSummaryHolder,
|
||||
private val defaultItemFactory: DefaultItemFactory,
|
||||
private val noticeItemFactory: NoticeItemFactory,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
|
||||
private val session: Session) {
|
||||
|
||||
// TODO inject this properly?
|
||||
private var roomId: String = ""
|
||||
|
||||
private val pillsPostProcessor by lazy {
|
||||
pillsPostProcessorFactory.create(roomSummaryHolder.roomSummary?.roomId)
|
||||
pillsPostProcessorFactory.create(roomId)
|
||||
}
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
|
@ -122,8 +123,8 @@ class MessageItemFactory @Inject constructor(
|
|||
callback: TimelineEventController.Callback?
|
||||
): VectorEpoxyModel<*>? {
|
||||
event.root.eventId ?: return null
|
||||
roomId = event.roomId
|
||||
val informationData = messageInformationDataFactory.create(event, nextEvent)
|
||||
|
||||
if (event.root.isRedacted()) {
|
||||
// message is redacted
|
||||
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
|
||||
|
@ -139,7 +140,7 @@ class MessageItemFactory @Inject constructor(
|
|||
|| event.isEncrypted() && event.root.content.toModel<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
|
||||
) {
|
||||
// This is an edit event, we should display it when debugging as a notice event
|
||||
return noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
|
||||
return noticeItemFactory.create(event, highlight, callback)
|
||||
}
|
||||
val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback)
|
||||
|
||||
|
@ -155,7 +156,7 @@ class MessageItemFactory @Inject constructor(
|
|||
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes)
|
||||
is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
|
||||
is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, callback)
|
||||
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes)
|
||||
}
|
||||
}
|
||||
|
@ -229,14 +230,13 @@ class MessageItemFactory @Inject constructor(
|
|||
attributes: AbsMessageItem.Attributes): VerificationRequestItem? {
|
||||
// If this request is not sent by me or sent to me, we should ignore it in timeline
|
||||
val myUserId = session.myUserId
|
||||
val roomId = roomSummaryHolder.roomSummary?.roomId
|
||||
if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) {
|
||||
return null
|
||||
}
|
||||
|
||||
val otherUserId = if (informationData.sentByMe) messageContent.toUserId else informationData.senderId
|
||||
val otherUserName = if (informationData.sentByMe) {
|
||||
session.getRoomMember(messageContent.toUserId, roomId ?: "")?.displayName
|
||||
session.getRoomMember(messageContent.toUserId, roomId)?.displayName
|
||||
} else {
|
||||
informationData.memberName
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvide
|
|||
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.item.NoticeItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.NoticeItem_
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -35,9 +34,8 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
|
|||
|
||||
fun create(event: TimelineEvent,
|
||||
highlight: Boolean,
|
||||
roomSummary: RoomSummary?,
|
||||
callback: TimelineEventController.Callback?): NoticeItem? {
|
||||
val formattedText = eventFormatter.format(event, roomSummary) ?: return null
|
||||
val formattedText = eventFormatter.format(event) ?: return null
|
||||
val informationData = informationDataFactory.create(event, null)
|
||||
val attributes = NoticeItem.Attributes(
|
||||
avatarRenderer = avatarRenderer,
|
||||
|
|
|
@ -21,7 +21,6 @@ import im.vector.app.core.epoxy.VectorEpoxyModel
|
|||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.resources.UserPreferencesProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.item.RoomCreateItem_
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
|
@ -33,7 +32,6 @@ import javax.inject.Inject
|
|||
class RoomCreateItemFactory @Inject constructor(private val stringProvider: StringProvider,
|
||||
private val userPreferencesProvider: UserPreferencesProvider,
|
||||
private val session: Session,
|
||||
private val roomSummaryHolder: RoomSummaryHolder,
|
||||
private val noticeItemFactory: NoticeItemFactory) {
|
||||
|
||||
fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
|
||||
|
@ -54,7 +52,7 @@ class RoomCreateItemFactory @Inject constructor(private val stringProvider: Stri
|
|||
|
||||
private fun defaultRendering(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
|
||||
return if (userPreferencesProvider.shouldShowHiddenEvents()) {
|
||||
noticeItemFactory.create(event, false, roomSummaryHolder.roomSummary, callback)
|
||||
noticeItemFactory.create(event, false, callback)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import im.vector.app.core.epoxy.EmptyItem_
|
|||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.resources.UserPreferencesProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import timber.log.Timber
|
||||
|
@ -32,7 +32,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
private val defaultItemFactory: DefaultItemFactory,
|
||||
private val encryptionItemFactory: EncryptionItemFactory,
|
||||
private val roomCreateItemFactory: RoomCreateItemFactory,
|
||||
private val roomSummaryHolder: RoomSummaryHolder,
|
||||
private val roomSummariesHolder: RoomSummariesHolder,
|
||||
private val verificationConclusionItemFactory: VerificationItemFactory,
|
||||
private val callItemFactory: CallItemFactory,
|
||||
private val userPreferencesProvider: UserPreferencesProvider) {
|
||||
|
@ -63,7 +63,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
EventType.STATE_ROOM_WIDGET,
|
||||
EventType.STATE_ROOM_POWER_LEVELS,
|
||||
EventType.REACTION,
|
||||
EventType.REDACTION -> noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
|
||||
EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback)
|
||||
EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
|
||||
// State room create
|
||||
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
|
||||
|
@ -91,7 +91,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
// TODO These are not filtered out by timeline when encrypted
|
||||
// For now manually ignore
|
||||
if (userPreferencesProvider.shouldShowHiddenEvents()) {
|
||||
noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
|
||||
noticeItemFactory.create(event, highlight, callback)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
|||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummaryHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineItem_
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
|
@ -51,7 +50,6 @@ class VerificationItemFactory @Inject constructor(
|
|||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val noticeItemFactory: NoticeItemFactory,
|
||||
private val userPreferencesProvider: UserPreferencesProvider,
|
||||
private val roomSummaryHolder: RoomSummaryHolder,
|
||||
private val stringProvider: StringProvider,
|
||||
private val session: Session
|
||||
) {
|
||||
|
@ -153,7 +151,7 @@ class VerificationItemFactory @Inject constructor(
|
|||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?
|
||||
): VectorEpoxyModel<*>? {
|
||||
if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, roomSummaryHolder.roomSummary, callback)
|
||||
if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, callback)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import im.vector.app.core.resources.StringProvider
|
|||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageOptionsContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS
|
||||
|
@ -41,7 +40,7 @@ class DisplayableEventFormatter @Inject constructor(
|
|||
private val noticeEventFormatter: NoticeEventFormatter
|
||||
) {
|
||||
|
||||
fun format(timelineEvent: TimelineEvent, appendAuthor: Boolean, roomSummary: RoomSummary?): CharSequence {
|
||||
fun format(timelineEvent: TimelineEvent, appendAuthor: Boolean): CharSequence {
|
||||
if (timelineEvent.root.isRedacted()) {
|
||||
return noticeEventFormatter.formatRedactedEvent(timelineEvent.root)
|
||||
}
|
||||
|
@ -131,7 +130,7 @@ class DisplayableEventFormatter @Inject constructor(
|
|||
}
|
||||
else -> {
|
||||
return span {
|
||||
text = noticeEventFormatter.format(timelineEvent, roomSummary) ?: ""
|
||||
text = noticeEventFormatter.format(timelineEvent) ?: ""
|
||||
textStyle = "italic"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.format
|
|||
import im.vector.app.ActiveSessionDataSource
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import org.matrix.android.sdk.api.extensions.appendNl
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
|
@ -55,6 +56,7 @@ class NoticeEventFormatter @Inject constructor(
|
|||
private val activeSessionDataSource: ActiveSessionDataSource,
|
||||
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val roomSummariesHolder: RoomSummariesHolder,
|
||||
private val sp: StringProvider
|
||||
) {
|
||||
|
||||
|
@ -65,7 +67,8 @@ class NoticeEventFormatter @Inject constructor(
|
|||
|
||||
private fun RoomSummary?.isDm() = this?.isDirect.orFalse()
|
||||
|
||||
fun format(timelineEvent: TimelineEvent, rs: RoomSummary?): CharSequence? {
|
||||
fun format(timelineEvent: TimelineEvent): CharSequence? {
|
||||
val rs = roomSummariesHolder.get(timelineEvent.roomId)
|
||||
return when (val type = timelineEvent.root.getClearType()) {
|
||||
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
|
||||
EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root, rs)
|
||||
|
|
|
@ -45,7 +45,7 @@ import javax.inject.Inject
|
|||
* This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline
|
||||
*/
|
||||
class MessageInformationDataFactory @Inject constructor(private val session: Session,
|
||||
private val roomSummaryHolder: RoomSummaryHolder,
|
||||
private val roomSummariesHolder: RoomSummariesHolder,
|
||||
private val dateFormatter: VectorDateFormatter,
|
||||
private val vectorPreferences: VectorPreferences) {
|
||||
|
||||
|
@ -116,7 +116,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||
}
|
||||
|
||||
private fun getE2EDecoration(event: TimelineEvent): E2EDecoration {
|
||||
val roomSummary = roomSummaryHolder.roomSummary
|
||||
val roomSummary = roomSummariesHolder.get(event.roomId)
|
||||
return if (
|
||||
event.root.sendState == SendState.SYNCED
|
||||
&& roomSummary?.isEncrypted.orFalse()
|
||||
|
|
|
@ -16,25 +16,28 @@
|
|||
|
||||
package im.vector.app.features.home.room.detail.timeline.helper
|
||||
|
||||
import im.vector.app.core.di.ScreenScope
|
||||
import org.matrix.android.sdk.api.session.room.model.RoomSummary
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/*
|
||||
This holds an instance of the current room summary.
|
||||
You should use this in the context of the timeline.
|
||||
You can use this to share room summary instances within the app.
|
||||
You should probably use this only in the context of the timeline
|
||||
*/
|
||||
@ScreenScope
|
||||
class RoomSummaryHolder @Inject constructor() {
|
||||
@Singleton
|
||||
class RoomSummariesHolder @Inject constructor() {
|
||||
|
||||
var roomSummary: RoomSummary? = null
|
||||
private set
|
||||
private var roomSummaries = HashMap<String, RoomSummary>()
|
||||
|
||||
fun set(roomSummary: RoomSummary) {
|
||||
this.roomSummary = roomSummary
|
||||
roomSummaries[roomSummary.roomId] = roomSummary
|
||||
}
|
||||
|
||||
fun get(roomId: String) = roomSummaries[roomId]
|
||||
|
||||
fun remove(roomId: String) = roomSummaries.remove(roomId)
|
||||
|
||||
fun clear() {
|
||||
roomSummary = null
|
||||
roomSummaries.clear()
|
||||
}
|
||||
}
|
|
@ -86,7 +86,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
|
|||
var latestEventTime: CharSequence = ""
|
||||
val latestEvent = roomSummary.latestPreviewableEvent
|
||||
if (latestEvent != null) {
|
||||
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect.not(), roomSummary)
|
||||
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect.not())
|
||||
latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
|
||||
}
|
||||
val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers)
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
package im.vector.app.features.invite
|
||||
|
||||
import im.vector.app.core.platform.VectorViewModelAction
|
||||
import im.vector.app.features.userdirectory.PendingInvitee
|
||||
import im.vector.app.features.userdirectory.PendingSelection
|
||||
|
||||
sealed class InviteUsersToRoomAction : VectorViewModelAction {
|
||||
data class InviteSelectedUsers(val invitees: Set<PendingInvitee>) : InviteUsersToRoomAction()
|
||||
data class InviteSelectedUsers(val selections: Set<PendingSelection>) : InviteUsersToRoomAction()
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ import im.vector.app.core.utils.checkPermissions
|
|||
import im.vector.app.core.utils.toast
|
||||
import im.vector.app.features.contactsbook.ContactsBookFragment
|
||||
import im.vector.app.features.contactsbook.ContactsBookViewModel
|
||||
import im.vector.app.features.contactsbook.ContactsBookViewState
|
||||
import im.vector.app.features.userdirectory.UserListFragment
|
||||
import im.vector.app.features.userdirectory.UserListFragmentArgs
|
||||
import im.vector.app.features.userdirectory.UserListSharedAction
|
||||
|
@ -53,7 +54,7 @@ import javax.inject.Inject
|
|||
@Parcelize
|
||||
data class InviteUsersToRoomArgs(val roomId: String) : Parcelable
|
||||
|
||||
class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory {
|
||||
class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Factory, ContactsBookViewModel.Factory, InviteUsersToRoomViewModel.Factory {
|
||||
|
||||
private val viewModel: InviteUsersToRoomViewModel by viewModel()
|
||||
private lateinit var sharedActionViewModel: UserListSharedActionViewModel
|
||||
|
@ -67,9 +68,11 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa
|
|||
injector.inject(this)
|
||||
}
|
||||
|
||||
override fun create(initialState: UserListViewState): UserListViewModel {
|
||||
return userListViewModelFactory.create(initialState)
|
||||
}
|
||||
override fun create(initialState: UserListViewState) = userListViewModelFactory.create(initialState)
|
||||
|
||||
override fun create(initialState: ContactsBookViewState) = contactsBookViewModelFactory.create(initialState)
|
||||
|
||||
override fun create(initialState: InviteUsersToRoomViewState) = inviteUsersToRoomViewModelFactory.create(initialState)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -92,7 +95,6 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa
|
|||
}
|
||||
.disposeOnDestroy()
|
||||
if (isFirstCreation()) {
|
||||
val args: InviteUsersToRoomArgs? = intent.extras?.getParcelable(MvRx.KEY_ARG)
|
||||
addFragment(
|
||||
R.id.container,
|
||||
UserListFragment::class.java,
|
||||
|
@ -100,7 +102,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa
|
|||
title = getString(R.string.invite_users_to_room_title),
|
||||
menuResId = R.menu.vector_invite_users_to_room,
|
||||
excludedUserIds = viewModel.getUserIdsOfRoomMembers(),
|
||||
existingRoomId = args?.roomId
|
||||
showInviteActions = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -110,7 +112,7 @@ class InviteUsersToRoomActivity : SimpleFragmentActivity(), UserListViewModel.Fa
|
|||
|
||||
private fun onMenuItemSelected(action: UserListSharedAction.OnMenuItemSelected) {
|
||||
if (action.itemId == R.id.action_invite_users_to_room_invite) {
|
||||
viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.invitees))
|
||||
viewModel.handle(InviteUsersToRoomAction.InviteSelectedUsers(action.selections))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package im.vector.app.features.invite
|
||||
|
||||
import com.airbnb.mvrx.ActivityViewModelContext
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.MvRxViewModelFactory
|
||||
import com.airbnb.mvrx.ViewModelContext
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
|
@ -24,7 +25,7 @@ import com.squareup.inject.assisted.AssistedInject
|
|||
import im.vector.app.R
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.features.userdirectory.PendingInvitee
|
||||
import im.vector.app.features.userdirectory.PendingSelection
|
||||
import io.reactivex.Observable
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.rx.rx
|
||||
|
@ -46,37 +47,40 @@ class InviteUsersToRoomViewModel @AssistedInject constructor(@Assisted
|
|||
|
||||
@JvmStatic
|
||||
override fun create(viewModelContext: ViewModelContext, state: InviteUsersToRoomViewState): InviteUsersToRoomViewModel? {
|
||||
val activity: InviteUsersToRoomActivity = (viewModelContext as ActivityViewModelContext).activity()
|
||||
return activity.inviteUsersToRoomViewModelFactory.create(state)
|
||||
val factory = when (viewModelContext) {
|
||||
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
|
||||
is ActivityViewModelContext -> viewModelContext.activity as? Factory
|
||||
}
|
||||
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
|
||||
}
|
||||
}
|
||||
|
||||
override fun handle(action: InviteUsersToRoomAction) {
|
||||
when (action) {
|
||||
is InviteUsersToRoomAction.InviteSelectedUsers -> inviteUsersToRoom(action.invitees)
|
||||
is InviteUsersToRoomAction.InviteSelectedUsers -> inviteUsersToRoom(action.selections)
|
||||
}
|
||||
}
|
||||
|
||||
private fun inviteUsersToRoom(invitees: Set<PendingInvitee>) {
|
||||
private fun inviteUsersToRoom(selections: Set<PendingSelection>) {
|
||||
_viewEvents.post(InviteUsersToRoomViewEvents.Loading)
|
||||
|
||||
Observable.fromIterable(invitees).flatMapCompletable { user ->
|
||||
Observable.fromIterable(selections).flatMapCompletable { user ->
|
||||
when (user) {
|
||||
is PendingInvitee.UserPendingInvitee -> room.rx().invite(user.user.userId, null)
|
||||
is PendingInvitee.ThreePidPendingInvitee -> room.rx().invite3pid(user.threePid)
|
||||
is PendingSelection.UserPendingSelection -> room.rx().invite(user.user.userId, null)
|
||||
is PendingSelection.ThreePidPendingSelection -> room.rx().invite3pid(user.threePid)
|
||||
}
|
||||
}.subscribe(
|
||||
{
|
||||
val successMessage = when (invitees.size) {
|
||||
val successMessage = when (selections.size) {
|
||||
1 -> stringProvider.getString(R.string.invitation_sent_to_one_user,
|
||||
invitees.first().getBestName())
|
||||
selections.first().getBestName())
|
||||
2 -> stringProvider.getString(R.string.invitations_sent_to_two_users,
|
||||
invitees.first().getBestName(),
|
||||
invitees.last().getBestName())
|
||||
selections.first().getBestName(),
|
||||
selections.last().getBestName())
|
||||
else -> stringProvider.getQuantityString(R.plurals.invitations_sent_to_one_and_more_users,
|
||||
invitees.size - 1,
|
||||
invitees.first().getBestName(),
|
||||
invitees.size - 1)
|
||||
selections.size - 1,
|
||||
selections.first().getBestName(),
|
||||
selections.size - 1)
|
||||
}
|
||||
_viewEvents.post(InviteUsersToRoomViewEvents.Success(successMessage))
|
||||
},
|
||||
|
|
|
@ -34,6 +34,7 @@ import im.vector.app.core.platform.VectorBaseActivity
|
|||
import im.vector.app.core.utils.toast
|
||||
import im.vector.app.features.call.conference.JitsiCallViewModel
|
||||
import im.vector.app.features.call.conference.VectorJitsiActivity
|
||||
import im.vector.app.features.call.transfer.CallTransferActivity
|
||||
import im.vector.app.features.createdirect.CreateDirectRoomActivity
|
||||
import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity
|
||||
|
@ -344,6 +345,11 @@ class DefaultNavigator @Inject constructor(
|
|||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
override fun openCallTransfer(context: Context, callId: String) {
|
||||
val intent = CallTransferActivity.newIntent(context, callId)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
private fun startActivity(context: Context, intent: Intent, buildTask: Boolean) {
|
||||
if (buildTask) {
|
||||
val stackBuilder = TaskStackBuilder.create(context)
|
||||
|
|
|
@ -113,4 +113,6 @@ interface Navigator {
|
|||
options: ((MutableList<Pair<View, String>>) -> Unit)?)
|
||||
|
||||
fun openSearch(context: Context, roomId: String)
|
||||
|
||||
fun openCallTransfer(context: Context, callId: String)
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
|
|||
if (room == null) {
|
||||
Timber.e("## Unable to resolve room for eventId [$event]")
|
||||
// Ok room is not known in store, but we can still display something
|
||||
val body = displayableEventFormatter.format(event, false, null)
|
||||
val body = displayableEventFormatter.format(event, false)
|
||||
val roomName = stringProvider.getString(R.string.notification_unknown_room_name)
|
||||
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
|
||||
|
||||
|
@ -124,7 +124,7 @@ class NotifiableEventResolver @Inject constructor(private val stringProvider: St
|
|||
}
|
||||
}
|
||||
|
||||
val body = displayableEventFormatter.format(event, false, room.roomSummary()).toString()
|
||||
val body = displayableEventFormatter.format(event, false).toString()
|
||||
val roomName = room.roomSummary()?.displayName ?: ""
|
||||
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
|
||||
|
||||
|
|
|
@ -297,12 +297,6 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
.setLights(accentColor, 500, 500)
|
||||
.setOngoing(true)
|
||||
|
||||
// Compat: Display the incoming call notification on the lock screen
|
||||
builder.priority = NotificationCompat.PRIORITY_HIGH
|
||||
|
||||
//
|
||||
// val pendingIntent = stackBuilder.getPendingIntent(requestId, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
val contentIntent = VectorCallActivity.newIntent(
|
||||
context = context,
|
||||
mxCall = mxCall,
|
||||
|
@ -340,9 +334,11 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
answerCallPendingIntent
|
||||
)
|
||||
)
|
||||
|
||||
builder.setFullScreenIntent(contentPendingIntent, true)
|
||||
|
||||
if (fromBg) {
|
||||
// Compat: Display the incoming call notification on the lock screen
|
||||
builder.priority = NotificationCompat.PRIORITY_HIGH
|
||||
builder.setFullScreenIntent(contentPendingIntent, true)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
|
@ -393,9 +389,8 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
*/
|
||||
@SuppressLint("NewApi")
|
||||
fun buildPendingCallNotification(mxCall: MxCall,
|
||||
title: String,
|
||||
fromBg: Boolean = false): Notification {
|
||||
val builder = NotificationCompat.Builder(context, if (fromBg) CALL_NOTIFICATION_CHANNEL_ID else SILENT_NOTIFICATION_CHANNEL_ID)
|
||||
title: String): Notification {
|
||||
val builder = NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle(ensureTitleNotEmpty(title))
|
||||
.apply {
|
||||
if (mxCall.isVideoCall) {
|
||||
|
@ -407,11 +402,6 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
.setSmallIcon(R.drawable.incoming_call_notification_transparent)
|
||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
|
||||
if (fromBg) {
|
||||
builder.priority = NotificationCompat.PRIORITY_LOW
|
||||
builder.setOngoing(true)
|
||||
}
|
||||
|
||||
val rejectCallPendingIntent = buildRejectCallPendingIntent(mxCall.callId)
|
||||
|
||||
builder.addAction(
|
||||
|
@ -450,6 +440,7 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
fun buildCallEndedNotification(): Notification {
|
||||
return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle(stringProvider.getString(R.string.call_ended))
|
||||
.setTimeoutAfter(2000)
|
||||
.setSmallIcon(R.drawable.ic_material_call_end_grey)
|
||||
.setCategory(NotificationCompat.CATEGORY_CALL)
|
||||
.build()
|
||||
|
|
|
@ -21,6 +21,8 @@ import android.view.View
|
|||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.setLeftDrawable
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
|
@ -42,18 +44,24 @@ class IncomingCallAlert(uid: String,
|
|||
: VectorAlert.ViewBinder {
|
||||
|
||||
override fun bind(view: View) {
|
||||
val callKind = if (isVideoCall) {
|
||||
R.string.action_video_call
|
||||
val (callKindText, callKindIcon) = if (isVideoCall) {
|
||||
Pair(R.string.action_video_call, R.drawable.ic_call_video_small)
|
||||
} else {
|
||||
R.string.action_voice_call
|
||||
Pair(R.string.action_voice_call, R.drawable.ic_call_audio_small)
|
||||
}
|
||||
view.findViewById<TextView>(R.id.incomingCallKindView).apply {
|
||||
setText(callKindText)
|
||||
setLeftDrawable(callKindIcon)
|
||||
}
|
||||
view.findViewById<TextView>(R.id.incomingCallKindView).setText(callKind)
|
||||
view.findViewById<TextView>(R.id.incomingCallNameView).text = matrixItem?.getBestName()
|
||||
view.findViewById<ImageView>(R.id.incomingCallAvatar)?.let { imageView ->
|
||||
matrixItem?.let { avatarRenderer.render(it, imageView) }
|
||||
matrixItem?.let { avatarRenderer.render(it, imageView, GlideApp.with(view.context.applicationContext)) }
|
||||
}
|
||||
view.findViewById<ImageView>(R.id.incomingCallAcceptView).setOnClickListener {
|
||||
onAccept()
|
||||
view.findViewById<ImageView>(R.id.incomingCallAcceptView).apply {
|
||||
setOnClickListener {
|
||||
onAccept()
|
||||
}
|
||||
setImageResource(callKindIcon)
|
||||
}
|
||||
view.findViewById<ImageView>(R.id.incomingCallRejectView).setOnClickListener {
|
||||
onReject()
|
||||
|
|
|
@ -21,6 +21,7 @@ import android.view.View
|
|||
import android.widget.ImageView
|
||||
import androidx.annotation.DrawableRes
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.glide.GlideApp
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
|
@ -43,7 +44,7 @@ class VerificationVectorAlert(uid: String,
|
|||
|
||||
override fun bind(view: View) {
|
||||
view.findViewById<ImageView>(R.id.ivUserAvatar)?.let { imageView ->
|
||||
matrixItem?.let { avatarRenderer.render(it, imageView) }
|
||||
matrixItem?.let { avatarRenderer.render(it, imageView, GlideApp.with(view.context.applicationContext)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,14 +19,14 @@ package im.vector.app.features.userdirectory
|
|||
import org.matrix.android.sdk.api.session.identity.ThreePid
|
||||
import org.matrix.android.sdk.api.session.user.model.User
|
||||
|
||||
sealed class PendingInvitee {
|
||||
data class UserPendingInvitee(val user: User) : PendingInvitee()
|
||||
data class ThreePidPendingInvitee(val threePid: ThreePid) : PendingInvitee()
|
||||
sealed class PendingSelection {
|
||||
data class UserPendingSelection(val user: User) : PendingSelection()
|
||||
data class ThreePidPendingSelection(val threePid: ThreePid) : PendingSelection()
|
||||
|
||||
fun getBestName(): String {
|
||||
return when (this) {
|
||||
is UserPendingInvitee -> user.getBestName()
|
||||
is ThreePidPendingInvitee -> threePid.value
|
||||
is UserPendingSelection -> user.getBestName()
|
||||
is ThreePidPendingSelection -> threePid.value
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ import im.vector.app.core.platform.VectorViewModelAction
|
|||
sealed class UserListAction : VectorViewModelAction {
|
||||
data class SearchUsers(val value: String) : UserListAction()
|
||||
object ClearSearchUsers : UserListAction()
|
||||
data class SelectPendingInvitee(val pendingInvitee: PendingInvitee) : UserListAction()
|
||||
data class RemovePendingInvitee(val pendingInvitee: PendingInvitee) : UserListAction()
|
||||
data class AddPendingSelection(val pendingSelection: PendingSelection) : UserListAction()
|
||||
data class RemovePendingSelection(val pendingSelection: PendingSelection) : UserListAction()
|
||||
object ComputeMatrixToLinkForSharing : UserListAction()
|
||||
}
|
||||
|
|
|
@ -54,10 +54,7 @@ class UserListController @Inject constructor(private val session: Session,
|
|||
|
||||
// Build generic items
|
||||
if (currentState.searchTerm.isBlank()) {
|
||||
// For now we remove this option if in invite to existing room flow (and not create DM)
|
||||
if (currentState.pendingInvitees.isEmpty()
|
||||
// For now we remove this option if in invite to existing room flow (and not create DM)
|
||||
&& currentState.existingRoomId == null) {
|
||||
if (currentState.showInviteActions()) {
|
||||
actionItem {
|
||||
id(R.drawable.ic_share)
|
||||
title(stringProvider.getString(R.string.invite_friends))
|
||||
|
@ -75,9 +72,7 @@ class UserListController @Inject constructor(private val session: Session,
|
|||
callback?.onContactBookClick()
|
||||
})
|
||||
}
|
||||
if (currentState.pendingInvitees.isEmpty()
|
||||
// For now we remove this option if in invite to existing room flow (and not create DM)
|
||||
&& currentState.existingRoomId == null) {
|
||||
if (currentState.showInviteActions()) {
|
||||
actionItem {
|
||||
id(R.drawable.ic_qr_code_add)
|
||||
title(stringProvider.getString(R.string.qr_code))
|
||||
|
|
|
@ -67,18 +67,22 @@ class UserListFragment @Inject constructor(
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
sharedActionViewModel = activityViewModelProvider.get(UserListSharedActionViewModel::class.java)
|
||||
views.userListTitle.text = args.title
|
||||
vectorBaseActivity.setSupportActionBar(views.userListToolbar)
|
||||
|
||||
if (args.showToolbar) {
|
||||
views.userListTitle.text = args.title
|
||||
vectorBaseActivity.setSupportActionBar(views.userListToolbar)
|
||||
setupCloseView()
|
||||
views.userListToolbar.isVisible = true
|
||||
} else {
|
||||
views.userListToolbar.isVisible = false
|
||||
}
|
||||
setupRecyclerView()
|
||||
setupSearchView()
|
||||
setupCloseView()
|
||||
|
||||
homeServerCapabilitiesViewModel.subscribe {
|
||||
views.userListE2EbyDefaultDisabled.isVisible = !it.isE2EByDefault
|
||||
}
|
||||
|
||||
viewModel.selectSubscribe(this, UserListViewState::pendingInvitees) {
|
||||
viewModel.selectSubscribe(this, UserListViewState::pendingSelections) {
|
||||
renderSelectedUsers(it)
|
||||
}
|
||||
|
||||
|
@ -105,7 +109,7 @@ class UserListFragment @Inject constructor(
|
|||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
withState(viewModel) {
|
||||
val showMenuItem = it.pendingInvitees.isNotEmpty()
|
||||
val showMenuItem = it.pendingSelections.isNotEmpty()
|
||||
menu.forEach { menuItem ->
|
||||
menuItem.isVisible = showMenuItem
|
||||
}
|
||||
|
@ -114,7 +118,7 @@ class UserListFragment @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean = withState(viewModel) {
|
||||
sharedActionViewModel.post(UserListSharedAction.OnMenuItemSelected(item.itemId, it.pendingInvitees))
|
||||
sharedActionViewModel.post(UserListSharedAction.OnMenuItemSelected(item.itemId, it.pendingSelections))
|
||||
return@withState true
|
||||
}
|
||||
|
||||
|
@ -156,14 +160,14 @@ class UserListFragment @Inject constructor(
|
|||
userListController.setData(it)
|
||||
}
|
||||
|
||||
private fun renderSelectedUsers(invitees: Set<PendingInvitee>) {
|
||||
private fun renderSelectedUsers(selections: Set<PendingSelection>) {
|
||||
invalidateOptionsMenu()
|
||||
|
||||
val currentNumberOfChips = views.chipGroup.childCount
|
||||
val newNumberOfChips = invitees.size
|
||||
val newNumberOfChips = selections.size
|
||||
|
||||
views.chipGroup.removeAllViews()
|
||||
invitees.forEach { addChipToGroup(it) }
|
||||
selections.forEach { addChipToGroup(it) }
|
||||
|
||||
// Scroll to the bottom when adding chips. When removing chips, do not scroll
|
||||
if (newNumberOfChips >= currentNumberOfChips) {
|
||||
|
@ -173,20 +177,22 @@ class UserListFragment @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun addChipToGroup(pendingInvitee: PendingInvitee) {
|
||||
private fun addChipToGroup(pendingSelection: PendingSelection) {
|
||||
val chip = Chip(requireContext())
|
||||
chip.setChipBackgroundColorResource(android.R.color.transparent)
|
||||
chip.chipStrokeWidth = dimensionConverter.dpToPx(1).toFloat()
|
||||
chip.text = pendingInvitee.getBestName()
|
||||
chip.text = pendingSelection.getBestName()
|
||||
chip.isClickable = true
|
||||
chip.isCheckable = false
|
||||
chip.isCloseIconVisible = true
|
||||
views.chipGroup.addView(chip)
|
||||
chip.setOnCloseIconClickListener {
|
||||
viewModel.handle(UserListAction.RemovePendingInvitee(pendingInvitee))
|
||||
viewModel.handle(UserListAction.RemovePendingSelection(pendingSelection))
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentState() = withState(viewModel) { it }
|
||||
|
||||
override fun onInviteFriendClick() {
|
||||
viewModel.handle(UserListAction.ComputeMatrixToLinkForSharing)
|
||||
}
|
||||
|
@ -197,17 +203,17 @@ class UserListFragment @Inject constructor(
|
|||
|
||||
override fun onItemClick(user: User) {
|
||||
view?.hideKeyboard()
|
||||
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(user)))
|
||||
viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.UserPendingSelection(user)))
|
||||
}
|
||||
|
||||
override fun onMatrixIdClick(matrixId: String) {
|
||||
view?.hideKeyboard()
|
||||
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.UserPendingInvitee(User(matrixId))))
|
||||
viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.UserPendingSelection(User(matrixId))))
|
||||
}
|
||||
|
||||
override fun onThreePidClick(threePid: ThreePid) {
|
||||
view?.hideKeyboard()
|
||||
viewModel.handle(UserListAction.SelectPendingInvitee(PendingInvitee.ThreePidPendingInvitee(threePid)))
|
||||
viewModel.handle(UserListAction.AddPendingSelection(PendingSelection.ThreePidPendingSelection(threePid)))
|
||||
}
|
||||
|
||||
override fun onUseQRCode() {
|
||||
|
|
|
@ -24,5 +24,7 @@ data class UserListFragmentArgs(
|
|||
val title: String,
|
||||
val menuResId: Int,
|
||||
val excludedUserIds: Set<String>? = null,
|
||||
val existingRoomId: String? = null
|
||||
val singleSelection: Boolean = false,
|
||||
val showInviteActions: Boolean = true,
|
||||
val showToolbar: Boolean = true
|
||||
) : Parcelable
|
||||
|
|
|
@ -21,7 +21,7 @@ import im.vector.app.core.platform.VectorSharedAction
|
|||
sealed class UserListSharedAction : VectorSharedAction {
|
||||
object Close : UserListSharedAction()
|
||||
object GoBack : UserListSharedAction()
|
||||
data class OnMenuItemSelected(val itemId: Int, val invitees: Set<PendingInvitee>) : UserListSharedAction()
|
||||
data class OnMenuItemSelected(val itemId: Int, val selections: Set<PendingSelection>) : UserListSharedAction()
|
||||
object OpenPhoneBook : UserListSharedAction()
|
||||
object AddByQrCode : UserListSharedAction()
|
||||
}
|
||||
|
|
|
@ -68,21 +68,15 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
|
|||
}
|
||||
|
||||
init {
|
||||
setState {
|
||||
copy(
|
||||
myUserId = session.myUserId,
|
||||
existingRoomId = initialState.existingRoomId
|
||||
)
|
||||
}
|
||||
observeUsers()
|
||||
}
|
||||
|
||||
override fun handle(action: UserListAction) {
|
||||
when (action) {
|
||||
is UserListAction.SearchUsers -> handleSearchUsers(action.value)
|
||||
is UserListAction.ClearSearchUsers -> handleClearSearchUsers()
|
||||
is UserListAction.SelectPendingInvitee -> handleSelectUser(action)
|
||||
is UserListAction.RemovePendingInvitee -> handleRemoveSelectedUser(action)
|
||||
is UserListAction.SearchUsers -> handleSearchUsers(action.value)
|
||||
is UserListAction.ClearSearchUsers -> handleClearSearchUsers()
|
||||
is UserListAction.AddPendingSelection -> handleSelectUser(action)
|
||||
is UserListAction.RemovePendingSelection -> handleRemoveSelectedUser(action)
|
||||
UserListAction.ComputeMatrixToLinkForSharing -> handleShareMyMatrixToLink()
|
||||
}.exhaustive
|
||||
}
|
||||
|
@ -168,13 +162,13 @@ class UserListViewModel @AssistedInject constructor(@Assisted initialState: User
|
|||
.disposeOnClear()
|
||||
}
|
||||
|
||||
private fun handleSelectUser(action: UserListAction.SelectPendingInvitee) = withState { state ->
|
||||
val selectedUsers = state.pendingInvitees.toggle(action.pendingInvitee)
|
||||
setState { copy(pendingInvitees = selectedUsers) }
|
||||
private fun handleSelectUser(action: UserListAction.AddPendingSelection) = withState { state ->
|
||||
val selections = state.pendingSelections.toggle(action.pendingSelection, singleElement = state.singleSelection)
|
||||
setState { copy(pendingSelections = selections) }
|
||||
}
|
||||
|
||||
private fun handleRemoveSelectedUser(action: UserListAction.RemovePendingInvitee) = withState { state ->
|
||||
val selectedUsers = state.pendingInvitees.minus(action.pendingInvitee)
|
||||
setState { copy(pendingInvitees = selectedUsers) }
|
||||
private fun handleRemoveSelectedUser(action: UserListAction.RemovePendingSelection) = withState { state ->
|
||||
val selections = state.pendingSelections.minus(action.pendingSelection)
|
||||
setState { copy(pendingSelections = selections) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,24 +28,27 @@ data class UserListViewState(
|
|||
val knownUsers: Async<PagedList<User>> = Uninitialized,
|
||||
val directoryUsers: Async<List<User>> = Uninitialized,
|
||||
val filteredMappedContacts: List<MappedContact> = emptyList(),
|
||||
val pendingInvitees: Set<PendingInvitee> = emptySet(),
|
||||
val createAndInviteState: Async<String> = Uninitialized,
|
||||
val pendingSelections: Set<PendingSelection> = emptySet(),
|
||||
val searchTerm: String = "",
|
||||
val myUserId: String = "",
|
||||
val existingRoomId: String? = null
|
||||
val singleSelection: Boolean,
|
||||
private val showInviteActions: Boolean
|
||||
) : MvRxState {
|
||||
|
||||
constructor(args: UserListFragmentArgs) : this(
|
||||
existingRoomId = args.existingRoomId
|
||||
excludedUserIds = args.excludedUserIds,
|
||||
singleSelection = args.singleSelection,
|
||||
showInviteActions = args.showInviteActions
|
||||
)
|
||||
|
||||
fun getSelectedMatrixId(): List<String> {
|
||||
return pendingInvitees
|
||||
return pendingSelections
|
||||
.mapNotNull {
|
||||
when (it) {
|
||||
is PendingInvitee.UserPendingInvitee -> it.user.userId
|
||||
is PendingInvitee.ThreePidPendingInvitee -> null
|
||||
is PendingSelection.UserPendingSelection -> it.user.userId
|
||||
is PendingSelection.ThreePidPendingSelection -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showInviteActions() = showInviteActions && pendingSelections.isEmpty()
|
||||
}
|
||||
|
|
17
vector/src/main/res/drawable/ic_call_transfer.xml
Normal file
17
vector/src/main/res/drawable/ic_call_transfer.xml
Normal file
|
@ -0,0 +1,17 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24h-24z"/>
|
||||
<path
|
||||
android:pathData="M8.027,15.9613C9.168,17.1932 11.9148,19.3263 12.6635,19.7641C12.7078,19.79 12.7585,19.8201 12.8152,19.8538C13.9576,20.5329 17.5373,22.6609 20.1454,20.6694C22.1661,19.1266 21.5091,17.3909 20.8289,16.875C20.3633,16.5128 18.9914,15.5145 17.7006,14.6152C16.4331,13.7322 15.7268,14.4397 15.2492,14.918C15.2404,14.9268 15.2317,14.9355 15.2231,14.9442L14.2621,15.9051C14.0174,16.1498 13.6451,16.0605 13.2886,15.7804C12.0092,14.8061 11.0681,13.8659 10.5972,13.395L10.5933,13.391C10.1225,12.9202 9.1939,11.9908 8.2196,10.7114C7.9395,10.3548 7.8502,9.9826 8.0949,9.7379L9.0559,8.7769C9.0645,8.7683 9.0732,8.7596 9.082,8.7508C9.5603,8.2732 10.2678,7.5668 9.3848,6.2994C8.4855,5.0086 7.4872,3.6367 7.125,3.1711C6.6091,2.4909 4.8734,1.8339 3.3306,3.8546C1.3391,6.4627 3.4671,10.0424 4.1462,11.1848C4.1799,11.2415 4.2101,11.2922 4.2359,11.3365C4.6737,12.0851 6.7951,14.8203 8.027,15.9613Z"
|
||||
android:fillColor="#C1C6CD"/>
|
||||
<path
|
||||
android:pathData="M13.3084,9.7333C12.9179,10.1238 12.9179,10.757 13.3084,11.1475C13.6989,11.538 14.3321,11.538 14.7226,11.1475L20.4401,5.43L20.4401,9.9101C20.4401,10.4624 20.8878,10.9101 21.4401,10.9101C21.9924,10.9101 22.4401,10.4624 22.4401,9.9101L22.4401,3.0158C22.4401,2.4635 21.9924,2.0158 21.4401,2.0158H14.5458C13.9935,2.0158 13.5458,2.4635 13.5458,3.0158C13.5458,3.5681 13.9935,4.0158 14.5458,4.0158L19.0259,4.0158L13.3084,9.7333Z"
|
||||
android:fillColor="#C1C6CD"
|
||||
android:fillType="evenOdd"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -14,10 +14,10 @@
|
|||
|
||||
<ImageView
|
||||
android:id="@+id/bgCallView"
|
||||
tools:src="@tools:sample/avatars"
|
||||
android:scaleType="centerCrop"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<org.webrtc.SurfaceViewRenderer
|
||||
android:id="@+id/fullscreenRenderer"
|
||||
|
@ -36,6 +36,35 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/otherKnownCallLayout"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="144dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:background="@color/riotx_background_light"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/otherKnownCallAvatarView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/otherSmallIsHeldIcon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_call_small_pause" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/otherMemberAvatar"
|
||||
android:layout_width="80dp"
|
||||
|
@ -49,14 +78,14 @@
|
|||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/smallIsHeldIcon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:id="@+id/smallIsHeldIcon"
|
||||
android:src="@drawable/ic_call_small_pause"
|
||||
app:layout_constraintTop_toTopOf="@id/otherMemberAvatar"
|
||||
app:layout_constraintBottom_toBottomOf="@id/otherMemberAvatar"
|
||||
app:layout_constraintEnd_toEndOf="@id/otherMemberAvatar"
|
||||
app:layout_constraintStart_toStartOf="@id/otherMemberAvatar"
|
||||
app:layout_constraintEnd_toEndOf="@id/otherMemberAvatar" />
|
||||
app:layout_constraintTop_toTopOf="@id/otherMemberAvatar" />
|
||||
|
||||
|
||||
<TextView
|
||||
|
@ -95,10 +124,10 @@
|
|||
android:id="@+id/callActionText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_margin="8dp"
|
||||
android:gravity="center"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:textSize="14sp"
|
||||
android:layout_margin="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/callStatusText"
|
||||
|
|
76
vector/src/main/res/layout/activity_call_transfer.xml
Normal file
76
vector/src/main/res/layout/activity_call_transfer.xml
Normal file
|
@ -0,0 +1,76 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/vector_coordinator_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/callTransferToolbar"
|
||||
style="@style/VectorToolbarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:elevation="4dp"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/callTransferFragmentContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/callTransferActionsLayout"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/callTransferToolbar" />
|
||||
|
||||
<RelativeLayout
|
||||
android:background="?riotx_header_panel_background"
|
||||
android:id="@+id/callTransferActionsLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:paddingVertical="8dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintStart_toEndOf="parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/callTransferConsultCheckBox"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:enabled="false"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/callTransferConsultTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toEndOf="@id/callTransferConsultCheckBox"
|
||||
android:layout_toStartOf="@+id/callTransferConnectAction"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:text="@string/call_transfer_consult_first" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/callTransferConnectAction"
|
||||
style="@style/VectorButtonStyleText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:text="@string/call_transfer_connect_action"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
<include
|
||||
android:id="@+id/waiting_view"
|
||||
layout="@layout/merge_overlay_waiting_view" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -4,6 +4,8 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:style="@style/AlertStyle"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
@ -43,6 +45,8 @@
|
|||
android:layout_marginEnd="8dp"
|
||||
android:ellipsize="end"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
app:drawableTint="?riotx_text_secondary"
|
||||
android:drawablePadding="4dp"
|
||||
android:textSize="15sp"
|
||||
android:maxLines="1"
|
||||
app:layout_constraintEnd_toStartOf="@+id/incomingCallRejectView"
|
||||
|
@ -63,6 +67,7 @@
|
|||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:tint="@color/white"
|
||||
android:src="@drawable/ic_call_answer" />
|
||||
|
||||
<ImageView
|
||||
|
|
|
@ -44,4 +44,13 @@
|
|||
app:tint="?attr/riotx_text_primary"
|
||||
tools:actionDescription="" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/callControlsTransfer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/call_transfer_title"
|
||||
app:leftIcon="@drawable/ic_call_transfer"
|
||||
app:tint="?attr/riotx_text_primary"
|
||||
tools:actionDescription="" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/syncStateView" />
|
||||
|
||||
<im.vector.app.core.ui.views.ActiveCallView
|
||||
<im.vector.app.core.ui.views.CurrentCallsView
|
||||
android:id="@+id/activeCallView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -96,7 +96,7 @@
|
|||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/roomToolbar" />
|
||||
|
||||
<im.vector.app.core.ui.views.ActiveCallView
|
||||
<im.vector.app.core.ui.views.CurrentCallsView
|
||||
android:id="@+id/activeCallView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
|
|
@ -9,38 +9,38 @@
|
|||
tools:parentTag="android.widget.RelativeLayout">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/activeCallInfo"
|
||||
android:id="@+id/currentCallsInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toStartOf="@id/returnToCallButton"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:drawablePadding="10dp"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:text="@string/active_call"
|
||||
android:textSize="14sp"
|
||||
android:text="@string/call_only_active"
|
||||
android:textColor="@color/white"
|
||||
app:drawableTint="@color/white"
|
||||
app:drawableStartCompat="@drawable/ic_call" />
|
||||
app:drawableStartCompat="@drawable/ic_call_answer" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/returnToCallButton"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignTop="@+id/activeCallInfo"
|
||||
android:layout_alignBottom="@+id/activeCallInfo"
|
||||
android:layout_alignTop="@+id/currentCallsInfo"
|
||||
android:layout_alignBottom="@+id/currentCallsInfo"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:gravity="center"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:text="@string/return_to_call"
|
||||
android:text="@string/action_return"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="15sp"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
</merge>
|
|
@ -141,6 +141,7 @@
|
|||
<string name="action_unpublish">Unpublish</string>
|
||||
<string name="copied_to_clipboard">Copied to clipboard</string>
|
||||
<string name="disable">Disable</string>
|
||||
<string name="action_return">Return</string>
|
||||
|
||||
<!-- dialog titles -->
|
||||
<string name="dialog_title_confirmation">Confirmation</string>
|
||||
|
@ -2773,4 +2774,23 @@
|
|||
<string name="call_tile_other_declined">%1$s declined this call</string>
|
||||
<string name="call_tile_ended">This call has ended</string>
|
||||
<string name="call_tile_call_back">Call back</string>
|
||||
|
||||
|
||||
<string name="call_only_active">Active call (%1$s)</string>
|
||||
<plurals name="call_only_paused">
|
||||
<item quantity="one">Paused call</item>
|
||||
<item quantity="other">%1$d paused calls</item>
|
||||
</plurals>
|
||||
<plurals name="call_one_active_and_other_paused">
|
||||
<item quantity="one">1 active call (%1$s) · 1 paused call</item>
|
||||
<item quantity="other">1 active call (%1$s) · %2$d paused calls</item>
|
||||
</plurals>
|
||||
|
||||
|
||||
<string name="call_transfer_consult_first">Consult first</string>
|
||||
<string name="call_transfer_connect_action">Connect</string>
|
||||
<string name="call_transfer_title">Transfer</string>
|
||||
<string name="call_transfer_failure">An error occured while transfering call</string>
|
||||
|
||||
|
||||
</resources>
|
||||
|
|
Loading…
Reference in a new issue