Merge pull request #2830 from vector-im/feature/fga/fix_some_voip_issues

Feature/fga/fix some voip issues
This commit is contained in:
Benoit Marty 2021-02-17 19:01:08 +01:00 committed by GitHub
commit fd94536118
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 444 additions and 278 deletions

View file

@ -26,6 +26,7 @@
<w>pkcs</w> <w>pkcs</w>
<w>previewable</w> <w>previewable</w>
<w>previewables</w> <w>previewables</w>
<w>pstn</w>
<w>riotx</w> <w>riotx</w>
<w>signin</w> <w>signin</w>
<w>signout</w> <w>signout</w>

View file

@ -16,12 +16,11 @@
package org.matrix.android.sdk.api.session.call package org.matrix.android.sdk.api.session.call
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.util.Cancelable
interface CallSignalingService { interface CallSignalingService {
fun getTurnServer(callback: MatrixCallback<TurnServerResponse>): Cancelable suspend fun getTurnServer(): TurnServerResponse
fun getPSTNProtocolChecker(): PSTNProtocolChecker
/** /**
* Create an outgoing call * Create an outgoing call

View file

@ -0,0 +1,98 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.api.session.call
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.thirdparty.GetThirdPartyProtocolsTask
import org.matrix.android.sdk.internal.task.TaskExecutor
import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
private const val PSTN_VECTOR_KEY = "im.vector.protocol.pstn"
private const val PSTN_MATRIX_KEY = "m.protocol.pstn"
/**
* This class is responsible for checking if the HS support the PSTN protocol.
* As long as the request succeed, it'll check only once by session.
*/
@SessionScope
class PSTNProtocolChecker @Inject internal constructor(private val taskExecutor: TaskExecutor,
private val getThirdPartyProtocolsTask: GetThirdPartyProtocolsTask) {
interface Listener {
fun onPSTNSupportUpdated()
}
private var alreadyChecked = AtomicBoolean(false)
private val pstnSupportListeners = mutableListOf<Listener>()
fun addListener(listener: Listener) {
pstnSupportListeners.add(listener)
}
fun removeListener(listener: Listener) {
pstnSupportListeners.remove(listener)
}
var supportedPSTNProtocol: String? = null
private set
fun checkForPSTNSupportIfNeeded() {
if (alreadyChecked.get()) return
taskExecutor.executorScope.checkForPSTNSupport()
}
private fun CoroutineScope.checkForPSTNSupport() = launch {
try {
supportedPSTNProtocol = getSupportedPSTN(3)
alreadyChecked.set(true)
if (supportedPSTNProtocol != null) {
pstnSupportListeners.forEach {
tryOrNull { it.onPSTNSupportUpdated() }
}
}
} catch (failure: Throwable) {
Timber.v("Fail to get supported PSTN, will check again next time.")
}
}
private suspend fun getSupportedPSTN(maxTries: Int): String? {
val thirdPartyProtocols: Map<String, ThirdPartyProtocol> = try {
getThirdPartyProtocolsTask.execute(Unit)
} catch (failure: Throwable) {
if (maxTries == 1) {
throw failure
} else {
// Wait for 10s before trying again
delay(10_000L)
return getSupportedPSTN(maxTries - 1)
}
}
return when {
thirdPartyProtocols.containsKey(PSTN_VECTOR_KEY) -> PSTN_VECTOR_KEY
thirdPartyProtocols.containsKey(PSTN_MATRIX_KEY) -> PSTN_MATRIX_KEY
else -> null
}
}
}

View file

@ -16,16 +16,12 @@
package org.matrix.android.sdk.internal.session.call package org.matrix.android.sdk.internal.session.call
import kotlinx.coroutines.Dispatchers
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.call.CallListener import org.matrix.android.sdk.api.session.call.CallListener
import org.matrix.android.sdk.api.session.call.CallSignalingService import org.matrix.android.sdk.api.session.call.CallSignalingService
import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxCall
import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker
import org.matrix.android.sdk.api.session.call.TurnServerResponse import org.matrix.android.sdk.api.session.call.TurnServerResponse
import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.launchToCallback
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -34,14 +30,16 @@ internal class DefaultCallSignalingService @Inject constructor(
private val callSignalingHandler: CallSignalingHandler, private val callSignalingHandler: CallSignalingHandler,
private val mxCallFactory: MxCallFactory, private val mxCallFactory: MxCallFactory,
private val activeCallHandler: ActiveCallHandler, private val activeCallHandler: ActiveCallHandler,
private val taskExecutor: TaskExecutor, private val turnServerDataSource: TurnServerDataSource,
private val turnServerDataSource: TurnServerDataSource private val pstnProtocolChecker: PSTNProtocolChecker
) : CallSignalingService { ) : CallSignalingService {
override fun getTurnServer(callback: MatrixCallback<TurnServerResponse>): Cancelable { override suspend fun getTurnServer(): TurnServerResponse {
return taskExecutor.executorScope.launchToCallback(Dispatchers.Default, callback) { return turnServerDataSource.getTurnServer()
turnServerDataSource.getTurnServer()
} }
override fun getPSTNProtocolChecker(): PSTNProtocolChecker {
return pstnProtocolChecker
} }
override fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall { override fun createOutgoingCall(roomId: String, otherUserId: String, isVideoCall: Boolean): MxCall {

View file

@ -0,0 +1,34 @@
/*
* Copyright 2019 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.epoxy
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
@EpoxyModelClass(layout = R.layout.item_timeline_empty)
abstract class TimelineEmptyItem : VectorEpoxyModel<TimelineEmptyItem.Holder>(), ItemWithEvents {
@EpoxyAttribute lateinit var eventId: String
override fun getEventIds(): List<String> {
return listOf(eventId)
}
class Holder : VectorEpoxyHolder()
}

View file

@ -16,6 +16,7 @@
package im.vector.app.features.call package im.vector.app.features.call
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
@ -29,17 +30,16 @@ import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.call.audio.CallAudioManager import im.vector.app.features.call.audio.CallAudioManager
import im.vector.app.features.call.webrtc.WebRtcCall import im.vector.app.features.call.webrtc.WebRtcCall
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import org.matrix.android.sdk.api.MatrixCallback import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxCall
import org.matrix.android.sdk.api.session.call.MxPeerConnectionState import org.matrix.android.sdk.api.session.call.MxPeerConnectionState
import org.matrix.android.sdk.api.session.call.TurnServerResponse
import org.matrix.android.sdk.api.session.room.model.call.supportCallTransfer import org.matrix.android.sdk.api.session.room.model.call.supportCallTransfer
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import java.util.Timer
import java.util.TimerTask
class VectorCallViewModel @AssistedInject constructor( class VectorCallViewModel @AssistedInject constructor(
@Assisted initialState: VectorCallViewState, @Assisted initialState: VectorCallViewState,
@ -50,7 +50,7 @@ class VectorCallViewModel @AssistedInject constructor(
private var call: WebRtcCall? = null private var call: WebRtcCall? = null
private var connectionTimeoutTimer: Timer? = null private var connectionTimeoutJob: Job? = null
private var hasBeenConnectedOnce = false private var hasBeenConnectedOnce = false
private val callListener = object : WebRtcCall.Listener { private val callListener = object : WebRtcCall.Listener {
@ -92,26 +92,20 @@ class VectorCallViewModel @AssistedInject constructor(
val callState = call.state val callState = call.state
if (callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED) { if (callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
hasBeenConnectedOnce = true hasBeenConnectedOnce = true
connectionTimeoutTimer?.cancel() connectionTimeoutJob?.cancel()
connectionTimeoutTimer = null connectionTimeoutJob = null
} else { } else {
// do we reset as long as it's moving? // do we reset as long as it's moving?
connectionTimeoutTimer?.cancel() connectionTimeoutJob?.cancel()
if (hasBeenConnectedOnce) { if (hasBeenConnectedOnce) {
connectionTimeoutTimer = Timer().apply { connectionTimeoutJob = viewModelScope.launch {
schedule(object : TimerTask() { delay(30_000)
override fun run() { try {
session.callSignalingService().getTurnServer(object : MatrixCallback<TurnServerResponse> { val turn = session.callSignalingService().getTurnServer()
override fun onFailure(failure: Throwable) { _viewEvents.post(VectorCallViewEvents.ConnectionTimeout(turn))
} catch (failure: Throwable) {
_viewEvents.post(VectorCallViewEvents.ConnectionTimeout(null)) _viewEvents.post(VectorCallViewEvents.ConnectionTimeout(null))
} }
override fun onSuccess(data: TurnServerResponse) {
_viewEvents.post(VectorCallViewEvents.ConnectionTimeout(data))
}
})
}
}, 30_000)
} }
} }
} }

View file

@ -22,20 +22,22 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import javax.inject.Inject import javax.inject.Inject
class DialPadLookup @Inject constructor(val session: Session, class DialPadLookup @Inject constructor(
val directRoomHelper: DirectRoomHelper, private val session: Session,
val callManager: WebRtcCallManager private val directRoomHelper: DirectRoomHelper,
private val callManager: WebRtcCallManager
) { ) {
class Failure : Throwable() class Failure : Throwable()
data class Result(val userId: String, val roomId: String) data class Result(val userId: String, val roomId: String)
suspend fun lookupPhoneNumber(phoneNumber: String): Result { suspend fun lookupPhoneNumber(phoneNumber: String): Result {
val supportedProtocolKey = callManager.supportedPSTNProtocol ?: throw Failure() val supportedProtocolKey = callManager.supportedPSTNProtocol ?: throw Failure()
val thirdPartyUser = tryOrNull { val thirdPartyUser = tryOrNull {
session.thirdPartyService().getThirdPartyUser(supportedProtocolKey, fields = mapOf( session.thirdPartyService().getThirdPartyUser(
"m.id.phone" to phoneNumber protocol = supportedProtocolKey,
)).firstOrNull() fields = mapOf("m.id.phone" to phoneNumber)
).firstOrNull()
} ?: throw Failure() } ?: throw Failure()
val roomId = directRoomHelper.ensureDMExists(thirdPartyUser.userId) val roomId = directRoomHelper.ensureDMExists(thirdPartyUser.userId)

View file

@ -1,43 +0,0 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.call.webrtc
import kotlinx.coroutines.delay
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.thirdparty.ThirdPartyProtocol
private const val PSTN_VECTOR_KEY = "im.vector.protocol.pstn"
private const val PSTN_MATRIX_KEY = "m.protocol.pstn"
suspend fun Session.getSupportedPSTN(maxTries: Int): String? {
val thirdPartyProtocols: Map<String, ThirdPartyProtocol> = try {
thirdPartyService().getThirdPartyProtocols()
} catch (failure: Throwable) {
if (maxTries == 1) {
return null
} else {
// Wait for 10s before trying again
delay(10_000L)
return getSupportedPSTN(maxTries - 1)
}
}
return when {
thirdPartyProtocols.containsKey(PSTN_VECTOR_KEY) -> PSTN_VECTOR_KEY
thirdPartyProtocols.containsKey(PSTN_MATRIX_KEY) -> PSTN_MATRIX_KEY
else -> null
}
}

View file

@ -53,7 +53,6 @@ import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.session.room.model.call.CallNegotiateContent 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.api.session.room.model.call.SdpType
import org.matrix.android.sdk.internal.util.awaitCallback
import org.threeten.bp.Duration import org.threeten.bp.Duration
import org.webrtc.AudioSource import org.webrtc.AudioSource
import org.webrtc.AudioTrack import org.webrtc.AudioTrack
@ -420,9 +419,7 @@ class WebRtcCall(val mxCall: MxCall,
private suspend fun getTurnServer(): TurnServerResponse? { private suspend fun getTurnServer(): TurnServerResponse? {
return tryOrNull { return tryOrNull {
awaitCallback { sessionProvider.get()?.callSignalingService()?.getTurnServer()
sessionProvider.get()?.callSignalingService()?.getTurnServer(it)
}
} }
} }

View file

@ -26,14 +26,13 @@ import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.audio.CallAudioManager import im.vector.app.features.call.audio.CallAudioManager
import im.vector.app.features.call.utils.EglUtils import im.vector.app.features.call.utils.EglUtils
import im.vector.app.push.fcm.FcmHelper import im.vector.app.push.fcm.FcmHelper
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.CallListener import org.matrix.android.sdk.api.session.call.CallListener
import org.matrix.android.sdk.api.session.call.CallState import org.matrix.android.sdk.api.session.call.CallState
import org.matrix.android.sdk.api.session.call.MxCall import org.matrix.android.sdk.api.session.call.MxCall
import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker
import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent import org.matrix.android.sdk.api.session.room.model.call.CallCandidatesContent
import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
@ -65,22 +64,26 @@ class WebRtcCallManager @Inject constructor(
private val currentSession: Session? private val currentSession: Session?
get() = activeSessionDataSource.currentValue?.orNull() get() = activeSessionDataSource.currentValue?.orNull()
private val pstnProtocolChecker: PSTNProtocolChecker?
get() = currentSession?.callSignalingService()?.getPSTNProtocolChecker()
interface CurrentCallListener { interface CurrentCallListener {
fun onCurrentCallChange(call: WebRtcCall?) {} fun onCurrentCallChange(call: WebRtcCall?) {}
fun onAudioDevicesChange() {} fun onAudioDevicesChange() {}
} }
interface PSTNSupportListener { val supportedPSTNProtocol: String?
fun onPSTNSupportUpdated() get() = pstnProtocolChecker?.supportedPSTNProtocol
val supportsPSTNProtocol: Boolean
get() = supportedPSTNProtocol != null
fun addPstnSupportListener(listener: PSTNProtocolChecker.Listener) {
pstnProtocolChecker?.addListener(listener)
} }
private val pstnSupportListeners = emptyList<PSTNSupportListener>().toMutableList() fun removePstnSupportListener(listener: PSTNProtocolChecker.Listener) {
fun addPstnSupportListener(listener: PSTNSupportListener) { pstnProtocolChecker?.removeListener(listener)
pstnSupportListeners.add(listener)
}
fun removePstnSupportListener(listener: PSTNSupportListener) {
pstnSupportListeners.remove(listener)
} }
private val currentCallsListeners = CopyOnWriteArrayList<CurrentCallListener>() private val currentCallsListeners = CopyOnWriteArrayList<CurrentCallListener>()
@ -104,27 +107,11 @@ class WebRtcCallManager @Inject constructor(
private var peerConnectionFactory: PeerConnectionFactory? = null private var peerConnectionFactory: PeerConnectionFactory? = null
private val executor = Executors.newSingleThreadExecutor() private val executor = Executors.newSingleThreadExecutor()
private val dispatcher = executor.asCoroutineDispatcher() private val dispatcher = executor.asCoroutineDispatcher()
var supportedPSTNProtocol: String? = null
private set
val supportsPSTNProtocol: Boolean
get() = supportedPSTNProtocol != null
private val rootEglBase by lazy { EglUtils.rootEglBase } private val rootEglBase by lazy { EglUtils.rootEglBase }
private var isInBackground: Boolean = true private var isInBackground: Boolean = true
init {
GlobalScope.launch {
supportedPSTNProtocol = currentSession?.getSupportedPSTN(3)
if (supportedPSTNProtocol != null) {
pstnSupportListeners.forEach {
tryOrNull { it.onPSTNSupportUpdated() }
}
}
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME) @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
fun entersForeground() { fun entersForeground() {
isInBackground = false isInBackground = false
@ -167,6 +154,10 @@ class WebRtcCallManager @Inject constructor(
return callsByCallId.values.toList() return callsByCallId.values.toList()
} }
fun checkForPSTNSupportIfNeeded() {
pstnProtocolChecker?.checkForPSTNSupportIfNeeded()
}
/** /**
* @return a set of all advertised call during the lifetime of the app. * @return a set of all advertised call during the lifetime of the app.
*/ */
@ -176,7 +167,6 @@ class WebRtcCallManager @Inject constructor(
Timber.v("## VOIP headSetButtonTapped") Timber.v("## VOIP headSetButtonTapped")
val call = getCurrentCall() ?: return val call = getCurrentCall() ?: return
if (call.mxCall.state is CallState.LocalRinging) { if (call.mxCall.state is CallState.LocalRinging) {
// accept call
call.acceptIncomingCall() call.acceptIncomingCall()
} }
if (call.mxCall.state is CallState.Connected) { if (call.mxCall.state is CallState.Connected) {

View file

@ -26,8 +26,8 @@ import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay import com.jakewharton.rxrelay2.BehaviorRelay
import com.jakewharton.rxrelay2.PublishRelay import com.jakewharton.rxrelay2.PublishRelay
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
@ -64,6 +64,7 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.call.PSTNProtocolChecker
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.LocalEcho import org.matrix.android.sdk.api.session.events.model.LocalEcho
@ -120,7 +121,7 @@ class RoomDetailViewModel @AssistedInject constructor(
private val directRoomHelper: DirectRoomHelper, private val directRoomHelper: DirectRoomHelper,
timelineSettingsFactory: TimelineSettingsFactory timelineSettingsFactory: TimelineSettingsFactory
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), ) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, WebRtcCallManager.PSTNSupportListener { Timeline.Listener, ChatEffectManager.Delegate, PSTNProtocolChecker.Listener {
private val room = session.getRoom(initialState.roomId)!! private val room = session.getRoom(initialState.roomId)!!
private val eventId = initialState.eventId private val eventId = initialState.eventId
@ -176,6 +177,7 @@ class RoomDetailViewModel @AssistedInject constructor(
// Inform the SDK that the room is displayed // Inform the SDK that the room is displayed
session.onRoomDisplayed(initialState.roomId) session.onRoomDisplayed(initialState.roomId)
callManager.addPstnSupportListener(this) callManager.addPstnSupportListener(this)
callManager.checkForPSTNSupportIfNeeded()
chatEffectManager.delegate = this chatEffectManager.delegate = this
} }

View file

@ -19,7 +19,7 @@ package im.vector.app.features.home.room.detail
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import im.vector.app.core.platform.DefaultListUpdateCallback import im.vector.app.core.platform.DefaultListUpdateCallback
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
@ -47,8 +47,8 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
if (layoutManager.findFirstVisibleItemPosition() != position) { if (layoutManager.findFirstVisibleItemPosition() != position) {
return return
} }
val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? BaseEventItem ?: return val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? ItemWithEvents ?: return
val firstNewItemIds = firstNewItem.getEventIds().firstOrNull() val firstNewItemIds = firstNewItem.getEventIds().firstOrNull() ?: return
val indexOfFirstNewItem = newTimelineEventIds.indexOf(firstNewItemIds) val indexOfFirstNewItem = newTimelineEventIds.indexOf(firstNewItemIds)
if (indexOfFirstNewItem != -1) { if (indexOfFirstNewItem != -1) {
Timber.v("Should scroll to position: $position") Timber.v("Should scroll to position: $position")

View file

@ -38,18 +38,15 @@ import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItem
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory 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.ContentDownloadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ReadMarkerVisibilityStateChangedListener import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.app.features.home.room.detail.timeline.item.BaseEventItem
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_ import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.media.VideoContentRenderer
@ -194,75 +191,20 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
} }
} }
private val interceptorHelper = TimelineControllerInterceptorHelper(
::positionOfReadMarker,
adapterPositionMapping,
vectorPreferences,
callManager
)
init { init {
addInterceptor(this) addInterceptor(this)
requestModelBuild() requestModelBuild()
} }
// Update position when we are building new items
override fun intercept(models: MutableList<EpoxyModel<*>>) = synchronized(modelCache) { override fun intercept(models: MutableList<EpoxyModel<*>>) = synchronized(modelCache) {
positionOfReadMarker = null interceptorHelper.intercept(models, unreadState, timeline, callback)
adapterPositionMapping.clear()
val callIds = mutableSetOf<String>()
val modelsIterator = models.listIterator()
val showHiddenEvents = vectorPreferences.shouldShowHiddenEvents()
modelsIterator.withIndex().forEach {
val index = it.index
val epoxyModel = it.value
if (epoxyModel is CallTileTimelineItem) {
val callId = epoxyModel.attributes.callId
// We should remove the call tile if we already have one for this call or
// if this is an active call tile without an actual call (which can happen with permalink)
val shouldRemoveCallItem = callIds.contains(callId)
|| (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive())
if (shouldRemoveCallItem && !showHiddenEvents) {
modelsIterator.remove()
return@forEach
}
callIds.add(callId)
}
if (epoxyModel is BaseEventItem) {
epoxyModel.getEventIds().forEach { eventId ->
adapterPositionMapping[eventId] = index
}
}
}
val currentUnreadState = this.unreadState
if (currentUnreadState is UnreadState.HasUnread) {
val position = adapterPositionMapping[currentUnreadState.firstUnreadEventId]?.plus(1)
positionOfReadMarker = position
if (position != null) {
val readMarker = TimelineReadMarkerItem_()
.also {
it.id("read_marker")
it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback))
}
models.add(position, readMarker)
}
}
val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false
if (shouldAddBackwardPrefetch) {
val indexOfPrefetchBackward = (previousModelsSize - 1)
.coerceAtMost(models.size - DEFAULT_PREFETCH_THRESHOLD)
.coerceAtLeast(0)
val loadingItem = LoadingItem_()
.id("prefetch_backward_loading${System.currentTimeMillis()}")
.showLoader(false)
.setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS)
models.add(indexOfPrefetchBackward, loadingItem)
}
val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false
if (shouldAddForwardPrefetch) {
val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD.coerceAtMost(models.size - 1)
val loadingItem = LoadingItem_()
.id("prefetch_forward_loading${System.currentTimeMillis()}")
.showLoader(false)
.setVisibilityStateChangedListener(Timeline.Direction.FORWARDS)
models.add(indexOfPrefetchForward, loadingItem)
}
previousModelsSize = models.size
} }
fun update(viewState: RoomDetailViewState) { fun update(viewState: RoomDetailViewState) {
@ -431,6 +373,14 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
} }
} }
private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ {
return onVisibilityStateChanged { _, _, visibilityState ->
if (visibilityState == VisibilityState.VISIBLE) {
callback?.onLoadMore(direction)
}
}
}
private fun updateUTDStates(event: TimelineEvent, nextEvent: TimelineEvent?) { private fun updateUTDStates(event: TimelineEvent, nextEvent: TimelineEvent?) {
if (vectorPreferences.labShowCompleteHistoryInEncryptedRoom()) { if (vectorPreferences.labShowCompleteHistoryInEncryptedRoom()) {
return return
@ -461,14 +411,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
return shouldAdd return shouldAdd
} }
private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ {
return onVisibilityStateChanged { _, _, visibilityState ->
if (visibilityState == VisibilityState.VISIBLE) {
callback?.onLoadMore(direction)
}
}
}
fun searchPositionOfEvent(eventId: String?): Int? = synchronized(modelCache) { fun searchPositionOfEvent(eventId: String?): Int? = synchronized(modelCache) {
return adapterPositionMapping[eventId] return adapterPositionMapping[eventId]
} }

View file

@ -16,7 +16,8 @@
package im.vector.app.features.home.room.detail.timeline.factory package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.core.epoxy.EmptyItem_ import im.vector.app.core.epoxy.TimelineEmptyItem
import im.vector.app.core.epoxy.TimelineEmptyItem_
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.UserPreferencesProvider 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.TimelineEventController
@ -114,6 +115,12 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
Timber.e(throwable, "failed to create message item") Timber.e(throwable, "failed to create message item")
defaultItemFactory.create(event, highlight, callback, throwable) defaultItemFactory.create(event, highlight, callback, throwable)
} }
return (computedModel ?: EmptyItem_()) return computedModel ?: buildEmptyItem(event)
}
private fun buildEmptyItem(timelineEvent: TimelineEvent): TimelineEmptyItem {
return TimelineEmptyItem_()
.id(timelineEvent.localId)
.eventId(timelineEvent.eventId)
} }
} }

