From 0d56707fd39554ed7ec589c826fbca910a0ab869 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 12 Aug 2021 11:10:00 +0200 Subject: [PATCH] Timeline call tiles: refact grouping events and fix some issues --- .../timeline/TimelineEventController.kt | 24 +--- .../timeline/factory/CallItemFactory.kt | 17 ++- .../factory/TimelineItemFactoryParams.kt | 4 +- .../timeline/factory/WidgetItemFactory.kt | 17 ++- .../timeline/helper/CallEventGrouper.kt | 68 --------- .../timeline/helper/TimelineEventsGroups.kt | 136 ++++++++++++++++++ .../timeline/item/CallTileTimelineItem.kt | 27 +++- .../item_timeline_event_call_tile_stub.xml | 2 +- 8 files changed, 193 insertions(+), 102 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/CallEventGrouper.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.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 21a49f981f..1243262fff 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 @@ -31,8 +31,6 @@ import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.nextOrNull import im.vector.app.core.extensions.prevOrNull -import im.vector.app.core.resources.UserPreferencesProvider -import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.home.room.detail.JitsiState import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailViewState @@ -41,7 +39,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.TimelineEventsGroups 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 @@ -81,10 +79,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val mergedHeaderItemFactory: MergedHeaderItemFactory, private val session: Session, - private val callManager: WebRtcCallManager, @TimelineEventControllerHandler private val backgroundHandler: Handler, - private val userPreferencesProvider: UserPreferencesProvider, private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper, private val readReceiptsItemFactory: ReadReceiptsItemFactory ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { @@ -166,7 +162,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec // Map eventId to adapter position private val adapterPositionMapping = HashMap() - private val callEventGroupers = HashMap() + private val timelineEventsGroups = TimelineEventsGroups() private val receiptsByEvent = HashMap>() private val modelCache = arrayListOf() private var currentSnapshot: List = emptyList() @@ -366,11 +362,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } // 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 timelineEventsGroup = timelineEventsGroups.getOrNull(event) val params = TimelineItemFactoryParams( event = event, prevEvent = prevEvent, @@ -379,7 +371,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec partialState = partialState, lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts, callback = callback, - callEventGrouper = callEventGrouper + eventsGroup = timelineEventsGroup ) modelCache[position] = buildCacheItem(params) } @@ -460,16 +452,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private fun preprocessReverseEvents() { receiptsByEvent.clear() - callEventGroupers.clear() + timelineEventsGroups.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) - } - } + timelineEventsGroups.addOrIgnore(event) val currentReadReceipts = ArrayList(event.readReceipts).filter { it.user.userId != session.myUserId } 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 1a34976dad..3664b9ddf2 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 @@ -21,6 +21,7 @@ import im.vector.app.features.call.vectorCallService 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.CallSignalingEventsGroup 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 @@ -45,7 +46,7 @@ class CallItemFactory @Inject constructor( val event = params.event if (event.root.eventId == null) return null val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents() - val callEventGrouper = params.callEventGrouper ?: return null + val callEventGrouper = params.eventsGroup?.let { CallSignalingEventsGroup(it) } ?: return null val roomId = event.roomId val informationData = messageInformationDataFactory.create(params) val callKind = if (callEventGrouper.isVideo()) CallTileTimelineItem.CallKind.VIDEO else CallTileTimelineItem.CallKind.AUDIO @@ -60,7 +61,8 @@ class CallItemFactory @Inject constructor( callback = params.callback, highlight = params.isHighlighted, informationData = informationData, - isStillActive = callEventGrouper.isInCall() + isStillActive = callEventGrouper.isInCall(), + formattedDuration = callEventGrouper.formattedDuration() ) } else { null @@ -76,7 +78,8 @@ class CallItemFactory @Inject constructor( callback = params.callback, highlight = params.isHighlighted, informationData = informationData, - isStillActive = callEventGrouper.isRinging() + isStillActive = callEventGrouper.isRinging(), + formattedDuration = callEventGrouper.formattedDuration() ) } else { null @@ -91,7 +94,8 @@ class CallItemFactory @Inject constructor( callback = params.callback, highlight = params.isHighlighted, informationData = informationData, - isStillActive = false + isStillActive = false, + formattedDuration = callEventGrouper.formattedDuration() ) } EventType.CALL_HANGUP -> { @@ -103,7 +107,8 @@ class CallItemFactory @Inject constructor( callback = params.callback, highlight = params.isHighlighted, informationData = informationData, - isStillActive = false + isStillActive = false, + formattedDuration = callEventGrouper.formattedDuration() ) } else -> null @@ -118,6 +123,7 @@ class CallItemFactory @Inject constructor( informationData: MessageInformationData, highlight: Boolean, isStillActive: Boolean, + formattedDuration: String, callback: TimelineEventController.Callback? ): CallTileTimelineItem? { val correctedRoomId = session.vectorCallService.userMapper.nativeRoomForVirtualRoom(roomId) ?: roomId @@ -129,6 +135,7 @@ class CallItemFactory @Inject constructor( callStatus = callStatus, informationData = informationData, avatarRenderer = it.avatarRenderer, + formattedDuration = formattedDuration, messageColorProvider = messageColorProvider, itemClickListener = it.itemClickListener, itemLongClickListener = it.itemLongClickListener, 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 e35dbee95d..cdfedb2925 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,7 +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 im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroup import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent data class TimelineItemFactoryParams( @@ -28,7 +28,7 @@ data class TimelineItemFactoryParams( val partialState: TimelineEventController.PartialState = TimelineEventController.PartialState(), val lastSentEventIdWithoutReadReceipts: String? = null, val callback: TimelineEventController.Callback? = null, - val callEventGrouper: CallEventGrouper?= null + val eventsGroup: TimelineEventsGroup? = null ) { val highlightedEventId: String? diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt index e856907717..8c5ef25bc4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt @@ -16,16 +16,16 @@ package im.vector.app.features.home.room.detail.timeline.factory -import im.vector.app.ActiveSessionDataSource import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider +import im.vector.app.features.home.room.detail.timeline.helper.JitsiWidgetEventsGroup import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory 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 org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.widgets.model.WidgetContent import org.matrix.android.sdk.api.session.widgets.model.WidgetType @@ -38,6 +38,7 @@ class WidgetItemFactory @Inject constructor( private val avatarSizeProvider: AvatarSizeProvider, private val messageColorProvider: MessageColorProvider, private val avatarRenderer: AvatarRenderer, + private val userPreferencesProvider: UserPreferencesProvider, private val roomSummariesHolder: RoomSummariesHolder ) { @@ -58,8 +59,12 @@ class WidgetItemFactory @Inject constructor( val event = params.event val roomId = event.roomId val userOfInterest = roomSummariesHolder.get(roomId)?.toMatrixItem() ?: return null - val isActive = widgetContent.isActive() - val callStatus = if (isActive && widgetContent.id == params.partialState.jitsiState.widgetId) { + val isActiveTile = widgetContent.isActive() + val jitsiWidgetEventsGroup = params.eventsGroup?.let { JitsiWidgetEventsGroup(it) } ?: return null + val isCallStillActive = jitsiWidgetEventsGroup.isStillActive() + val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents() + if (isActiveTile && !isCallStillActive && !showHiddenEvents) return null + val callStatus = if (isActiveTile && widgetContent.id == params.partialState.jitsiState.widgetId) { if (params.partialState.jitsiState.hasJoined) { CallTileTimelineItem.CallStatus.IN_CALL } else { @@ -68,7 +73,6 @@ class WidgetItemFactory @Inject constructor( } else { CallTileTimelineItem.CallStatus.ENDED } - val fakeCallId = widgetContent.id ?: prevWidgetContent?.id ?: return null val attributes = CallTileTimelineItem.Attributes( callId = fakeCallId, @@ -83,7 +87,8 @@ class WidgetItemFactory @Inject constructor( readReceiptsCallback = params.callback, userOfInterest = userOfInterest, callback = params.callback, - isStillActive = isActive + isStillActive = isCallStillActive, + formattedDuration = "" ) return CallTileTimelineItem_() .attributes(attributes) 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 deleted file mode 100644 index f9ee508105..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/CallEventGrouper.kt +++ /dev/null @@ -1,68 +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.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 - } - - fun isInCall(): Boolean{ - return getHangup() == null && getReject() == null - } - - /** - * Returns true if there are only events from one side. - */ - fun callWasMissed(): Boolean { - return events.distinctBy { it.senderInfo.userId }.size == 1 - } - - 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/helper/TimelineEventsGroups.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt new file mode 100644 index 0000000000..eeeba166e7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt @@ -0,0 +1,136 @@ +/* + * 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 im.vector.app.core.utils.TextUtils +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 +import org.matrix.android.sdk.api.session.widgets.model.WidgetContent +import org.matrix.android.sdk.api.session.widgets.model.WidgetType +import org.threeten.bp.Duration + +class TimelineEventsGroup(val groupId: String) { + + val events: Set + get() = _events + + private val _events = HashSet() + + fun add(timelineEvent: TimelineEvent) { + _events.add(timelineEvent) + } +} + +class TimelineEventsGroups { + + private val groups = HashMap() + + fun addOrIgnore(event: TimelineEvent) { + val groupId = event.getGroupIdOrNull() ?: return + groups.getOrPut(groupId) { TimelineEventsGroup(groupId) }.add(event) + } + + fun getOrNull(event: TimelineEvent): TimelineEventsGroup? { + val groupId = event.getGroupIdOrNull() ?: return null + return groups[groupId] + } + + private fun TimelineEvent.getGroupIdOrNull(): String? { + val type = root.getClearType() + val content = root.getClearContent() + return if (EventType.isCallEvent(type)) { + (content?.get("call_id") as? String) + } else { + val widgetContent: WidgetContent = root.getClearContent().toModel() ?: return null + val isJitsi = WidgetType.fromString(widgetContent.type ?: "") == WidgetType.Jitsi + if (isJitsi) { + widgetContent.id + } else { + null + } + } + } + + fun clear() { + groups.clear() + } +} + +class JitsiWidgetEventsGroup(private val group: TimelineEventsGroup) { + + fun isStillActive(): Boolean { + return group.events.none { + it.root.getClearContent().toModel()?.isActive() == false + } + } +} + +class CallSignalingEventsGroup(private val group: TimelineEventsGroup) { + + val callId: String = group.groupId + + 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 + } + + fun isInCall(): Boolean { + return getHangup() == null && getReject() == null + } + + fun formattedDuration(): String { + val start = getAnswer()?.root?.originServerTs + val end = getHangup()?.root?.originServerTs + return if (start == null || end == null) { + "" + } else { + val durationInMillis = (end - start).coerceAtLeast(0L) + val duration = Duration.ofMillis(durationInMillis) + TextUtils.formatDuration(duration) + } + } + + /** + * Returns true if there are only events from one side. + */ + fun callWasMissed(): Boolean { + return group.events.distinctBy { it.senderInfo.userId }.size == 1 + } + + private fun getAnswer(): TimelineEvent? { + return group.events.firstOrNull { it.root.getClearType() == EventType.CALL_ANSWER } + } + + private fun getInvite(): TimelineEvent? { + return group.events.firstOrNull { it.root.getClearType() == EventType.CALL_INVITE } + } + + private fun getHangup(): TimelineEvent? { + return group.events.firstOrNull { it.root.getClearType() == EventType.CALL_HANGUP } + } + + private fun getReject(): TimelineEvent? { + return group.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 94dcd31423..7cd06d32de 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 @@ -16,6 +16,7 @@ package im.vector.app.features.home.room.detail.timeline.item import android.content.res.Resources +import android.telecom.Call import android.view.View import android.view.ViewGroup import android.widget.Button @@ -95,7 +96,19 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem { + val endCallStatus = holder.resources.getString(R.string.call_tile_video_call_has_ended, attributes.formattedDuration) + holder.statusView.setStatus(endCallStatus) + } + CallKind.AUDIO -> { + val endCallStatus = holder.resources.getString(R.string.call_tile_voice_call_has_ended, attributes.formattedDuration) + holder.statusView.setStatus(endCallStatus) + } + CallKind.CONFERENCE -> { + holder.statusView.setStatus(R.string.call_tile_ended) + } + } } private fun renderRejectedStatus(holder: Holder) { @@ -194,6 +207,10 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem { + holder.statusView.setStatus(R.string.call_tile_video_active) + } attributes.informationData.sentByMe -> { holder.statusView.setStatus(R.string.call_ringing) } @@ -207,8 +224,13 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem