Introduces CallEventGrouper so we can manage properly call history

This commit is contained in:
ganfra 2021-07-29 12:06:38 +02:00
parent d2785b69df
commit f405532e4c
5 changed files with 156 additions and 76 deletions

View File

@ -41,6 +41,7 @@ import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItem
import im.vector.app.features.home.room.detail.timeline.factory.ReadReceiptsItemFactory
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
import im.vector.app.features.home.room.detail.timeline.helper.CallEventGrouper
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.TimelineControllerInterceptorHelper
@ -48,7 +49,6 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiff
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
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.item.AbsMessageItem
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_
@ -56,7 +56,6 @@ import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
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.ReadReceiptsItem
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer
@ -164,6 +163,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
// Map eventId to adapter position
private val adapterPositionMapping = HashMap<String, Int>()
private val callEventGroupers = HashMap<String, CallEventGrouper>()
private val receiptsByEvent = HashMap<String, MutableList<ReadReceipt>>()
private val modelCache = arrayListOf<CacheItemData?>()
private var currentSnapshot: List<TimelineEvent> = emptyList()
private var inSubmitList: Boolean = false
@ -353,8 +354,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
if (modelCache.isEmpty()) {
return
}
val receiptsByEvents = getReadReceiptsByShownEvent()
val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvents)
preprocessReverseEvents()
val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvent)
(0 until modelCache.size).forEach { position ->
val event = currentSnapshot[position]
val nextEvent = currentSnapshot.nextOrNull(position)
@ -362,22 +363,28 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
}
val params = TimelineItemFactoryParams(
event = event,
prevEvent = prevEvent,
nextEvent = nextEvent,
nextDisplayableEvent = nextDisplayableEvent,
partialState = partialState,
lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts,
callback = callback
)
// Should be build if not cached or if model should be refreshed
if (modelCache[position] == null || modelCache[position]?.isCacheable == false) {
val callEventGrouper = if (EventType.isCallEvent(event.root.getClearType())) {
(event.root.getClearContent()?.get("call_id") as? String)?.let { callId -> callEventGroupers[callId] }
} else {
null
}
val params = TimelineItemFactoryParams(
event = event,
prevEvent = prevEvent,
nextEvent = nextEvent,
nextDisplayableEvent = nextDisplayableEvent,
partialState = partialState,
lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts,
callback = callback,
callEventGrouper = callEventGrouper
)
modelCache[position] = buildCacheItem(params)
}
val itemCachedData = modelCache[position] ?: return@forEach
// Then update with additional models if needed
modelCache[position] = itemCachedData.enrichWithModels(event, nextEvent, position, receiptsByEvents)
modelCache[position] = itemCachedData.enrichWithModels(event, nextEvent, position, receiptsByEvent)
}
}
@ -450,15 +457,18 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
return null
}
private fun getReadReceiptsByShownEvent(): Map<String, List<ReadReceipt>> {
val receiptsByEvent = HashMap<String, MutableList<ReadReceipt>>()
if (!userPreferencesProvider.shouldShowReadReceipts()) {
return receiptsByEvent
}
var lastShownEventId: String? = null
private fun preprocessReverseEvents() {
receiptsByEvent.clear()
callEventGroupers.clear()
val itr = currentSnapshot.listIterator(currentSnapshot.size)
var lastShownEventId: String? = null
while (itr.hasPrevious()) {
val event = itr.previous()
if (EventType.isCallEvent(event.root.getClearType())) {
(event.root.getClearContent()?.get("call_id") as? String)?.also { callId ->
callEventGroupers.getOrPut(callId) { CallEventGrouper(session.myUserId, callId) }.add(event)
}
}
val currentReadReceipts = ArrayList(event.readReceipts).filter {
it.user.userId != session.myUserId
}
@ -471,7 +481,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val existingReceipts = receiptsByEvent.getOrPut(lastShownEventId) { ArrayList() }
existingReceipts.addAll(currentReadReceipts)
}
return receiptsByEvent
}
private fun buildDaySeparatorItem(originServerTs: Long?): DaySeparatorItem {

View File

@ -16,81 +16,79 @@
package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.call.vectorCallService
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
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.RoomSummariesHolder
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
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
import org.matrix.android.sdk.api.session.Session
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.CallHangupContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class CallItemFactory @Inject constructor(
private val session: Session,
private val userPreferencesProvider: UserPreferencesProvider,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper,
private val messageColorProvider: MessageColorProvider,
private val messageInformationDataFactory: MessageInformationDataFactory,
private val messageItemAttributesFactory: MessageItemAttributesFactory,
private val avatarSizeProvider: AvatarSizeProvider,
private val roomSummariesHolder: RoomSummariesHolder,
private val callManager: WebRtcCallManager
) {
private val roomSummariesHolder: RoomSummariesHolder) {
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event
if (event.root.eventId == null) return null
val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()
val callEventGrouper = params.callEventGrouper ?: return null
val roomId = event.roomId
val informationData = messageInformationDataFactory.create(params)
val callSignalingContent = event.getCallSignalingContent() ?: return null
val callId = callSignalingContent.callId ?: return null
val call = callManager.getCallById(callId)
val callKind = when {
call == null -> CallTileTimelineItem.CallKind.UNKNOWN
call.mxCall.isVideoCall -> CallTileTimelineItem.CallKind.VIDEO
else -> CallTileTimelineItem.CallKind.AUDIO
}
val callKind = if (callEventGrouper.isVideo()) CallTileTimelineItem.CallKind.VIDEO else CallTileTimelineItem.CallKind.AUDIO
val isRinging = callEventGrouper.isRinging()
return when (event.root.getClearType()) {
EventType.CALL_ANSWER -> {
createCallTileTimelineItem(
roomId = roomId,
callId = callId,
callStatus = CallTileTimelineItem.CallStatus.IN_CALL,
callKind = callKind,
callback = params.callback,
highlight = params.isHighlighted,
informationData = informationData,
isStillActive = call != null
)
if (isRinging || showHiddenEvents) {
createCallTileTimelineItem(
roomId = roomId,
callId = callEventGrouper.callId,
callStatus = CallTileTimelineItem.CallStatus.IN_CALL,
callKind = callKind,
callback = params.callback,
highlight = params.isHighlighted,
informationData = informationData,
isStillActive = isRinging
)
} else {
null
}
}
EventType.CALL_INVITE -> {
createCallTileTimelineItem(
roomId = roomId,
callId = callId,
callStatus = CallTileTimelineItem.CallStatus.INVITED,
callKind = callKind,
callback = params.callback,
highlight = params.isHighlighted,
informationData = informationData,
isStillActive = call != null
)
if (isRinging || showHiddenEvents) {
createCallTileTimelineItem(
roomId = roomId,
callId = callEventGrouper.callId,
callStatus = CallTileTimelineItem.CallStatus.INVITED,
callKind = callKind,
callback = params.callback,
highlight = params.isHighlighted,
informationData = informationData,
isStillActive = isRinging
)
} else {
null
}
}
EventType.CALL_REJECT -> {
createCallTileTimelineItem(
roomId = roomId,
callId = callId,
callId = callEventGrouper.callId,
callStatus = CallTileTimelineItem.CallStatus.REJECTED,
callKind = callKind,
callback = params.callback,
@ -102,8 +100,8 @@ class CallItemFactory @Inject constructor(
EventType.CALL_HANGUP -> {
createCallTileTimelineItem(
roomId = roomId,
callId = callId,
callStatus = CallTileTimelineItem.CallStatus.ENDED,
callId = callEventGrouper.callId,
callStatus = if (callEventGrouper.callWasMissed()) CallTileTimelineItem.CallStatus.MISSED else CallTileTimelineItem.CallStatus.ENDED,
callKind = callKind,
callback = params.callback,
highlight = params.isHighlighted,
@ -115,16 +113,6 @@ class CallItemFactory @Inject constructor(
}
}
private fun TimelineEvent.getCallSignalingContent(): CallSignalingContent? {
return when (root.getClearType()) {
EventType.CALL_INVITE -> root.getClearContent().toModel<CallInviteContent>()
EventType.CALL_HANGUP -> root.getClearContent().toModel<CallHangupContent>()
EventType.CALL_REJECT -> root.getClearContent().toModel<CallRejectContent>()
EventType.CALL_ANSWER -> root.getClearContent().toModel<CallAnswerContent>()
else -> null
}
}
private fun createCallTileTimelineItem(
roomId: String,
callId: String,

View File

@ -17,6 +17,7 @@
package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.CallEventGrouper
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
data class TimelineItemFactoryParams(
@ -26,7 +27,8 @@ data class TimelineItemFactoryParams(
val nextDisplayableEvent: TimelineEvent? = null,
val partialState: TimelineEventController.PartialState = TimelineEventController.PartialState(),
val lastSentEventIdWithoutReadReceipts: String? = null,
val callback: TimelineEventController.Callback? = null
val callback: TimelineEventController.Callback? = null,
val callEventGrouper: CallEventGrouper?= null
) {
val highlightedEventId: String?

View File

@ -0,0 +1,64 @@
/*
* 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 org.matrix.android.sdk.api.extensions.orFalse
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.CallInviteContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
class CallEventGrouper(private val myUserId: String, val callId: String) {
private val events = HashSet<TimelineEvent>()
fun add(timelineEvent: TimelineEvent) {
events.add(timelineEvent)
}
fun isVideo(): Boolean {
val invite = getInvite() ?: return false
return invite.root.getClearContent().toModel<CallInviteContent>()?.isVideo().orFalse()
}
fun isRinging(): Boolean {
return getAnswer() == null && getHangup() == null && getReject() == null
}
/**
* Returns true if there are only events from the other side - we missed the call
*/
fun callWasMissed(): Boolean {
return events.none { it.senderInfo.userId == myUserId }
}
private fun getAnswer(): TimelineEvent? {
return events.firstOrNull { it.root.getClearType() == EventType.CALL_ANSWER }
}
private fun getInvite(): TimelineEvent? {
return events.firstOrNull { it.root.getClearType() == EventType.CALL_INVITE }
}
private fun getHangup(): TimelineEvent? {
return events.firstOrNull { it.root.getClearType() == EventType.CALL_HANGUP }
}
private fun getReject(): TimelineEvent? {
return events.firstOrNull { it.root.getClearType() == EventType.CALL_REJECT }
}
}

View File

@ -15,6 +15,7 @@
*/
package im.vector.app.features.home.room.detail.timeline.item
import android.content.res.Resources
import android.view.View
import android.view.ViewGroup
import android.widget.Button
@ -72,10 +73,22 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
CallStatus.IN_CALL -> renderInCallStatus(holder)
CallStatus.REJECTED -> renderRejectedStatus(holder)
CallStatus.ENDED -> renderEndedStatus(holder)
CallStatus.MISSED -> renderMissedStatus(holder)
}
renderSendState(holder.view, null, holder.failedToSendIndicator)
}
private fun renderMissedStatus(holder: Holder) {
holder.acceptRejectViewGroup.isVisible = false
holder.statusView.isVisible = true
val status = if (attributes.callKind == CallKind.VIDEO) {
holder.resources.getQuantityString(R.plurals.missed_video_call, 1)
} else {
holder.resources.getQuantityString(R.plurals.missed_audio_call, 1)
}
holder.statusView.text = status
}
private fun renderEndedStatus(holder: Holder) {
holder.acceptRejectViewGroup.isVisible = false
holder.statusView.isVisible = true
@ -91,7 +104,7 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
attributes.callback?.onTimelineItemAction(callbackAction)
}
} else {
holder.statusView.text = holder.view.context.getString(R.string.call_tile_other_declined, attributes.userOfInterest.getBestName())
holder.statusView.text = holder.resources.getString(R.string.call_tile_other_declined, attributes.userOfInterest.getBestName())
}
}
@ -166,7 +179,7 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
if (attributes.informationData.sentByMe) {
holder.statusView.setText(R.string.call_tile_you_started_call)
} else {
holder.statusView.text = holder.view.context.getString(R.string.call_tile_other_started_call, attributes.userOfInterest.getBestName())
holder.statusView.text = holder.resources.getString(R.string.call_tile_other_started_call, attributes.userOfInterest.getBestName())
}
}
}
@ -182,6 +195,9 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
val statusView by bind<TextView>(R.id.itemCallStatusTextView)
val endGuideline by bind<View>(R.id.messageEndGuideline)
val failedToSendIndicator by bind<ImageView>(R.id.messageFailToSendIndicator)
val resources: Resources
get() = view.context.resources
}
companion object {
@ -215,6 +231,7 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
INVITED,
IN_CALL,
REJECTED,
MISSED,
ENDED;
fun isActive() = this == INVITED || this == IN_CALL