View file

@ -0,0 +1,151 @@
/*
* Copyright (c) 2021 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.home.room.detail.timeline.helper
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState
import im.vector.app.core.epoxy.LoadingItem_
import im.vector.app.core.epoxy.TimelineEmptyItem_
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.detail.UnreadState
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import kotlin.reflect.KMutableProperty0
private const val DEFAULT_PREFETCH_THRESHOLD = 30
class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMutableProperty0<Int?>,
private val adapterPositionMapping: MutableMap<String, Int>,
private val vectorPreferences: VectorPreferences,
private val callManager: WebRtcCallManager
) {
private var previousModelsSize = 0
// Update position when we are building new items
fun intercept(
models: MutableList<EpoxyModel<*>>,
unreadState: UnreadState,
timeline: Timeline?,
callback: TimelineEventController.Callback?
) {
positionOfReadMarker.set(null)
adapterPositionMapping.clear()
val callIds = mutableSetOf<String>()
// Add some prefetch loader if needed
models.addBackwardPrefetchIfNeeded(timeline, callback)
models.addForwardPrefetchIfNeeded(timeline, callback)
val modelsIterator = models.listIterator()
val showHiddenEvents = vectorPreferences.shouldShowHiddenEvents()
var index = 0
val firstUnreadEventId = (unreadState as? UnreadState.HasUnread)?.firstUnreadEventId
// Then iterate on models so we have the exact positions in the adapter
modelsIterator.forEach { epoxyModel ->
if (epoxyModel is ItemWithEvents) {
epoxyModel.getEventIds().forEach { eventId ->
adapterPositionMapping[eventId] = index
if (eventId == firstUnreadEventId) {
modelsIterator.addReadMarkerItem(callback)
index++
positionOfReadMarker.set(index)
}
}
}
if (epoxyModel is CallTileTimelineItem) {
modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents)
}
index++
}
previousModelsSize = models.size
}
private fun MutableListIterator<EpoxyModel<*>>.addReadMarkerItem(callback: TimelineEventController.Callback?) {
val readMarker = TimelineReadMarkerItem_()
.also {
it.id("read_marker")
it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback))
}
add(readMarker)
// Use next as we still have some process to do before the next iterator loop
next()
}
private fun MutableListIterator<EpoxyModel<*>>.removeCallItemIfNeeded(
epoxyModel: CallTileTimelineItem,
callIds: MutableSet<String>,
showHiddenEvents: Boolean
) {
val callId = epoxyModel.attributes.callId
// We should remove the call tile if we already have one for this call or
// if this is an active call tile without an actual call (which can happen with permalink)
val shouldRemoveCallItem = callIds.contains(callId)
|| (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive())
if (shouldRemoveCallItem && !showHiddenEvents) {
remove()
val emptyItem = TimelineEmptyItem_()
.id(epoxyModel.id())
.eventId(epoxyModel.attributes.informationData.eventId)
add(emptyItem)
}
callIds.add(callId)
}
private fun MutableList<EpoxyModel<*>>.addBackwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) {
val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false
if (shouldAddBackwardPrefetch) {
val indexOfPrefetchBackward = (previousModelsSize - 1)
.coerceAtMost(size - DEFAULT_PREFETCH_THRESHOLD)
.coerceAtLeast(0)
val loadingItem = LoadingItem_()
.id("prefetch_backward_loading${System.currentTimeMillis()}")
.showLoader(false)
.setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS, callback)
add(indexOfPrefetchBackward, loadingItem)
}
}
private fun MutableList<EpoxyModel<*>>.addForwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) {
val shouldAddForwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.FORWARDS) ?: false
if (shouldAddForwardPrefetch) {
val indexOfPrefetchForward = DEFAULT_PREFETCH_THRESHOLD.coerceAtMost(size - 1)
val loadingItem = LoadingItem_()
.id("prefetch_forward_loading${System.currentTimeMillis()}")
.showLoader(false)
.setVisibilityStateChangedListener(Timeline.Direction.FORWARDS, callback)
add(indexOfPrefetchForward, loadingItem)
}
}
private fun LoadingItem_.setVisibilityStateChangedListener(
direction: Timeline.Direction,
callback: TimelineEventController.Callback?
): LoadingItem_ {
return onVisibilityStateChanged { _, _, visibilityState ->
if (visibilityState == VisibilityState.VISIBLE) {
callback?.onLoadMore(direction)
}
}
}
}

View file

@ -32,7 +32,7 @@ import im.vector.app.core.utils.DimensionConverter
/** /**
* Children must override getViewType() * Children must override getViewType()
*/ */
abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>() { abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>(), ItemWithEvents {
// To use for instance when opening a permalink with an eventId // To use for instance when opening a permalink with an eventId
@EpoxyAttribute @EpoxyAttribute
@ -53,12 +53,6 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
holder.checkableBackground.isChecked = highlighted holder.checkableBackground.isChecked = highlighted
} }
/**
* Returns the eventIds associated with the EventItem.
* Will generally get only one, but it handles the merging items.
*/
abstract fun getEventIds(): List<String>
abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() { abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() {
val leftGuideline by bind<View>(R.id.messageStartGuideline) val leftGuideline by bind<View>(R.id.messageStartGuideline)
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground) val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)

View file

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 New Vector Ltd * Copyright (c) 2021 New Vector Ltd
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,12 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.core.epoxy package im.vector.app.features.home.room.detail.timeline.item
import com.airbnb.epoxy.EpoxyModelClass interface ItemWithEvents {
import im.vector.app.R /**
* Returns the eventIds associated with the EventItem.
@EpoxyModelClass(layout = R.layout.item_empty) * Will generally get only one, but it handles the merged items.
abstract class EmptyItem : VectorEpoxyModel<EmptyItem.Holder>() { */
class Holder : VectorEpoxyHolder() fun getEventIds(): List<String>
} }