From f405532e4c27cfa858fba3910d7d39f17bd8f3b5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 29 Jul 2021 12:06:38 +0200 Subject: [PATCH] Introduces CallEventGrouper so we can manage properly call history --- .../timeline/TimelineEventController.kt | 51 +++++----- .../timeline/factory/CallItemFactory.kt | 92 ++++++++----------- .../factory/TimelineItemFactoryParams.kt | 4 +- .../timeline/helper/CallEventGrouper.kt | 64 +++++++++++++ .../timeline/item/CallTileTimelineItem.kt | 21 ++++- 5 files changed, 156 insertions(+), 76 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/CallEventGrouper.kt diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 6c75bb1e63..0317d95b80 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -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() + private val callEventGroupers = HashMap() + private val receiptsByEvent = HashMap>() private val modelCache = arrayListOf() private var currentSnapshot: List = 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> { - val receiptsByEvent = HashMap>() - 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 { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt index 9697fb6672..189db25730 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt @@ -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() - EventType.CALL_HANGUP -> root.getClearContent().toModel() - EventType.CALL_REJECT -> root.getClearContent().toModel() - EventType.CALL_ANSWER -> root.getClearContent().toModel() - else -> null - } - } - private fun createCallTileTimelineItem( roomId: String, callId: String, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt index ea5d21dc10..e35dbee95d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt @@ -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? diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/CallEventGrouper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/CallEventGrouper.kt new file mode 100644 index 0000000000..f3488583f4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/CallEventGrouper.kt @@ -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() + + fun add(timelineEvent: TimelineEvent) { + events.add(timelineEvent) + } + + fun isVideo(): Boolean { + val invite = getInvite() ?: return false + return invite.root.getClearContent().toModel()?.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 } + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt index c59f2258f8..68ed0254ba 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt @@ -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 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(R.id.itemCallStatusTextView) val endGuideline by bind(R.id.messageEndGuideline) val failedToSendIndicator by bind(R.id.messageFailToSendIndicator) + + val resources: Resources + get() = view.context.resources } companion object { @@ -215,6 +231,7 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem