From 30fe3773ae5bd430995057c8a22d375c61e6fe7c Mon Sep 17 00:00:00 2001 From: chagai95 <31655082+chagai95@users.noreply.github.com> Date: Thu, 19 May 2022 15:03:51 +0200 Subject: [PATCH 01/71] refactor - better naming, return native user id and not sip user id and create a dm with the native user instead of with the sip user --- .../features/call/dialpad/DialPadLookup.kt | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt index e835a74fd6..14ce5f2dc0 100644 --- a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt @@ -42,18 +42,23 @@ class DialPadLookup @Inject constructor( val sipUserId = thirdPartyUser.userId val nativeLookupResults = session.sipNativeLookup(thirdPartyUser.userId) // If I have a native user I check for an existing native room with him... - val roomId = if (nativeLookupResults.isNotEmpty()) { + if (nativeLookupResults.isNotEmpty()) { val nativeUserId = nativeLookupResults.first().userId if (nativeUserId == session.myUserId) { throw Failure.NumberIsYours } - session.roomService().getExistingDirectRoomWithUser(nativeUserId) - // if there is not, just create a DM with the sip user - ?: directRoomHelper.ensureDMExists(sipUserId) - } else { - // do the same if there is no corresponding native user. - directRoomHelper.ensureDMExists(sipUserId) + var nativeRoomId = session.getExistingDirectRoomWithUser(nativeUserId) + if (nativeRoomId == null) { + // if there is no existing native room with the existing native user, + // just create a DM with the native user + nativeRoomId = directRoomHelper.ensureDMExists(nativeUserId) + } + Timber.d("lookupPhoneNumber with nativeUserId: $nativeUserId and nativeRoomId: $nativeRoomId") + return Result(userId = nativeUserId, roomId = nativeRoomId) } - return Result(userId = sipUserId, roomId = roomId) + // If there is no native user then we return sipUserId and sipRoomId - this is usually a PSTN call. + val sipRoomId = directRoomHelper.ensureDMExists(sipUserId) + Timber.d("lookupPhoneNumber with sipRoomId: $sipRoomId and sipUserId: $sipUserId") + return Result(userId = sipUserId, roomId = sipRoomId) } } From 8c783f94142eaa2e36c01aa6d4885370e8d49528 Mon Sep 17 00:00:00 2001 From: chagai95 <31655082+chagai95@users.noreply.github.com> Date: Thu, 19 May 2022 15:12:04 +0200 Subject: [PATCH 02/71] Create 6101.bugfix --- changelog.d/6101.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6101.bugfix diff --git a/changelog.d/6101.bugfix b/changelog.d/6101.bugfix new file mode 100644 index 0000000000..2d8da5327d --- /dev/null +++ b/changelog.d/6101.bugfix @@ -0,0 +1 @@ +Refactor - better naming, return native user id and not sip user id and create a dm with the native user instead of with the sip user. From f949c517b6f369d79664fc0160aba1646006c200 Mon Sep 17 00:00:00 2001 From: chagai95 <31655082+chagai95@users.noreply.github.com> Date: Fri, 20 May 2022 15:52:43 +0200 Subject: [PATCH 03/71] import timber and use .roomService() --- .../java/im/vector/app/features/call/dialpad/DialPadLookup.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt index 14ce5f2dc0..3ab2ee50c0 100644 --- a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt @@ -23,6 +23,7 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.createdirect.DirectRoomHelper import org.matrix.android.sdk.api.session.Session import javax.inject.Inject +import timber.log.Timber class DialPadLookup @Inject constructor( private val session: Session, @@ -47,7 +48,7 @@ class DialPadLookup @Inject constructor( if (nativeUserId == session.myUserId) { throw Failure.NumberIsYours } - var nativeRoomId = session.getExistingDirectRoomWithUser(nativeUserId) + var nativeRoomId = session.roomService().getExistingDirectRoomWithUser(nativeUserId) if (nativeRoomId == null) { // if there is no existing native room with the existing native user, // just create a DM with the native user From c2707d4538c44aa4e9ec3ddbbc727eb01787e8da Mon Sep 17 00:00:00 2001 From: chagai95 <31655082+chagai95@users.noreply.github.com> Date: Tue, 14 Jun 2022 14:08:22 +0200 Subject: [PATCH 04/71] Wrong import order --- .../java/im/vector/app/features/call/dialpad/DialPadLookup.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt index 3ab2ee50c0..8f904c8ab8 100644 --- a/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt +++ b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt @@ -22,8 +22,8 @@ import im.vector.app.features.call.vectorCallService import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.createdirect.DirectRoomHelper import org.matrix.android.sdk.api.session.Session -import javax.inject.Inject import timber.log.Timber +import javax.inject.Inject class DialPadLookup @Inject constructor( private val session: Session, From bd9fa483127b3acacee8cf1ecb010e5fdb5875a9 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 21 Jun 2022 17:03:56 +0300 Subject: [PATCH 05/71] Refactor poll item factory to make it testable. --- .../timeline/factory/MessageItemFactory.kt | 161 +----------------- .../factory/MessageItemFactoryHelper.kt | 82 +++++++++ .../timeline/factory/PollItemFactory.kt | 136 +++++++++++++++ 3 files changed, 223 insertions(+), 156 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactoryHelper.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 54bfbdd8a0..fba6ffbe51 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -16,13 +16,8 @@ package im.vector.app.features.home.room.detail.timeline.factory -import android.text.Spannable import android.text.SpannableStringBuilder import android.text.Spanned -import android.text.TextPaint -import android.text.style.AbsoluteSizeSpan -import android.text.style.ClickableSpan -import android.text.style.ForegroundColorSpan import android.view.View import dagger.Lazy import im.vector.app.R @@ -35,6 +30,7 @@ import im.vector.app.core.time.Clock import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.containsOnlyEmojis import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.factory.MessageItemFactoryHelper.annotateWithEdited import im.vector.app.features.home.room.detail.timeline.helper.AudioMessagePlaybackTracker import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder @@ -57,14 +53,6 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_ -import im.vector.app.features.home.room.detail.timeline.item.PollItem -import im.vector.app.features.home.room.detail.timeline.item.PollItem_ -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollEnded -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollReady -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollSending -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollUndisclosed -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollVoted -import im.vector.app.features.home.room.detail.timeline.item.PollResponseData import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_ import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem @@ -81,18 +69,11 @@ import im.vector.app.features.location.UrlMapProvider import im.vector.app.features.location.toLocationData import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.VideoContentRenderer -import im.vector.app.features.poll.PollState -import im.vector.app.features.poll.PollState.Ended -import im.vector.app.features.poll.PollState.Ready -import im.vector.app.features.poll.PollState.Sending -import im.vector.app.features.poll.PollState.Undisclosed -import im.vector.app.features.poll.PollState.Voted import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.voice.AudioWaveformView import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import me.gujun.android.span.span import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl -import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.attachments.toElementToDecrypt import org.matrix.android.sdk.api.session.events.model.RelationType @@ -113,8 +94,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent -import org.matrix.android.sdk.api.session.room.model.message.PollAnswer -import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.model.message.getFileUrl import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent @@ -149,6 +128,7 @@ class MessageItemFactory @Inject constructor( private val vectorPreferences: VectorPreferences, private val urlMapProvider: UrlMapProvider, private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory, + private val pollItemFactory: PollItemFactory, ) { // TODO inject this properly? @@ -208,7 +188,7 @@ class MessageItemFactory @Inject constructor( is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes) is MessageAudioContent -> buildAudioContent(params, messageContent, informationData, highlight, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes) + is MessagePollContent -> pollItemFactory.create(messageContent, informationData, highlight, callback, attributes) is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes) is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) @@ -244,93 +224,6 @@ class MessageItemFactory @Inject constructor( .leftGuideline(avatarSizeProvider.leftGuideline) } - private fun buildPollItem( - pollContent: MessagePollContent, - informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes, - ): PollItem { - val pollResponseSummary = informationData.pollResponseAggregatedSummary - val pollState = createPollState(informationData, pollResponseSummary, pollContent) - val pollCreationInfo = pollContent.getBestPollCreationInfo() - val questionText = pollCreationInfo?.question?.getBestQuestion().orEmpty() - val question = createPollQuestion(informationData, questionText, callback) - val optionViewStates = pollCreationInfo?.answers?.mapToOptions(pollState, informationData) - val totalVotesText = createTotalVotesText(pollState, pollResponseSummary) - - return PollItem_() - .attributes(attributes) - .eventId(informationData.eventId) - .pollQuestion(question) - .canVote(pollState.isVotable()) - .totalVotesText(totalVotesText) - .optionViewStates(optionViewStates) - .edited(informationData.hasBeenEdited) - .highlighted(highlight) - .leftGuideline(avatarSizeProvider.leftGuideline) - .callback(callback) - } - - private fun createPollState( - informationData: MessageInformationData, - pollResponseSummary: PollResponseData?, - pollContent: MessagePollContent, - ): PollState = when { - !informationData.sendState.isSent() -> Sending - pollResponseSummary?.isClosed.orFalse() -> Ended - pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED -> Undisclosed - pollResponseSummary?.myVote?.isNotEmpty().orFalse() -> Voted(pollResponseSummary?.totalVotes ?: 0) - else -> Ready - } - - private fun List.mapToOptions( - pollState: PollState, - informationData: MessageInformationData, - ) = map { answer -> - val pollResponseSummary = informationData.pollResponseAggregatedSummary - val winnerVoteCount = pollResponseSummary?.winnerVoteCount - val optionId = answer.id ?: "" - val optionAnswer = answer.getBestAnswer() ?: "" - val voteSummary = pollResponseSummary?.votes?.get(answer.id) - val voteCount = voteSummary?.total ?: 0 - val votePercentage = voteSummary?.percentage ?: 0.0 - val isMyVote = pollResponseSummary?.myVote == answer.id - val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount - - when (pollState) { - Sending -> PollSending(optionId, optionAnswer) - Ready -> PollReady(optionId, optionAnswer) - is Voted -> PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote) - Undisclosed -> PollUndisclosed(optionId, optionAnswer, isMyVote) - Ended -> PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner) - } - } - - private fun createPollQuestion( - informationData: MessageInformationData, - question: String, - callback: TimelineEventController.Callback?, - ) = if (informationData.hasBeenEdited) { - annotateWithEdited(question, callback, informationData) - } else { - question - }.toEpoxyCharSequence() - - private fun createTotalVotesText( - pollState: PollState, - pollResponseSummary: PollResponseData?, - ): String { - val votes = pollResponseSummary?.totalVotes ?: 0 - return when { - pollState is Ended -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, votes, votes) - pollState is Undisclosed -> "" - pollState is Voted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, votes, votes) - votes == 0 -> stringProvider.getString(R.string.poll_no_votes_cast) - else -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, votes, votes) - } - } - private fun buildAudioMessageItem( params: TimelineItemFactoryParams, messageContent: MessageAudioContent, @@ -627,7 +520,7 @@ class MessageItemFactory @Inject constructor( return MessageTextItem_() .message( if (informationData.hasBeenEdited) { - annotateWithEdited(linkifiedBody, callback, informationData) + annotateWithEdited(stringProvider, colorProvider, dimensionConverter, linkifiedBody, callback, informationData) } else { linkifiedBody }.toEpoxyCharSequence() @@ -645,50 +538,6 @@ class MessageItemFactory @Inject constructor( .movementMethod(createLinkMovementMethod(callback)) } - private fun annotateWithEdited( - linkifiedBody: CharSequence, - callback: TimelineEventController.Callback?, - informationData: MessageInformationData, - ): Spannable { - val spannable = SpannableStringBuilder() - spannable.append(linkifiedBody) - val editedSuffix = stringProvider.getString(R.string.edited_suffix) - spannable.append(" ").append(editedSuffix) - val color = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) - val editStart = spannable.lastIndexOf(editedSuffix) - val editEnd = editStart + editedSuffix.length - spannable.setSpan( - ForegroundColorSpan(color), - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ) - - // Note: text size is set to 14sp - spannable.setSpan( - AbsoluteSizeSpan(dimensionConverter.spToPx(13)), - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ) - - spannable.setSpan( - object : ClickableSpan() { - override fun onClick(widget: View) { - callback?.onEditedDecorationClicked(informationData) - } - - override fun updateDrawState(ds: TextPaint) { - // nop - } - }, - editStart, - editEnd, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ) - return spannable - } - private fun buildNoticeMessageItem( messageContent: MessageNoticeContent, @Suppress("UNUSED_PARAMETER") @@ -735,7 +584,7 @@ class MessageItemFactory @Inject constructor( return MessageTextItem_() .message( if (informationData.hasBeenEdited) { - annotateWithEdited(message, callback, informationData) + annotateWithEdited(stringProvider, colorProvider, dimensionConverter, message, callback, informationData) } else { message }.toEpoxyCharSequence() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactoryHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactoryHelper.kt new file mode 100644 index 0000000000..0c4c7238e7 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactoryHelper.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2022 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.factory + +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.TextPaint +import android.text.style.AbsoluteSizeSpan +import android.text.style.ClickableSpan +import android.text.style.ForegroundColorSpan +import android.view.View +import im.vector.app.R +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData + +object MessageItemFactoryHelper { + + fun annotateWithEdited( + stringProvider: StringProvider, + colorProvider: ColorProvider, + dimensionConverter: DimensionConverter, + linkifiedBody: CharSequence, + callback: TimelineEventController.Callback?, + informationData: MessageInformationData, + ): Spannable { + val spannable = SpannableStringBuilder() + spannable.append(linkifiedBody) + val editedSuffix = stringProvider.getString(R.string.edited_suffix) + spannable.append(" ").append(editedSuffix) + val color = colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary) + val editStart = spannable.lastIndexOf(editedSuffix) + val editEnd = editStart + editedSuffix.length + spannable.setSpan( + ForegroundColorSpan(color), + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + + // Note: text size is set to 14sp + spannable.setSpan( + AbsoluteSizeSpan(dimensionConverter.spToPx(13)), + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + + spannable.setSpan( + object : ClickableSpan() { + override fun onClick(widget: View) { + callback?.onEditedDecorationClicked(informationData) + } + + override fun updateDrawState(ds: TextPaint) { + // nop + } + }, + editStart, + editEnd, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + return spannable + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt new file mode 100644 index 0000000000..09b601f080 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2022 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.factory + +import androidx.annotation.VisibleForTesting +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.factory.MessageItemFactoryHelper.annotateWithEdited +import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider +import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem +import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.app.features.home.room.detail.timeline.item.PollItem_ +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState +import im.vector.app.features.home.room.detail.timeline.item.PollResponseData +import im.vector.app.features.poll.PollState +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.model.message.PollAnswer +import org.matrix.android.sdk.api.session.room.model.message.PollType +import javax.inject.Inject + +class PollItemFactory @Inject constructor( + private val stringProvider: StringProvider, + private val avatarSizeProvider: AvatarSizeProvider, + private val colorProvider: ColorProvider, + private val dimensionConverter: DimensionConverter, +) { + + fun create( + pollContent: MessagePollContent, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes, + ): VectorEpoxyModel<*>? { + val pollResponseSummary = informationData.pollResponseAggregatedSummary + val pollState = createPollState(informationData, pollResponseSummary, pollContent) + val pollCreationInfo = pollContent.getBestPollCreationInfo() + val questionText = pollCreationInfo?.question?.getBestQuestion().orEmpty() + val question = createPollQuestion(informationData, questionText, callback) + val optionViewStates = pollCreationInfo?.answers?.mapToOptions(pollState, informationData) + val totalVotesText = createTotalVotesText(pollState, pollResponseSummary) + + return PollItem_() + .attributes(attributes) + .eventId(informationData.eventId) + .pollQuestion(question) + .canVote(pollState.isVotable()) + .totalVotesText(totalVotesText) + .optionViewStates(optionViewStates) + .edited(informationData.hasBeenEdited) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) + .callback(callback) + } + + @VisibleForTesting + private fun createPollState( + informationData: MessageInformationData, + pollResponseSummary: PollResponseData?, + pollContent: MessagePollContent, + ): PollState = when { + !informationData.sendState.isSent() -> PollState.Sending + pollResponseSummary?.isClosed.orFalse() -> PollState.Ended + pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED -> PollState.Undisclosed + pollResponseSummary?.myVote?.isNotEmpty().orFalse() -> PollState.Voted(pollResponseSummary?.totalVotes ?: 0) + else -> PollState.Ready + } + + @VisibleForTesting + private fun List.mapToOptions( + pollState: PollState, + informationData: MessageInformationData, + ) = map { answer -> + val pollResponseSummary = informationData.pollResponseAggregatedSummary + val winnerVoteCount = pollResponseSummary?.winnerVoteCount + val optionId = answer.id ?: "" + val optionAnswer = answer.getBestAnswer() ?: "" + val voteSummary = pollResponseSummary?.votes?.get(answer.id) + val voteCount = voteSummary?.total ?: 0 + val votePercentage = voteSummary?.percentage ?: 0.0 + val isMyVote = pollResponseSummary?.myVote == answer.id + val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount + + when (pollState) { + PollState.Sending -> PollOptionViewState.PollSending(optionId, optionAnswer) + PollState.Ready -> PollOptionViewState.PollReady(optionId, optionAnswer) + is PollState.Voted -> PollOptionViewState.PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote) + PollState.Undisclosed -> PollOptionViewState.PollUndisclosed(optionId, optionAnswer, isMyVote) + PollState.Ended -> PollOptionViewState.PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner) + } + } + + private fun createPollQuestion( + informationData: MessageInformationData, + question: String, + callback: TimelineEventController.Callback?, + ) = if (informationData.hasBeenEdited) { + annotateWithEdited(stringProvider, colorProvider, dimensionConverter, question, callback, informationData) + } else { + question + }.toEpoxyCharSequence() + + private fun createTotalVotesText( + pollState: PollState, + pollResponseSummary: PollResponseData?, + ): String { + val votes = pollResponseSummary?.totalVotes ?: 0 + return when { + pollState is PollState.Ended -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, votes, votes) + pollState is PollState.Undisclosed -> "" + pollState is PollState.Voted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, votes, votes) + votes == 0 -> stringProvider.getString(R.string.poll_no_votes_cast) + else -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, votes, votes) + } + } +} From 77dfd5f8264dc9ce4e457c41755dbd8f57692ff3 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 21 Jun 2022 17:26:26 +0300 Subject: [PATCH 06/71] Create initial test class. --- .../timeline/factory/PollItemFactoryTest.kt | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt new file mode 100644 index 0000000000..70268a70f8 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022 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.factory + +import com.airbnb.mvrx.test.MvRxTestRule +import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryData +import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout +import io.mockk.mockk +import io.mockk.unmockkAll +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.matrix.android.sdk.api.session.room.send.SendState + +private val A_MESSAGE_INFORMATION_DATA = MessageInformationData( + eventId = "eventId", + senderId = "senderId", + ageLocalTS = 0, + avatarUrl = "", + sendState = SendState.SENDING, + messageLayout = TimelineMessageLayout.Default(showAvatar = true, showDisplayName = true, showTimestamp = true), + reactionsSummary = ReactionsSummaryData(), + sentByMe = true, +) + +class PollItemFactoryTest { + + private val testDispatcher = UnconfinedTestDispatcher() + + @get:Rule + val mvRxTestRule = MvRxTestRule( + testDispatcher = testDispatcher // See https://github.com/airbnb/mavericks/issues/599 + ) + + private lateinit var pollItemFactory: PollItemFactory + + @Before + fun setup() { + // We are not going to test any UI related code + pollItemFactory = PollItemFactory( + stringProvider = mockk(), + avatarSizeProvider = mockk(), + colorProvider = mockk(), + dimensionConverter = mockk(), + ) + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given ` = runTest { + + } +} From a886e93c7ea0522687d825645538e230abd0bb09 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 12:13:53 +0300 Subject: [PATCH 07/71] Test sending poll state. --- .../room/detail/timeline/factory/PollItemFactory.kt | 8 ++++---- .../detail/timeline/factory/PollItemFactoryTest.kt | 11 +++++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt index 09b601f080..dbed274838 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt @@ -73,8 +73,8 @@ class PollItemFactory @Inject constructor( .callback(callback) } - @VisibleForTesting - private fun createPollState( + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun createPollState( informationData: MessageInformationData, pollResponseSummary: PollResponseData?, pollContent: MessagePollContent, @@ -86,8 +86,8 @@ class PollItemFactory @Inject constructor( else -> PollState.Ready } - @VisibleForTesting - private fun List.mapToOptions( + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun List.mapToOptions( pollState: PollState, informationData: MessageInformationData, ) = map { answer -> diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index 70268a70f8..5fab6a6b0a 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -20,14 +20,17 @@ import com.airbnb.mvrx.test.MvRxTestRule import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryData import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout +import im.vector.app.features.poll.PollState import io.mockk.mockk import io.mockk.unmockkAll import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBe import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.send.SendState private val A_MESSAGE_INFORMATION_DATA = MessageInformationData( @@ -69,7 +72,11 @@ class PollItemFactoryTest { } @Test - fun `given ` = runTest { - + fun `given a sending poll state then returns PollState as Sending`() = runTest { + pollItemFactory.createPollState( + informationData = A_MESSAGE_INFORMATION_DATA, + pollResponseSummary = null, + pollContent = MessagePollContent() + ) shouldBe PollState.Sending } } From 8854b81977ed65208f0a6a8a7e8dc5d243003cb6 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 12:34:52 +0300 Subject: [PATCH 08/71] Test ended poll state. --- .../timeline/factory/PollItemFactoryTest.kt | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index 5fab6a6b0a..d27736043b 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.factory import com.airbnb.mvrx.test.MvRxTestRule import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.app.features.home.room.detail.timeline.item.PollResponseData import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryData import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout import im.vector.app.features.poll.PollState @@ -38,12 +39,17 @@ private val A_MESSAGE_INFORMATION_DATA = MessageInformationData( senderId = "senderId", ageLocalTS = 0, avatarUrl = "", - sendState = SendState.SENDING, + sendState = SendState.SENT, messageLayout = TimelineMessageLayout.Default(showAvatar = true, showDisplayName = true, showTimestamp = true), reactionsSummary = ReactionsSummaryData(), sentByMe = true, ) +private val A_POLL_RESPONSE_DATA = PollResponseData( + myVote = null, + votes = emptyMap(), +) + class PollItemFactoryTest { private val testDispatcher = UnconfinedTestDispatcher() @@ -72,11 +78,23 @@ class PollItemFactoryTest { } @Test - fun `given a sending poll state then returns PollState as Sending`() = runTest { + fun `given a sending poll state then PollState is Sending`() = runTest { + val sendingPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(sendState = SendState.SENT) pollItemFactory.createPollState( - informationData = A_MESSAGE_INFORMATION_DATA, - pollResponseSummary = null, + informationData = sendingPollInformationData, + pollResponseSummary = A_POLL_RESPONSE_DATA, pollContent = MessagePollContent() ) shouldBe PollState.Sending } + + @Test + fun `given a sent poll state when poll is closed then PollState is Ended`() = runTest { + val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true) + + pollItemFactory.createPollState( + informationData = A_MESSAGE_INFORMATION_DATA, + pollResponseSummary = closedPollSummary, + pollContent = MessagePollContent() + ) shouldBe PollState.Ended + } } From 0fe4b9f07febc6308ab6805e6fd1c94d02ad65ec Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 12:57:50 +0300 Subject: [PATCH 09/71] Test undisclosed poll state. --- .../room/model/message/PollCreationInfo.kt | 5 ++- .../timeline/factory/PollItemFactory.kt | 2 +- .../timeline/factory/PollItemFactoryTest.kt | 41 ++++++++++++++++++- 3 files changed, 44 insertions(+), 4 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt index 81b034a809..ee31d5959e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/PollCreationInfo.kt @@ -25,4 +25,7 @@ data class PollCreationInfo( @Json(name = "kind") val kind: PollType? = PollType.DISCLOSED_UNSTABLE, @Json(name = "max_selections") val maxSelections: Int = 1, @Json(name = "answers") val answers: List? = null -) +) { + + fun isUndisclosed() = kind in listOf(PollType.UNDISCLOSED_UNSTABLE, PollType.UNDISCLOSED) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt index dbed274838..842eeb65d8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt @@ -81,7 +81,7 @@ class PollItemFactory @Inject constructor( ): PollState = when { !informationData.sendState.isSent() -> PollState.Sending pollResponseSummary?.isClosed.orFalse() -> PollState.Ended - pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED -> PollState.Undisclosed + pollContent.getBestPollCreationInfo()?.isUndisclosed().orFalse() -> PollState.Undisclosed pollResponseSummary?.myVote?.isNotEmpty().orFalse() -> PollState.Voted(pollResponseSummary?.totalVotes ?: 0) else -> PollState.Ready } diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index d27736043b..051386007f 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -32,6 +32,10 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.model.message.PollAnswer +import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo +import org.matrix.android.sdk.api.session.room.model.message.PollQuestion +import org.matrix.android.sdk.api.session.room.model.message.PollType import org.matrix.android.sdk.api.session.room.send.SendState private val A_MESSAGE_INFORMATION_DATA = MessageInformationData( @@ -50,6 +54,30 @@ private val A_POLL_RESPONSE_DATA = PollResponseData( votes = emptyMap(), ) +private val A_POLL_CONTENT = MessagePollContent( + unstablePollCreationInfo = PollCreationInfo( + question = PollQuestion( + unstableQuestion = "What is your favourite coffee?" + ), + kind = PollType.UNDISCLOSED_UNSTABLE, + maxSelections = 1, + answers = listOf( + PollAnswer( + id = "5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", + unstableAnswer = "Double Espresso" + ), + PollAnswer( + id = "ec1a4db0-46d8-4d7a-9bb6-d80724715938", + unstableAnswer = "Macchiato" + ), + PollAnswer( + id = "3677ca8e-061b-40ab-bffe-b22e4e88fcad", + unstableAnswer = "Iced Coffee" + ), + ) + ) +) + class PollItemFactoryTest { private val testDispatcher = UnconfinedTestDispatcher() @@ -83,7 +111,7 @@ class PollItemFactoryTest { pollItemFactory.createPollState( informationData = sendingPollInformationData, pollResponseSummary = A_POLL_RESPONSE_DATA, - pollContent = MessagePollContent() + pollContent = A_POLL_CONTENT ) shouldBe PollState.Sending } @@ -94,7 +122,16 @@ class PollItemFactoryTest { pollItemFactory.createPollState( informationData = A_MESSAGE_INFORMATION_DATA, pollResponseSummary = closedPollSummary, - pollContent = MessagePollContent() + pollContent = A_POLL_CONTENT ) shouldBe PollState.Ended } + + @Test + fun `given a sent poll when undisclosed poll type is selected then PollState is Undisclosed`() = runTest { + pollItemFactory.createPollState( + informationData = A_MESSAGE_INFORMATION_DATA, + pollResponseSummary = A_POLL_RESPONSE_DATA, + pollContent = A_POLL_CONTENT + ) shouldBe PollState.Undisclosed + } } From 2c5ddca8219bf8e56cae7dabc8bb0f078316aef1 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 13:21:50 +0300 Subject: [PATCH 10/71] Test voted poll state. --- .../timeline/factory/PollItemFactoryTest.kt | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index 051386007f..50ddb0afb9 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -27,6 +27,7 @@ import io.mockk.unmockkAll import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBe +import org.amshove.kluent.shouldBeEqualTo import org.junit.After import org.junit.Before import org.junit.Rule @@ -107,11 +108,11 @@ class PollItemFactoryTest { @Test fun `given a sending poll state then PollState is Sending`() = runTest { - val sendingPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(sendState = SendState.SENT) + val sendingPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(sendState = SendState.SENDING) pollItemFactory.createPollState( informationData = sendingPollInformationData, pollResponseSummary = A_POLL_RESPONSE_DATA, - pollContent = A_POLL_CONTENT + pollContent = A_POLL_CONTENT, ) shouldBe PollState.Sending } @@ -122,7 +123,7 @@ class PollItemFactoryTest { pollItemFactory.createPollState( informationData = A_MESSAGE_INFORMATION_DATA, pollResponseSummary = closedPollSummary, - pollContent = A_POLL_CONTENT + pollContent = A_POLL_CONTENT, ) shouldBe PollState.Ended } @@ -131,7 +132,26 @@ class PollItemFactoryTest { pollItemFactory.createPollState( informationData = A_MESSAGE_INFORMATION_DATA, pollResponseSummary = A_POLL_RESPONSE_DATA, - pollContent = A_POLL_CONTENT + pollContent = A_POLL_CONTENT, ) shouldBe PollState.Undisclosed } + + @Test + fun `given a sent poll when my vote exists then PollState is Voted`() = runTest { + val votedPollData = A_POLL_RESPONSE_DATA.copy( + totalVotes = 1, + myVote = "5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", + ) + val disclosedPollContent = A_POLL_CONTENT.copy( + unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( + kind = PollType.DISCLOSED_UNSTABLE + ) + ) + + pollItemFactory.createPollState( + informationData = A_MESSAGE_INFORMATION_DATA, + pollResponseSummary = votedPollData, + pollContent = disclosedPollContent, + ) shouldBeEqualTo PollState.Voted(1) + } } From 5a948891f0f8b6b6524d1bde06a81c4fdfd57ffe Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 13:27:52 +0300 Subject: [PATCH 11/71] Test ready poll state. --- .../timeline/factory/PollItemFactoryTest.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index 50ddb0afb9..156cde6ff7 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -154,4 +154,19 @@ class PollItemFactoryTest { pollContent = disclosedPollContent, ) shouldBeEqualTo PollState.Voted(1) } + + @Test + fun `given a sent poll when poll type is disclosed then PollState is Ready`() = runTest { + val disclosedPollContent = A_POLL_CONTENT.copy( + unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( + kind = PollType.DISCLOSED_UNSTABLE + ) + ) + + pollItemFactory.createPollState( + informationData = A_MESSAGE_INFORMATION_DATA, + pollResponseSummary = A_POLL_RESPONSE_DATA, + pollContent = disclosedPollContent, + ) shouldBe PollState.Ready + } } From 2cf40cbcf2a9b1e27f48a08210211721fdfd3a33 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 14:05:42 +0300 Subject: [PATCH 12/71] Test sending option view states. --- .../detail/timeline/factory/PollItemFactory.kt | 1 - .../timeline/factory/PollItemFactoryTest.kt | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt index 842eeb65d8..05e945c193 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt @@ -35,7 +35,6 @@ import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.PollAnswer -import org.matrix.android.sdk.api.session.room.model.message.PollType import javax.inject.Inject class PollItemFactory @Inject constructor( diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index 156cde6ff7..d7cd757a39 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.factory import com.airbnb.mvrx.test.MvRxTestRule import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState import im.vector.app.features.home.room.detail.timeline.item.PollResponseData import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryData import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout @@ -169,4 +170,19 @@ class PollItemFactoryTest { pollContent = disclosedPollContent, ) shouldBe PollState.Ready } + + @Test + fun `given a sending poll then all option view states is PollSending`() = runTest { + with(pollItemFactory) { + A_POLL_CONTENT + .getBestPollCreationInfo() + ?.answers + ?.mapToOptions(PollState.Sending, A_MESSAGE_INFORMATION_DATA) + ?.forEachIndexed { index, pollOptionViewState -> + A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> + pollOptionViewState shouldBeEqualTo PollOptionViewState.PollSending(option.id ?: "", option.getBestAnswer() ?: "") + } + } + } + } } From 0f0492db3b21eaf69daac418560d6f22e17d4436 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 15:27:04 +0300 Subject: [PATCH 13/71] Test ready option view states. --- .../timeline/factory/PollItemFactoryTest.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index d7cd757a39..02a24d8ca7 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -185,4 +185,19 @@ class PollItemFactoryTest { } } } + + @Test + fun `given a sent poll then all option view states is PollReady`() = runTest { + with(pollItemFactory) { + A_POLL_CONTENT + .getBestPollCreationInfo() + ?.answers + ?.mapToOptions(PollState.Sending, A_MESSAGE_INFORMATION_DATA) + ?.forEachIndexed { index, pollOptionViewState -> + A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> + pollOptionViewState shouldBeEqualTo PollOptionViewState.PollSending(option.id ?: "", option.getBestAnswer() ?: "") + } + } + } + } } From 8bb421a916afc43f6d2cad996d7e12935180e8b8 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 15:44:28 +0300 Subject: [PATCH 14/71] Test poll voted option view states. --- .../timeline/factory/PollItemFactoryTest.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index 02a24d8ca7..acd84eb4b0 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -200,4 +200,26 @@ class PollItemFactoryTest { } } } + + @Test + fun `given a sent poll when a vote is cast then all option view states is PollVoted`() = runTest { + with(pollItemFactory) { + A_POLL_CONTENT + .getBestPollCreationInfo() + ?.answers + ?.mapToOptions(PollState.Voted(1), A_MESSAGE_INFORMATION_DATA) + ?.forEachIndexed { index, pollOptionViewState -> + A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> + val voteSummary = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.votes?.get(option.id) + pollOptionViewState shouldBeEqualTo PollOptionViewState.PollVoted( + optionId = option.id ?: "", + optionAnswer = option.getBestAnswer() ?: "", + voteCount = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.totalVotes ?: 0, + votePercentage = voteSummary?.percentage ?: 0.0, + isSelected = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.myVote == option.id, + ) + } + } + } + } } From d0d2929a84e4b27f37a19f02a59620bee0b14587 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 16:29:19 +0300 Subject: [PATCH 15/71] Test undisclosed option view states. --- .../timeline/factory/PollItemFactoryTest.kt | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index acd84eb4b0..8209f992e0 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -222,4 +222,23 @@ class PollItemFactoryTest { } } } + + @Test + fun `given a sent poll when the poll is undisclosed then all option view states is PollUndisclosed`() = runTest { + with(pollItemFactory) { + A_POLL_CONTENT + .getBestPollCreationInfo() + ?.answers + ?.mapToOptions(PollState.Undisclosed, A_MESSAGE_INFORMATION_DATA) + ?.forEachIndexed { index, pollOptionViewState -> + A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> + pollOptionViewState shouldBeEqualTo PollOptionViewState.PollUndisclosed( + optionId = option.id ?: "", + optionAnswer = option.getBestAnswer() ?: "", + isSelected = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.myVote == option.id, + ) + } + } + } + } } From aab558af0926ae89f4ebdd68bf462ae7631e08a9 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 16:40:11 +0300 Subject: [PATCH 16/71] Test ended poll option view states. --- .../timeline/factory/PollItemFactoryTest.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index 8209f992e0..6c4f9e872e 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -241,4 +241,28 @@ class PollItemFactoryTest { } } } + + @Test + fun `given an ended poll then all option view states is Ended`() = runTest { + with(pollItemFactory) { + A_POLL_CONTENT + .getBestPollCreationInfo() + ?.answers + ?.mapToOptions(PollState.Ended, A_MESSAGE_INFORMATION_DATA) + ?.forEachIndexed { index, pollOptionViewState -> + A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> + val voteSummary = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.votes?.get(option.id) + val voteCount = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.totalVotes ?: 0 + val winnerVoteCount = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.winnerVoteCount ?: 0 + pollOptionViewState shouldBeEqualTo PollOptionViewState.PollEnded( + optionId = option.id ?: "", + optionAnswer = option.getBestAnswer() ?: "", + voteCount = voteCount, + votePercentage = voteSummary?.percentage ?: 0.0, + isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount, + ) + } + } + } + } } From a7bc2ef3bc8451e1a09df2fd69af95c1acc81c56 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 16:45:22 +0300 Subject: [PATCH 17/71] Changelog added. --- changelog.d/6366.misc | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6366.misc diff --git a/changelog.d/6366.misc b/changelog.d/6366.misc new file mode 100644 index 0000000000..5752b3d700 --- /dev/null +++ b/changelog.d/6366.misc @@ -0,0 +1 @@ +Poll view state unit tests From 2be43e929435ff665167c574e89ff0e94896db91 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 22 Jun 2022 17:18:17 +0300 Subject: [PATCH 18/71] Test isVotable function. --- .../detail/timeline/factory/PollItemFactoryTest.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt index 6c4f9e872e..be397e25ea 100644 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt @@ -265,4 +265,18 @@ class PollItemFactoryTest { } } } + + @Test + fun `given a poll state when it is not Sending and not Ended then the poll is votable`() = runTest { + val sendingPollState = PollState.Sending + sendingPollState.isVotable() shouldBe false + val readyPollState = PollState.Ready + readyPollState.isVotable() shouldBe true + val votedPollState = PollState.Voted(1) + votedPollState.isVotable() shouldBe true + val undisclosedPollState = PollState.Undisclosed + undisclosedPollState.isVotable() shouldBe true + var endedPollState = PollState.Ended + endedPollState.isVotable() shouldBe false + } } From fb4c83b3c4ca0af7c6fc68aa8de4ab18b708fb84 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jun 2022 08:33:40 +0000 Subject: [PATCH 19/71] Bump dokka-gradle-plugin from 1.6.21 to 1.7.0 Bumps dokka-gradle-plugin from 1.6.21 to 1.7.0. --- updated-dependencies: - dependency-name: org.jetbrains.dokka:dokka-gradle-plugin dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index bfa8f1c9a9..882008e2e0 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ buildscript { classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5' classpath "com.likethesalad.android:stem-plugin:2.1.1" classpath 'org.owasp:dependency-check-gradle:7.1.1' - classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.6.21" + classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.0" classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files From a4cae9ef077eea29f047b5be5953a4e85f395449 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 23 Jun 2022 17:40:16 +0200 Subject: [PATCH 20/71] Fixing missing "u=" in geo URI pattern for uncertainty --- .../room/model/message/LocationInfo.kt | 2 +- .../model/message/MessageLocationContent.kt | 2 +- .../room/send/LocalEchoEventFactory.kt | 4 +-- .../LiveLocationAggregationProcessorTest.kt | 2 +- .../app/features/location/LocationData.kt | 4 +-- .../app/features/location/LocationDataTest.kt | 33 ++++++++++--------- .../UserLiveLocationViewStateMapperTest.kt | 2 +- 7 files changed, 26 insertions(+), 23 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt index a1fd3bd2ec..3c603962cb 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationInfo.kt @@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class LocationInfo( /** - * Required. RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30' representing this location. + * Required. RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;u=30' representing this location. */ @Json(name = "uri") val geoUri: String? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt index 0a66a6e400..1275642b5f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt @@ -35,7 +35,7 @@ data class MessageLocationContent( @Json(name = "body") override val body: String, /** - * Required. RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30' representing this location. + * Required. RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;u=30' representing this location. */ @Json(name = "geo_uri") val geoUri: String, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt index bcaa257d78..f52500de1b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt @@ -708,7 +708,7 @@ internal class LocalEchoEventFactory @Inject constructor( } /** - * Returns RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30' + * Returns RFC5870 formatted geo uri 'geo:latitude,longitude;u=uncertainty' like 'geo:40.05,29.24;u=30' * Uncertainty of the location is in meters and not required. */ private fun buildGeoUri(latitude: Double, longitude: Double, uncertainty: Double?): String { @@ -718,7 +718,7 @@ internal class LocalEchoEventFactory @Inject constructor( append(",") append(longitude) uncertainty?.let { - append(";") + append(";u=") append(it) } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt index e6d63f5e5e..933087af2b 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/aggregation/livelocation/LiveLocationAggregationProcessorTest.kt @@ -46,7 +46,7 @@ private const val A_TIMEOUT_MILLIS = 15 * 60 * 1000L private const val A_LATITUDE = 40.05 private const val A_LONGITUDE = 29.24 private const val A_UNCERTAINTY = 30.0 -private const val A_GEO_URI = "geo:$A_LATITUDE,$A_LONGITUDE;$A_UNCERTAINTY" +private const val A_GEO_URI = "geo:$A_LATITUDE,$A_LONGITUDE;u=$A_UNCERTAINTY" internal class LiveLocationAggregationProcessorTest { diff --git a/vector/src/main/java/im/vector/app/features/location/LocationData.kt b/vector/src/main/java/im/vector/app/features/location/LocationData.kt index b3466ff871..3c25a5b398 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationData.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationData.kt @@ -30,7 +30,7 @@ data class LocationData( /** * Creates location data from a MessageLocationContent. - * "geo:40.05,29.24;30" -> LocationData(40.05, 29.24, 30) + * "geo:40.05,29.24;u=30" -> LocationData(40.05, 29.24, 30) * @return location data or null if geo uri is not valid */ fun MessageLocationContent.toLocationData(): LocationData? { @@ -39,7 +39,7 @@ fun MessageLocationContent.toLocationData(): LocationData? { /** * Creates location data from a geoUri String. - * "geo:40.05,29.24;30" -> LocationData(40.05, 29.24, 30) + * "geo:40.05,29.24;u=30" -> LocationData(40.05, 29.24, 30) * @return location data or null if geo uri is null or not valid */ fun String?.toLocationData(): LocationData? { diff --git a/vector/src/test/java/im/vector/app/features/location/LocationDataTest.kt b/vector/src/test/java/im/vector/app/features/location/LocationDataTest.kt index 6b97b715db..d3a1e79922 100644 --- a/vector/src/test/java/im/vector/app/features/location/LocationDataTest.kt +++ b/vector/src/test/java/im/vector/app/features/location/LocationDataTest.kt @@ -28,19 +28,22 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageLocationCont class LocationDataTest { @Test fun validCases() { - parseGeo("geo:12.34,56.78;13.56") shouldBeEqualTo + parseGeo("geo:12.34,56.78;u=13.56") shouldBeEqualTo LocationData(latitude = 12.34, longitude = 56.78, uncertainty = 13.56) parseGeo("geo:12.34,56.78") shouldBeEqualTo LocationData(latitude = 12.34, longitude = 56.78, uncertainty = null) // Error is ignored in case of invalid uncertainty - parseGeo("geo:12.34,56.78;13.5z6") shouldBeEqualTo + parseGeo("geo:12.34,56.78;u=13.5z6") shouldBeEqualTo LocationData(latitude = 12.34, longitude = 56.78, uncertainty = null) - parseGeo("geo:12.34,56.78;13. 56") shouldBeEqualTo + parseGeo("geo:12.34,56.78;u=13. 56") shouldBeEqualTo LocationData(latitude = 12.34, longitude = 56.78, uncertainty = null) // Space are ignored (trim) - parseGeo("geo: 12.34,56.78;13.56") shouldBeEqualTo + parseGeo("geo: 12.34,56.78;u=13.56") shouldBeEqualTo LocationData(latitude = 12.34, longitude = 56.78, uncertainty = 13.56) - parseGeo("geo:12.34,56.78; 13.56") shouldBeEqualTo + parseGeo("geo:12.34,56.78; u=13.56") shouldBeEqualTo + LocationData(latitude = 12.34, longitude = 56.78, uncertainty = 13.56) + // missing "u=" for uncertainty is ignored + parseGeo("geo:12.34,56.78;13.56") shouldBeEqualTo LocationData(latitude = 12.34, longitude = 56.78, uncertainty = 13.56) } @@ -50,17 +53,17 @@ class LocationDataTest { parseGeo("geo").shouldBeNull() parseGeo("geo:").shouldBeNull() parseGeo("geo:12.34").shouldBeNull() - parseGeo("geo:12.34;13.56").shouldBeNull() - parseGeo("gea:12.34,56.78;13.56").shouldBeNull() - parseGeo("geo:12.x34,56.78;13.56").shouldBeNull() - parseGeo("geo:12.34,56.7y8;13.56").shouldBeNull() + parseGeo("geo:12.34;u=13.56").shouldBeNull() + parseGeo("gea:12.34,56.78;u=13.56").shouldBeNull() + parseGeo("geo:12.x34,56.78;u=13.56").shouldBeNull() + parseGeo("geo:12.34,56.7y8;u=13.56").shouldBeNull() // Spaces are not ignored if inside the numbers - parseGeo("geo:12.3 4,56.78;13.56").shouldBeNull() - parseGeo("geo:12.34,56.7 8;13.56").shouldBeNull() + parseGeo("geo:12.3 4,56.78;u=13.56").shouldBeNull() + parseGeo("geo:12.34,56.7 8;u=13.56").shouldBeNull() // Or in the protocol part - parseGeo(" geo:12.34,56.78;13.56").shouldBeNull() - parseGeo("ge o:12.34,56.78;13.56").shouldBeNull() - parseGeo("geo :12.34,56.78;13.56").shouldBeNull() + parseGeo(" geo:12.34,56.78;u=13.56").shouldBeNull() + parseGeo("ge o:12.34,56.78;u=13.56").shouldBeNull() + parseGeo("geo :12.34,56.78;u=13.56").shouldBeNull() } @Test @@ -77,7 +80,7 @@ class LocationDataTest { @Test fun unstablePrefixTest() { - val geoUri = "geo :12.34,56.78;13.56" + val geoUri = "geo :12.34,56.78;u=13.56" val contentWithUnstablePrefixes = MessageLocationContent(body = "", geoUri = "", unstableLocationInfo = LocationInfo(geoUri = geoUri)) contentWithUnstablePrefixes.getBestLocationInfo()?.geoUri.shouldBeEqualTo(geoUri) diff --git a/vector/src/test/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapperTest.kt b/vector/src/test/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapperTest.kt index 95e0ff1b0b..46742da874 100644 --- a/vector/src/test/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapperTest.kt +++ b/vector/src/test/java/im/vector/app/features/location/live/map/UserLiveLocationViewStateMapperTest.kt @@ -46,7 +46,7 @@ private const val A_LOCATION_TIMESTAMP = 122L private const val A_LATITUDE = 40.05 private const val A_LONGITUDE = 29.24 private const val A_UNCERTAINTY = 30.0 -private const val A_GEO_URI = "geo:$A_LATITUDE,$A_LONGITUDE;$A_UNCERTAINTY" +private const val A_GEO_URI = "geo:$A_LATITUDE,$A_LONGITUDE;u=$A_UNCERTAINTY" class UserLiveLocationViewStateMapperTest { From e26393b1b585ac2fcaa21dd43184cc7d4f6c5fdf Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 24 Jun 2022 09:40:52 +0200 Subject: [PATCH 21/71] Adding changelog entry --- changelog.d/6375.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6375.bugfix diff --git a/changelog.d/6375.bugfix b/changelog.d/6375.bugfix new file mode 100644 index 0000000000..769ed81e69 --- /dev/null +++ b/changelog.d/6375.bugfix @@ -0,0 +1 @@ +[Location Share] - Adding missing prefix "u=" for uncertainty in geo URI From 8406b2a4eb45a4c8fb93402f821ada046994e3a2 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 16 Jun 2022 12:04:37 +0200 Subject: [PATCH 22/71] Adding use case to stop live location share WIP --- .../home/room/detail/TimelineViewModel.kt | 14 ++++- .../location/LocationSharingService.kt | 27 ++------- .../LocationSharingServiceConnection.kt | 36 ++++++++--- .../live/StopLiveLocationShareUseCase.kt | 44 ++++++++++++++ .../live/map/LocationLiveMapViewModel.kt | 16 ++++- .../live/StopLiveLocationShareUseCaseTest.kt | 60 +++++++++++++++++++ .../GetListOfUserLiveLocationUseCaseTest.kt | 3 +- .../FakeLocationSharingServiceConnection.kt | 37 ++++++++++++ 8 files changed, 204 insertions(+), 33 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/location/live/StopLiveLocationShareUseCase.kt create mode 100644 vector/src/test/java/im/vector/app/features/location/live/StopLiveLocationShareUseCaseTest.kt create mode 100644 vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingServiceConnection.kt diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 1c2255246b..48f8aef421 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -53,6 +53,7 @@ import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.location.LocationSharingServiceConnection +import im.vector.app.features.location.live.StopLiveLocationShareUseCase import im.vector.app.features.notifications.NotificationDrawerManager import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.raw.wellknown.getOutboundSessionKeySharingStrategyOrDefault @@ -92,6 +93,7 @@ import org.matrix.android.sdk.api.session.file.FileService import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.getStateEvent import org.matrix.android.sdk.api.session.room.getTimelineEvent +import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.model.Membership @@ -133,8 +135,9 @@ class TimelineViewModel @AssistedInject constructor( private val decryptionFailureTracker: DecryptionFailureTracker, private val notificationDrawerManager: NotificationDrawerManager, private val locationSharingServiceConnection: LocationSharingServiceConnection, + private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase, timelineFactory: TimelineFactory, - appStateHandler: AppStateHandler + appStateHandler: AppStateHandler, ) : VectorViewModel(initialState), Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener, LocationSharingServiceConnection.Callback { @@ -1139,7 +1142,12 @@ class TimelineViewModel @AssistedInject constructor( } private fun handleStopLiveLocationSharing() { - locationSharingServiceConnection.stopLiveLocationSharing(room.roomId) + viewModelScope.launch { + val result = stopLiveLocationShareUseCase.execute(room.roomId) + if (result is UpdateLiveLocationShareResult.Failure) { + _viewEvents.post(RoomDetailViewEvents.Failure(throwable = result.error, showInDialog = true)) + } + } } private fun observeRoomSummary() { @@ -1310,7 +1318,7 @@ class TimelineViewModel @AssistedInject constructor( // we should also mark it as read here, for the scenario that the user // is already in the thread timeline markThreadTimelineAsReadLocal() - locationSharingServiceConnection.unbind() + locationSharingServiceConnection.unbind(this) super.onCleared() } } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index ef612eeec2..e883a89237 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt @@ -132,31 +132,16 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { fun stopSharingLocation(roomId: String) { Timber.i("### LocationSharingService.stopSharingLocation for $roomId") + synchronized(roomArgsMap) { + val beaconIds = roomArgsMap + .filter { it.value.roomId == roomId } + .map { it.key } + beaconIds.forEach { roomArgsMap.remove(it) } - launchInIO { session -> - when (val result = sendStoppedBeaconInfo(session, roomId)) { - is UpdateLiveLocationShareResult.Success -> { - synchronized(roomArgsMap) { - val beaconIds = roomArgsMap - .filter { it.value.roomId == roomId } - .map { it.key } - beaconIds.forEach { roomArgsMap.remove(it) } - - tryToDestroyMe() - } - } - is UpdateLiveLocationShareResult.Failure -> callback?.onServiceError(result.error) - else -> Unit - } + tryToDestroyMe() } } - private suspend fun sendStoppedBeaconInfo(session: Session, roomId: String): UpdateLiveLocationShareResult? { - return session.getRoom(roomId) - ?.locationSharingService() - ?.stopLiveLocationShare() - } - override fun onLocationUpdate(locationData: LocationData) { Timber.i("### LocationSharingService.onLocationUpdate. Uncertainty: ${locationData.uncertainty}") diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt index af09e0b1e0..33d4fbbb49 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt @@ -22,7 +22,9 @@ import android.content.Intent import android.content.ServiceConnection import android.os.IBinder import javax.inject.Inject +import javax.inject.Singleton +@Singleton class LocationSharingServiceConnection @Inject constructor( private val context: Context ) : ServiceConnection, LocationSharingService.Callback { @@ -33,12 +35,12 @@ class LocationSharingServiceConnection @Inject constructor( fun onLocationServiceError(error: Throwable) } - private var callback: Callback? = null + private val callbacks = mutableSetOf() private var isBound = false private var locationSharingService: LocationSharingService? = null fun bind(callback: Callback) { - this.callback = callback + addCallback(callback) if (isBound) { callback.onLocationServiceRunning() @@ -49,8 +51,8 @@ class LocationSharingServiceConnection @Inject constructor( } } - fun unbind() { - callback = null + fun unbind(callback: Callback) { + removeCallback(callback) } fun stopLiveLocationSharing(roomId: String) { @@ -62,17 +64,37 @@ class LocationSharingServiceConnection @Inject constructor( it.callback = this } isBound = true - callback?.onLocationServiceRunning() + onCallbackActionNoArg(Callback::onLocationServiceRunning) } override fun onServiceDisconnected(className: ComponentName) { isBound = false locationSharingService?.callback = null locationSharingService = null - callback?.onLocationServiceStopped() + onCallbackActionNoArg(Callback::onLocationServiceStopped) } override fun onServiceError(error: Throwable) { - callback?.onLocationServiceError(error) + forwardErrorToCallbacks(error) + } + + @Synchronized + private fun addCallback(callback: Callback) { + callbacks.add(callback) + } + + @Synchronized + private fun removeCallback(callback: Callback) { + callbacks.remove(callback) + } + + @Synchronized + private fun onCallbackActionNoArg(action: Callback.() -> Unit) { + callbacks.forEach(action) + } + + @Synchronized + private fun forwardErrorToCallbacks(error: Throwable) { + callbacks.forEach { it.onLocationServiceError(error) } } } diff --git a/vector/src/main/java/im/vector/app/features/location/live/StopLiveLocationShareUseCase.kt b/vector/src/main/java/im/vector/app/features/location/live/StopLiveLocationShareUseCase.kt new file mode 100644 index 0000000000..f4ad48089c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/StopLiveLocationShareUseCase.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 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.location.live + +import im.vector.app.features.location.LocationSharingServiceConnection +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult +import javax.inject.Inject + +class StopLiveLocationShareUseCase @Inject constructor( + private val locationSharingServiceConnection: LocationSharingServiceConnection, + private val session: Session +) { + + suspend fun execute(roomId: String): UpdateLiveLocationShareResult? { + val result = sendStoppedBeaconInfo(session, roomId) + when (result) { + is UpdateLiveLocationShareResult.Success -> locationSharingServiceConnection.stopLiveLocationSharing(roomId) + else -> Unit + } + return result + } + + private suspend fun sendStoppedBeaconInfo(session: Session, roomId: String): UpdateLiveLocationShareResult? { + return session.getRoom(roomId) + ?.locationSharingService() + ?.stopLiveLocationShare() + } +} diff --git a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt index e89649709a..15c76b083e 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/map/LocationLiveMapViewModel.kt @@ -24,13 +24,17 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.platform.VectorViewModel import im.vector.app.features.location.LocationSharingServiceConnection +import im.vector.app.features.location.live.StopLiveLocationShareUseCase import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult class LocationLiveMapViewModel @AssistedInject constructor( @Assisted private val initialState: LocationLiveMapViewState, getListOfUserLiveLocationUseCase: GetListOfUserLiveLocationUseCase, private val locationSharingServiceConnection: LocationSharingServiceConnection, + private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase, ) : VectorViewModel(initialState), LocationSharingServiceConnection.Callback { @AssistedFactory @@ -47,6 +51,11 @@ class LocationLiveMapViewModel @AssistedInject constructor( locationSharingServiceConnection.bind(this) } + override fun onCleared() { + locationSharingServiceConnection.unbind(this) + super.onCleared() + } + override fun handle(action: LocationLiveMapAction) { when (action) { is LocationLiveMapAction.AddMapSymbol -> handleAddMapSymbol(action) @@ -70,7 +79,12 @@ class LocationLiveMapViewModel @AssistedInject constructor( } private fun handleStopSharing() { - locationSharingServiceConnection.stopLiveLocationSharing(initialState.roomId) + viewModelScope.launch { + val result = stopLiveLocationShareUseCase.execute(initialState.roomId) + if (result is UpdateLiveLocationShareResult.Failure) { + _viewEvents.post(LocationLiveMapViewEvents.Error(result.error)) + } + } } override fun onLocationServiceRunning() { diff --git a/vector/src/test/java/im/vector/app/features/location/live/StopLiveLocationShareUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/location/live/StopLiveLocationShareUseCaseTest.kt new file mode 100644 index 0000000000..f508968cc8 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/location/live/StopLiveLocationShareUseCaseTest.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022 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.location.live + +import im.vector.app.test.fakes.FakeLocationSharingServiceConnection +import im.vector.app.test.fakes.FakeSession +import io.mockk.unmockkAll +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Test +import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult + +private const val A_ROOM_ID = "room_id" +private const val AN_EVENT_ID = "event_id" + +class StopLiveLocationShareUseCaseTest { + + private val fakeLocationSharingServiceConnection = FakeLocationSharingServiceConnection() + private val fakeSession = FakeSession() + + private val stopLiveLocationShareUseCase = StopLiveLocationShareUseCase( + locationSharingServiceConnection = fakeLocationSharingServiceConnection.instance, + session = fakeSession + ) + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given a room id when calling use case then the current live is stopped with success`() = runTest { + fakeLocationSharingServiceConnection.givenStopLiveLocationSharing() + + val result = stopLiveLocationShareUseCase.execute(A_ROOM_ID) + + result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) + fakeLocationSharingServiceConnection.verifyStopLiveLocationSharing(A_ROOM_ID) + } + + @Test + fun `given a room id and error during the process when calling use case then result is failure`() = runTest { + + } +} diff --git a/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt index 8a5a30e612..3835006712 100644 --- a/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt @@ -24,6 +24,7 @@ import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.unmockkAll import io.mockk.unmockkStatic import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf @@ -55,7 +56,7 @@ class GetListOfUserLiveLocationUseCaseTest { @After fun tearDown() { - unmockkStatic("androidx.lifecycle.FlowLiveDataConversions") + unmockkAll() } @Test diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingServiceConnection.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingServiceConnection.kt new file mode 100644 index 0000000000..2a152e44fe --- /dev/null +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingServiceConnection.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2022 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.test.fakes + +import im.vector.app.features.location.LocationSharingServiceConnection +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify + +class FakeLocationSharingServiceConnection { + + val instance = mockk() + + fun givenStopLiveLocationSharing() { + every { instance.stopLiveLocationSharing(any()) } just runs + } + + fun verifyStopLiveLocationSharing(roomId: String) { + verify { instance.stopLiveLocationSharing(roomId) } + } +} From d50b0fbb6ba51912b1dc221049c64b3c966309ee Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Thu, 16 Jun 2022 17:45:16 +0200 Subject: [PATCH 23/71] Adding unit tests for the stop live use case --- .../live/StopLiveLocationShareUseCaseTest.kt | 17 +++++++++- .../live/map/LocationLiveMapViewModelTest.kt | 34 ++++++++++++------- .../test/fakes/FakeLocationSharingService.kt | 6 ++++ .../FakeLocationSharingServiceConnection.kt | 12 +++++++ 4 files changed, 55 insertions(+), 14 deletions(-) diff --git a/vector/src/test/java/im/vector/app/features/location/live/StopLiveLocationShareUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/location/live/StopLiveLocationShareUseCaseTest.kt index f508968cc8..9ebddc76eb 100644 --- a/vector/src/test/java/im/vector/app/features/location/live/StopLiveLocationShareUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/location/live/StopLiveLocationShareUseCaseTest.kt @@ -46,15 +46,30 @@ class StopLiveLocationShareUseCaseTest { @Test fun `given a room id when calling use case then the current live is stopped with success`() = runTest { fakeLocationSharingServiceConnection.givenStopLiveLocationSharing() + val updateLiveResult = UpdateLiveLocationShareResult.Success(AN_EVENT_ID) + fakeSession.roomService() + .getRoom(A_ROOM_ID) + .locationSharingService() + .givenStopLiveLocationShareReturns(updateLiveResult) val result = stopLiveLocationShareUseCase.execute(A_ROOM_ID) - result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) + result shouldBeEqualTo updateLiveResult fakeLocationSharingServiceConnection.verifyStopLiveLocationSharing(A_ROOM_ID) } @Test fun `given a room id and error during the process when calling use case then result is failure`() = runTest { + val error = Throwable() + val updateLiveResult = UpdateLiveLocationShareResult.Failure(error) + fakeSession.roomService() + .getRoom(A_ROOM_ID) + .locationSharingService() + .givenStopLiveLocationShareReturns(updateLiveResult) + val result = stopLiveLocationShareUseCase.execute(A_ROOM_ID) + + result shouldBeEqualTo updateLiveResult + fakeLocationSharingServiceConnection.verifyStopLiveLocationSharingNotCalled(A_ROOM_ID) } } diff --git a/vector/src/test/java/im/vector/app/features/location/live/map/LocationLiveMapViewModelTest.kt b/vector/src/test/java/im/vector/app/features/location/live/map/LocationLiveMapViewModelTest.kt index b477265506..dd1a894a28 100644 --- a/vector/src/test/java/im/vector/app/features/location/live/map/LocationLiveMapViewModelTest.kt +++ b/vector/src/test/java/im/vector/app/features/location/live/map/LocationLiveMapViewModelTest.kt @@ -18,39 +18,47 @@ package im.vector.app.features.location.live.map import com.airbnb.mvrx.test.MvRxTestRule import im.vector.app.features.location.LocationData -import im.vector.app.features.location.LocationSharingServiceConnection +import im.vector.app.features.location.live.StopLiveLocationShareUseCase +import im.vector.app.test.fakes.FakeLocationSharingServiceConnection import im.vector.app.test.test import io.mockk.every -import io.mockk.just import io.mockk.mockk -import io.mockk.runs -import io.mockk.verify +import io.mockk.unmockkAll import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Rule import org.junit.Test import org.matrix.android.sdk.api.util.MatrixItem +private const val A_ROOM_ID = "room_id" + class LocationLiveMapViewModelTest { @get:Rule - val mvrxTestRule = MvRxTestRule() + val mvRxTestRule = MvRxTestRule(testDispatcher = UnconfinedTestDispatcher()) - private val fakeRoomId = "" - - private val args = LocationLiveMapViewArgs(roomId = fakeRoomId) + private val args = LocationLiveMapViewArgs(roomId = A_ROOM_ID) private val getListOfUserLiveLocationUseCase = mockk() - private val locationServiceConnection = mockk() + private val locationServiceConnection = FakeLocationSharingServiceConnection() + private val stopLiveLocationShareUseCase = mockk() private fun createViewModel(): LocationLiveMapViewModel { return LocationLiveMapViewModel( LocationLiveMapViewState(args), getListOfUserLiveLocationUseCase, - locationServiceConnection + locationServiceConnection.instance, + stopLiveLocationShareUseCase ) } + @After + fun tearDown() { + unmockkAll() + } + @Test fun `given the viewModel has been initialized then viewState contains user locations list`() = runTest { val userLocations = listOf( @@ -63,8 +71,8 @@ class LocationLiveMapViewModelTest { showStopSharingButton = false ) ) - every { locationServiceConnection.bind(any()) } just runs - every { getListOfUserLiveLocationUseCase.execute(fakeRoomId) } returns flowOf(userLocations) + locationServiceConnection.givenBind() + every { getListOfUserLiveLocationUseCase.execute(A_ROOM_ID) } returns flowOf(userLocations) val viewModel = createViewModel() viewModel @@ -76,6 +84,6 @@ class LocationLiveMapViewModelTest { ) .finish() - verify { locationServiceConnection.bind(viewModel) } + locationServiceConnection.verifyBind(viewModel) } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt index 2cd98c086c..0c105a588a 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt @@ -18,9 +18,11 @@ package im.vector.app.test.fakes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import org.matrix.android.sdk.api.session.room.location.LocationSharingService +import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary class FakeLocationSharingService : LocationSharingService by mockk() { @@ -31,4 +33,8 @@ class FakeLocationSharingService : LocationSharingService by mockk() { every { getRunningLiveLocationShareSummaries() } returns it } } + + fun givenStopLiveLocationShareReturns(result: UpdateLiveLocationShareResult) { + coEvery { stopLiveLocationShare() } returns result + } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingServiceConnection.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingServiceConnection.kt index 2a152e44fe..d0d148a9e2 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingServiceConnection.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingServiceConnection.kt @@ -27,6 +27,14 @@ class FakeLocationSharingServiceConnection { val instance = mockk() + fun givenBind() { + every { instance.bind(any()) } just runs + } + + fun verifyBind(callback: LocationSharingServiceConnection.Callback) { + verify { instance.bind(callback) } + } + fun givenStopLiveLocationSharing() { every { instance.stopLiveLocationSharing(any()) } just runs } @@ -34,4 +42,8 @@ class FakeLocationSharingServiceConnection { fun verifyStopLiveLocationSharing(roomId: String) { verify { instance.stopLiveLocationSharing(roomId) } } + + fun verifyStopLiveLocationSharingNotCalled(roomId: String) { + verify(inverse = true) { instance.stopLiveLocationSharing(roomId) } + } } From 3ab941eace35e7c3f9a8d7116eea299bbf83af54 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 20 Jun 2022 14:29:00 +0200 Subject: [PATCH 24/71] Adding changelog entry --- changelog.d/6349.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6349.bugfix diff --git a/changelog.d/6349.bugfix b/changelog.d/6349.bugfix new file mode 100644 index 0000000000..70718248a7 --- /dev/null +++ b/changelog.d/6349.bugfix @@ -0,0 +1 @@ +[Location sharing] Fix stop of a live not possible from another device From 96da695473494f6091249bccc2cd92d7852f9e17 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 20 Jun 2022 15:04:17 +0200 Subject: [PATCH 25/71] Service API to listen live summaries given a list of event ids --- .../room/location/LocationSharingService.kt | 6 +++++ ...cationShareAggregatedSummaryEntityQuery.kt | 13 ++++++++- .../location/DefaultLocationSharingService.kt | 8 ++++++ .../DefaultLocationSharingServiceTest.kt | 27 +++++++++++++++++++ .../android/sdk/test/fakes/FakeRealm.kt | 8 ++++++ 5 files changed, 61 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt index 0f88f891cc..4620bfb2cd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt @@ -60,4 +60,10 @@ interface LocationSharingService { * Returns a LiveData on the list of current running live location shares. */ fun getRunningLiveLocationShareSummaries(): LiveData> + + /** + * Returns a LiveData on the list of live location shares with the given eventIds. + * @param eventIds the list of event ids + */ + fun getLiveLocationShareSummaries(eventIds: List): LiveData> } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt index 6bcd737474..2dca96c492 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt @@ -76,7 +76,7 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveIn realm: Realm, roomId: String, userId: String, - ignoredEventId: String + ignoredEventId: String, ): List { return LiveLocationShareAggregatedSummaryEntity .whereRoomId(realm, roomId = roomId) @@ -100,3 +100,14 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findRunningLiveI .isNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID) .isNotNull(LiveLocationShareAggregatedSummaryEntityFields.LAST_LOCATION_CONTENT) } + +internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findLiveInRoom( + realm: Realm, + roomId: String, + eventIds: List, +): RealmQuery { + return LiveLocationShareAggregatedSummaryEntity + .whereRoomId(realm, roomId = roomId) + .isNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID) + .`in`(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventIds.toTypedArray()) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt index 015c1cca0b..bf2d9398ba 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt @@ -27,6 +27,7 @@ import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationSh import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity +import org.matrix.android.sdk.internal.database.query.findLiveInRoom import org.matrix.android.sdk.internal.database.query.findRunningLiveInRoom import org.matrix.android.sdk.internal.di.SessionDatabase @@ -88,4 +89,11 @@ internal class DefaultLocationSharingService @AssistedInject constructor( liveLocationShareAggregatedSummaryMapper ) } + + override fun getLiveLocationShareSummaries(eventIds: List): LiveData> { + return monarchy.findAllMappedWithChanges( + { LiveLocationShareAggregatedSummaryEntity.findLiveInRoom(it, roomId = roomId, eventIds = eventIds) }, + liveLocationShareAggregatedSummaryMapper + ) + } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt index 30a9671733..e06e3af7fb 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt @@ -33,6 +33,7 @@ import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationS import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields import org.matrix.android.sdk.test.fakes.FakeMonarchy import org.matrix.android.sdk.test.fakes.givenEqualTo +import org.matrix.android.sdk.test.fakes.givenIn import org.matrix.android.sdk.test.fakes.givenIsNotEmpty import org.matrix.android.sdk.test.fakes.givenIsNotNull @@ -168,4 +169,30 @@ internal class DefaultLocationSharingServiceTest { result shouldBeEqualTo listOf(summary) } + + @Test + fun `given a list of event ids livedata on live summaries is correctly computed`() { + val eventIds = listOf("event_id_1", "event_id_2", "event_id_3") + val entity = LiveLocationShareAggregatedSummaryEntity() + val summary = LiveLocationShareAggregatedSummary( + userId = "", + isActive = true, + endOfLiveTimestampMillis = 123, + lastLocationDataContent = null + ) + + fakeMonarchy.givenWhere() + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, fakeRoomId) + .givenIsNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID) + .givenIn(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventIds) + fakeMonarchy.givenFindAllMappedWithChangesReturns( + realmEntities = listOf(entity), + mappedResult = listOf(summary), + fakeLiveLocationShareAggregatedSummaryMapper + ) + + val result = defaultLocationSharingService.getLiveLocationShareSummaries(eventIds).value + + result shouldBeEqualTo listOf(summary) + } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt index 0ebff87278..c3264a32f1 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt @@ -97,3 +97,11 @@ inline fun RealmQuery.givenIsNotNull( every { isNotNull(fieldName) } returns this return this } + +inline fun RealmQuery.givenIn( + fieldName: String, + values: List +): RealmQuery { + every { `in`(fieldName, values.toTypedArray()) } returns this + return this +} From 9a3935433238bbcb623ea32352d9675e8858d3a3 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 20 Jun 2022 15:23:37 +0200 Subject: [PATCH 26/71] Adding use case to retrieve flow on live summaries given a list of event ids --- .../GetLiveLocationShareSummariesUseCase.kt | 38 ++++++++ ...etLiveLocationShareSummariesUseCaseTest.kt | 86 +++++++++++++++++++ .../GetListOfUserLiveLocationUseCaseTest.kt | 20 ++--- .../test/fakes/FakeLocationSharingService.kt | 14 ++- 4 files changed, 146 insertions(+), 12 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummariesUseCase.kt create mode 100644 vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummariesUseCaseTest.kt diff --git a/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummariesUseCase.kt b/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummariesUseCase.kt new file mode 100644 index 0000000000..0753f093e2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummariesUseCase.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 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.location.live + +import androidx.lifecycle.asFlow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.getRoom +import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary +import javax.inject.Inject + +class GetLiveLocationShareSummariesUseCase @Inject constructor( + private val session: Session, +) { + + fun execute(roomId: String, eventIds: List): Flow> { + return session.getRoom(roomId) + ?.locationSharingService() + ?.getLiveLocationShareSummaries(eventIds) + ?.asFlow() + ?: emptyFlow() + } +} diff --git a/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummariesUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummariesUseCaseTest.kt new file mode 100644 index 0000000000..e0ee2c8039 --- /dev/null +++ b/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummariesUseCaseTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2022 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.location.live + +import androidx.lifecycle.asFlow +import im.vector.app.test.fakes.FakeSession +import io.mockk.every +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeEqualTo +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary +import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent + +private const val A_ROOM_ID = "room_id" + +class GetLiveLocationShareSummariesUseCaseTest { + + private val fakeSession = FakeSession() + + private val getLiveLocationShareSummariesUseCase = GetLiveLocationShareSummariesUseCase( + session = fakeSession + ) + + @Before + fun setUp() { + mockkStatic("androidx.lifecycle.FlowLiveDataConversions") + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given a room id when calling use case then the current live is stopped with success`() = runTest { + val eventIds = listOf("event_id_1", "event_id_2", "event_id_3") + val summary1 = LiveLocationShareAggregatedSummary( + userId = "userId1", + isActive = true, + endOfLiveTimestampMillis = 123, + lastLocationDataContent = MessageBeaconLocationDataContent() + ) + val summary2 = LiveLocationShareAggregatedSummary( + userId = "userId2", + isActive = true, + endOfLiveTimestampMillis = 1234, + lastLocationDataContent = MessageBeaconLocationDataContent() + ) + val summary3 = LiveLocationShareAggregatedSummary( + userId = "userId3", + isActive = true, + endOfLiveTimestampMillis = 1234, + lastLocationDataContent = MessageBeaconLocationDataContent() + ) + val summaries = listOf(summary1, summary2, summary3) + val liveData = fakeSession.roomService() + .getRoom(A_ROOM_ID) + .locationSharingService() + .givenLiveLocationShareSummaries(eventIds, summaries) + every { liveData.asFlow() } returns flowOf(summaries) + + val result = getLiveLocationShareSummariesUseCase.execute(A_ROOM_ID, eventIds).first() + + result shouldBeEqualTo listOf(summary1, summary2, summary3) + } +} diff --git a/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt index 3835006712..b747b72cb1 100644 --- a/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.amshove.kluent.internal.assertEquals +import org.amshove.kluent.shouldBeEqualTo import org.junit.After import org.junit.Before import org.junit.Rule @@ -38,16 +39,17 @@ import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationSh import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.api.util.MatrixItem +private const val A_ROOM_ID = "room_id" + class GetListOfUserLiveLocationUseCaseTest { - @get:Rule - val mvRxTestRule = MvRxTestRule() - private val fakeSession = FakeSession() - private val viewStateMapper = mockk() - private val getListOfUserLiveLocationUseCase = GetListOfUserLiveLocationUseCase(fakeSession, viewStateMapper) + private val getListOfUserLiveLocationUseCase = GetListOfUserLiveLocationUseCase( + session = fakeSession, + userLiveLocationViewStateMapper = viewStateMapper + ) @Before fun setUp() { @@ -61,8 +63,6 @@ class GetListOfUserLiveLocationUseCaseTest { @Test fun `given a room id then the correct flow of view states list is collected`() = runTest { - val roomId = "roomId" - val summary1 = LiveLocationShareAggregatedSummary( userId = "userId1", isActive = true, @@ -83,7 +83,7 @@ class GetListOfUserLiveLocationUseCaseTest { ) val summaries = listOf(summary1, summary2, summary3) val liveData = fakeSession.roomService() - .getRoom(roomId) + .getRoom(A_ROOM_ID) .locationSharingService() .givenRunningLiveLocationShareSummaries(summaries) @@ -109,8 +109,8 @@ class GetListOfUserLiveLocationUseCaseTest { coEvery { viewStateMapper.map(summary2) } returns viewState2 coEvery { viewStateMapper.map(summary3) } returns null - val viewStates = getListOfUserLiveLocationUseCase.execute(roomId).first() + val viewStates = getListOfUserLiveLocationUseCase.execute(A_ROOM_ID).first() - assertEquals(listOf(viewState1, viewState2), viewStates) + viewStates shouldBeEqualTo listOf(viewState1, viewState2) } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt index 0c105a588a..8ad7004c58 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt @@ -27,13 +27,23 @@ import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationSh class FakeLocationSharingService : LocationSharingService by mockk() { - fun givenRunningLiveLocationShareSummaries(summaries: List): - LiveData> { + fun givenRunningLiveLocationShareSummaries( + summaries: List + ): LiveData> { return MutableLiveData(summaries).also { every { getRunningLiveLocationShareSummaries() } returns it } } + fun givenLiveLocationShareSummaries( + eventIds: List, + summaries: List + ): LiveData> { + return MutableLiveData(summaries).also { + every { getLiveLocationShareSummaries(eventIds) } returns it + } + } + fun givenStopLiveLocationShareReturns(result: UpdateLiveLocationShareResult) { coEvery { stopLiveLocationShare() } returns result } From 785ce03e672f73600378e643141a9d8e68db15ae Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 20 Jun 2022 15:49:35 +0200 Subject: [PATCH 27/71] Synchronizing access to map of roomArgs --- .../location/LocationSharingService.kt | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index e883a89237..4adac6846d 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt @@ -100,7 +100,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { ?.let { result -> when (result) { is UpdateLiveLocationShareResult.Success -> { - roomArgsMap[result.beaconEventId] = roomArgs + addRoomArgs(result.beaconEventId, roomArgs) locationTracker.requestLastKnownLocation() } is UpdateLiveLocationShareResult.Failure -> { @@ -132,21 +132,16 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { fun stopSharingLocation(roomId: String) { Timber.i("### LocationSharingService.stopSharingLocation for $roomId") - synchronized(roomArgsMap) { - val beaconIds = roomArgsMap - .filter { it.value.roomId == roomId } - .map { it.key } - beaconIds.forEach { roomArgsMap.remove(it) } - - tryToDestroyMe() - } + removeRoomArgs(roomId) + tryToDestroyMe() } + @Synchronized override fun onLocationUpdate(locationData: LocationData) { Timber.i("### LocationSharingService.onLocationUpdate. Uncertainty: ${locationData.uncertainty}") // Emit location update to all rooms in which live location sharing is active - roomArgsMap.toMap().forEach { item -> + roomArgsMap.forEach { item -> sendLiveLocation(item.value.roomId, item.key, locationData) } } @@ -173,6 +168,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { stopSelf() } + @Synchronized private fun tryToDestroyMe() { if (roomArgsMap.isEmpty()) { Timber.i("### LocationSharingService. Destroying self, time is up for all rooms") @@ -193,6 +189,19 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { destroyMe() } + @Synchronized + private fun addRoomArgs(beaconEventId: String, roomArgs: RoomArgs) { + roomArgsMap[beaconEventId] = roomArgs + } + + @Synchronized + private fun removeRoomArgs(roomId: String) { + val beaconIds = roomArgsMap + .filter { it.value.roomId == roomId } + .map { it.key } + beaconIds.forEach { roomArgsMap.remove(it) } + } + private fun launchInIO(block: suspend CoroutineScope.(Session) -> Unit) = activeSessionHolder .getSafeActiveSession() From 3cffedd353bbc0c6069b700f92e0c4ace21adb6d Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Mon, 20 Jun 2022 17:13:16 +0200 Subject: [PATCH 28/71] Changing in API to get livedata on a live of a given id --- .../room/location/LocationSharingService.kt | 7 +-- ...cationShareAggregatedSummaryEntityQuery.kt | 11 ----- .../location/DefaultLocationSharingService.kt | 19 +++++--- .../DefaultLocationSharingServiceTest.kt | 44 ++++++++++++++----- .../android/sdk/test/fakes/FakeMonarchy.kt | 7 ++- .../android/sdk/test/fakes/FakeRealm.kt | 8 ---- ... => GetLiveLocationShareSummaryUseCase.kt} | 8 ++-- ...GetLiveLocationShareSummaryUseCaseTest.kt} | 34 +++++--------- .../GetListOfUserLiveLocationUseCaseTest.kt | 6 +-- .../test/fakes/FakeLocationSharingService.kt | 15 ++++--- 10 files changed, 79 insertions(+), 80 deletions(-) rename vector/src/main/java/im/vector/app/features/location/live/{GetLiveLocationShareSummariesUseCase.kt => GetLiveLocationShareSummaryUseCase.kt} (79%) rename vector/src/test/java/im/vector/app/features/location/live/{GetLiveLocationShareSummariesUseCaseTest.kt => GetLiveLocationShareSummaryUseCaseTest.kt} (58%) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt index 4620bfb2cd..1ddaa87d34 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt @@ -19,6 +19,7 @@ package org.matrix.android.sdk.api.session.room.location import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional /** * Manage all location sharing related features. @@ -62,8 +63,8 @@ interface LocationSharingService { fun getRunningLiveLocationShareSummaries(): LiveData> /** - * Returns a LiveData on the list of live location shares with the given eventIds. - * @param eventIds the list of event ids + * Returns a LiveData on the live location share summary with the given eventId. + * @param beaconInfoEventId event id of the initial beacon info state event */ - fun getLiveLocationShareSummaries(eventIds: List): LiveData> + fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData> } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt index 2dca96c492..d69f251f6f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/LiveLocationShareAggregatedSummaryEntityQuery.kt @@ -100,14 +100,3 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findRunningLiveI .isNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID) .isNotNull(LiveLocationShareAggregatedSummaryEntityFields.LAST_LOCATION_CONTENT) } - -internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findLiveInRoom( - realm: Realm, - roomId: String, - eventIds: List, -): RealmQuery { - return LiveLocationShareAggregatedSummaryEntity - .whereRoomId(realm, roomId = roomId) - .isNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID) - .`in`(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventIds.toTypedArray()) -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt index bf2d9398ba..8e67142c52 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingService.kt @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.room.location import androidx.lifecycle.LiveData +import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -25,10 +26,12 @@ import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity -import org.matrix.android.sdk.internal.database.query.findLiveInRoom import org.matrix.android.sdk.internal.database.query.findRunningLiveInRoom +import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase internal class DefaultLocationSharingService @AssistedInject constructor( @@ -90,10 +93,14 @@ internal class DefaultLocationSharingService @AssistedInject constructor( ) } - override fun getLiveLocationShareSummaries(eventIds: List): LiveData> { - return monarchy.findAllMappedWithChanges( - { LiveLocationShareAggregatedSummaryEntity.findLiveInRoom(it, roomId = roomId, eventIds = eventIds) }, - liveLocationShareAggregatedSummaryMapper - ) + override fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData> { + return Transformations.map( + monarchy.findAllMappedWithChanges( + { LiveLocationShareAggregatedSummaryEntity.where(it, roomId = roomId, eventId = beaconInfoEventId) }, + liveLocationShareAggregatedSummaryMapper + ) + ) { + it.firstOrNull().toOptional() + } } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt index e06e3af7fb..4b556402d5 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultLocationSharingServiceTest.kt @@ -16,24 +16,32 @@ package org.matrix.android.sdk.internal.session.room.location +import androidx.arch.core.util.Function +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Transformations import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.slot import io.mockk.unmockkAll import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.After +import org.junit.Before import org.junit.Test import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields import org.matrix.android.sdk.test.fakes.FakeMonarchy import org.matrix.android.sdk.test.fakes.givenEqualTo -import org.matrix.android.sdk.test.fakes.givenIn import org.matrix.android.sdk.test.fakes.givenIsNotEmpty import org.matrix.android.sdk.test.fakes.givenIsNotNull @@ -47,7 +55,6 @@ private const val A_TIMEOUT = 15_000L @ExperimentalCoroutinesApi internal class DefaultLocationSharingServiceTest { - private val fakeRoomId = A_ROOM_ID private val fakeMonarchy = FakeMonarchy() private val sendStaticLocationTask = mockk() private val sendLiveLocationTask = mockk() @@ -56,7 +63,7 @@ internal class DefaultLocationSharingServiceTest { private val fakeLiveLocationShareAggregatedSummaryMapper = mockk() private val defaultLocationSharingService = DefaultLocationSharingService( - roomId = fakeRoomId, + roomId = A_ROOM_ID, monarchy = fakeMonarchy.instance, sendStaticLocationTask = sendStaticLocationTask, sendLiveLocationTask = sendLiveLocationTask, @@ -65,6 +72,11 @@ internal class DefaultLocationSharingServiceTest { liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper ) + @Before + fun setUp() { + mockkStatic("androidx.lifecycle.Transformations") + } + @After fun tearDown() { unmockkAll() @@ -155,7 +167,7 @@ internal class DefaultLocationSharingServiceTest { ) fakeMonarchy.givenWhere() - .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, fakeRoomId) + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, A_ROOM_ID) .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true) .givenIsNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID) .givenIsNotNull(LiveLocationShareAggregatedSummaryEntityFields.LAST_LOCATION_CONTENT) @@ -171,8 +183,7 @@ internal class DefaultLocationSharingServiceTest { } @Test - fun `given a list of event ids livedata on live summaries is correctly computed`() { - val eventIds = listOf("event_id_1", "event_id_2", "event_id_3") + fun `given an event id when getting livedata on corresponding live summary then it is correctly computed`() { val entity = LiveLocationShareAggregatedSummaryEntity() val summary = LiveLocationShareAggregatedSummary( userId = "", @@ -182,17 +193,26 @@ internal class DefaultLocationSharingServiceTest { ) fakeMonarchy.givenWhere() - .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, fakeRoomId) - .givenIsNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID) - .givenIn(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventIds) - fakeMonarchy.givenFindAllMappedWithChangesReturns( + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, A_ROOM_ID) + .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, AN_EVENT_ID) + val liveData = fakeMonarchy.givenFindAllMappedWithChangesReturns( realmEntities = listOf(entity), mappedResult = listOf(summary), fakeLiveLocationShareAggregatedSummaryMapper ) + val mapper = slot, Optional>>() + every { + Transformations.map( + liveData, + capture(mapper) + ) + } answers { + val value = secondArg, Optional>>().apply(listOf(summary)) + MutableLiveData(value) + } - val result = defaultLocationSharingService.getLiveLocationShareSummaries(eventIds).value + val result = defaultLocationSharingService.getLiveLocationShareSummary(AN_EVENT_ID).value - result shouldBeEqualTo listOf(summary) + result shouldBeEqualTo summary.toOptional() } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt index 9b4ca332d5..d77084fe3b 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeMonarchy.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.test.fakes +import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.zhuinden.monarchy.Monarchy import io.mockk.MockKVerificationScope @@ -60,10 +61,11 @@ internal class FakeMonarchy { realmEntities: List, mappedResult: List, mapper: Monarchy.Mapper - ) { + ): LiveData> { every { mapper.map(any()) } returns mockk() val monarchyQuery = slot>() val monarchyMapper = slot>() + val result = MutableLiveData(mappedResult) every { instance.findAllMappedWithChanges(capture(monarchyQuery), capture(monarchyMapper)) } answers { @@ -71,7 +73,8 @@ internal class FakeMonarchy { realmEntities.forEach { monarchyMapper.captured.map(it) } - MutableLiveData(mappedResult) + result } + return result } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt index c3264a32f1..0ebff87278 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeRealm.kt @@ -97,11 +97,3 @@ inline fun RealmQuery.givenIsNotNull( every { isNotNull(fieldName) } returns this return this } - -inline fun RealmQuery.givenIn( - fieldName: String, - values: List -): RealmQuery { - every { `in`(fieldName, values.toTypedArray()) } returns this - return this -} diff --git a/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummariesUseCase.kt b/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt similarity index 79% rename from vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummariesUseCase.kt rename to vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt index 0753f093e2..0687189925 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummariesUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt @@ -19,20 +19,22 @@ package im.vector.app.features.location.live import androidx.lifecycle.asFlow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.mapNotNull import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import javax.inject.Inject -class GetLiveLocationShareSummariesUseCase @Inject constructor( +class GetLiveLocationShareSummaryUseCase @Inject constructor( private val session: Session, ) { - fun execute(roomId: String, eventIds: List): Flow> { + fun execute(roomId: String, eventId: String): Flow { return session.getRoom(roomId) ?.locationSharingService() - ?.getLiveLocationShareSummaries(eventIds) + ?.getLiveLocationShareSummary(eventId) ?.asFlow() + ?.mapNotNull { it.getOrNull() } ?: emptyFlow() } } diff --git a/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummariesUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCaseTest.kt similarity index 58% rename from vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummariesUseCaseTest.kt rename to vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCaseTest.kt index e0ee2c8039..67e11e682d 100644 --- a/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummariesUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCaseTest.kt @@ -30,14 +30,16 @@ import org.junit.Before import org.junit.Test import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent +import org.matrix.android.sdk.api.util.Optional private const val A_ROOM_ID = "room_id" +private const val AN_EVENT_ID = "event_id" -class GetLiveLocationShareSummariesUseCaseTest { +class GetLiveLocationShareSummaryUseCaseTest { private val fakeSession = FakeSession() - private val getLiveLocationShareSummariesUseCase = GetLiveLocationShareSummariesUseCase( + private val getLiveLocationShareSummaryUseCase = GetLiveLocationShareSummaryUseCase( session = fakeSession ) @@ -52,35 +54,21 @@ class GetLiveLocationShareSummariesUseCaseTest { } @Test - fun `given a room id when calling use case then the current live is stopped with success`() = runTest { - val eventIds = listOf("event_id_1", "event_id_2", "event_id_3") - val summary1 = LiveLocationShareAggregatedSummary( - userId = "userId1", + fun `given a room id and event id when calling use case then live data on summary is returned`() = runTest { + val summary = LiveLocationShareAggregatedSummary( + userId = "userId", isActive = true, endOfLiveTimestampMillis = 123, lastLocationDataContent = MessageBeaconLocationDataContent() ) - val summary2 = LiveLocationShareAggregatedSummary( - userId = "userId2", - isActive = true, - endOfLiveTimestampMillis = 1234, - lastLocationDataContent = MessageBeaconLocationDataContent() - ) - val summary3 = LiveLocationShareAggregatedSummary( - userId = "userId3", - isActive = true, - endOfLiveTimestampMillis = 1234, - lastLocationDataContent = MessageBeaconLocationDataContent() - ) - val summaries = listOf(summary1, summary2, summary3) val liveData = fakeSession.roomService() .getRoom(A_ROOM_ID) .locationSharingService() - .givenLiveLocationShareSummaries(eventIds, summaries) - every { liveData.asFlow() } returns flowOf(summaries) + .givenLiveLocationShareSummaryReturns(AN_EVENT_ID, summary) + every { liveData.asFlow() } returns flowOf(Optional(summary)) - val result = getLiveLocationShareSummariesUseCase.execute(A_ROOM_ID, eventIds).first() + val result = getLiveLocationShareSummaryUseCase.execute(A_ROOM_ID, AN_EVENT_ID).first() - result shouldBeEqualTo listOf(summary1, summary2, summary3) + result shouldBeEqualTo summary } } diff --git a/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt index b747b72cb1..9eeaa31bbf 100644 --- a/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/location/live/map/GetListOfUserLiveLocationUseCaseTest.kt @@ -17,7 +17,6 @@ package im.vector.app.features.location.live.map import androidx.lifecycle.asFlow -import com.airbnb.mvrx.test.MvRxTestRule import im.vector.app.features.location.LocationData import im.vector.app.test.fakes.FakeSession import io.mockk.coEvery @@ -25,15 +24,12 @@ import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.unmockkAll -import io.mockk.unmockkStatic import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest -import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.shouldBeEqualTo import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent @@ -85,7 +81,7 @@ class GetListOfUserLiveLocationUseCaseTest { val liveData = fakeSession.roomService() .getRoom(A_ROOM_ID) .locationSharingService() - .givenRunningLiveLocationShareSummaries(summaries) + .givenRunningLiveLocationShareSummariesReturns(summaries) every { liveData.asFlow() } returns flowOf(summaries) diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt index 8ad7004c58..cebd45b2bb 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingService.kt @@ -24,10 +24,11 @@ import io.mockk.mockk import org.matrix.android.sdk.api.session.room.location.LocationSharingService import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary +import org.matrix.android.sdk.api.util.Optional class FakeLocationSharingService : LocationSharingService by mockk() { - fun givenRunningLiveLocationShareSummaries( + fun givenRunningLiveLocationShareSummariesReturns( summaries: List ): LiveData> { return MutableLiveData(summaries).also { @@ -35,12 +36,12 @@ class FakeLocationSharingService : LocationSharingService by mockk() { } } - fun givenLiveLocationShareSummaries( - eventIds: List, - summaries: List - ): LiveData> { - return MutableLiveData(summaries).also { - every { getLiveLocationShareSummaries(eventIds) } returns it + fun givenLiveLocationShareSummaryReturns( + eventId: String, + summary: LiveLocationShareAggregatedSummary + ): LiveData> { + return MutableLiveData(Optional(summary)).also { + every { getLiveLocationShareSummary(eventId) } returns it } } From 81e14c7c3b6529f08d2cf0b0cacc5a548c836c12 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Tue, 21 Jun 2022 09:43:29 +0200 Subject: [PATCH 29/71] Observing live status in DB from location sharing Android service --- .../room/location/LocationSharingService.kt | 3 ++ .../location/LocationSharingService.kt | 53 ++++++++++--------- .../LocationSharingServiceConnection.kt | 4 -- .../GetLiveLocationShareSummaryUseCase.kt | 4 ++ .../live/StopLiveLocationShareUseCase.kt | 9 +--- .../live/StopLiveLocationShareUseCaseTest.kt | 6 --- .../FakeLocationSharingServiceConnection.kt | 12 ----- 7 files changed, 37 insertions(+), 54 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt index 1ddaa87d34..ada3dc85d7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/location/LocationSharingService.kt @@ -16,6 +16,7 @@ package org.matrix.android.sdk.api.session.room.location +import androidx.annotation.MainThread import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.api.util.Cancelable @@ -60,11 +61,13 @@ interface LocationSharingService { /** * Returns a LiveData on the list of current running live location shares. */ + @MainThread fun getRunningLiveLocationShareSummaries(): LiveData> /** * Returns a LiveData on the live location share summary with the given eventId. * @param beaconInfoEventId event id of the initial beacon info state event */ + @MainThread fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData> } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index 4adac6846d..3ba5031200 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt @@ -18,22 +18,27 @@ package im.vector.app.features.location import android.content.Intent import android.os.Binder +import android.os.Handler import android.os.IBinder import android.os.Parcelable import dagger.hilt.android.AndroidEntryPoint import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.services.VectorService +import im.vector.app.features.location.live.GetLiveLocationShareSummaryUseCase import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.session.coroutineScope import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.distinctUntilChangedBy +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import timber.log.Timber -import java.util.Timer -import java.util.TimerTask import javax.inject.Inject @AndroidEntryPoint @@ -49,6 +54,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { @Inject lateinit var notificationUtils: NotificationUtils @Inject lateinit var locationTracker: LocationTracker @Inject lateinit var activeSessionHolder: ActiveSessionHolder + @Inject lateinit var getLiveLocationShareSummaryUseCase: GetLiveLocationShareSummaryUseCase private val binder = LocalBinder() @@ -56,8 +62,9 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { * Keep track of a map between beacon event Id starting the live and RoomArgs. */ private val roomArgsMap = mutableMapOf() - private val timers = mutableListOf() var callback: Callback? = null + private val jobs = mutableListOf() + private val mainHandler by lazy { Handler(mainLooper) } override fun onCreate() { super.onCreate() @@ -78,9 +85,6 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { val notification = notificationUtils.buildLiveLocationSharingNotification() startForeground(roomArgs.roomId.hashCode(), notification) - // Schedule a timer to stop sharing - scheduleTimer(roomArgs.roomId, roomArgs.durationMillis) - // Send beacon info state event launchInIO { session -> sendStartingLiveBeaconInfo(session, roomArgs) @@ -101,6 +105,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { when (result) { is UpdateLiveLocationShareResult.Success -> { addRoomArgs(result.beaconEventId, roomArgs) + listenForLiveSummaryChanges(roomArgs.roomId, result.beaconEventId) locationTracker.requestLastKnownLocation() } is UpdateLiveLocationShareResult.Failure -> { @@ -115,22 +120,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { } } - private fun scheduleTimer(roomId: String, durationMillis: Long) { - Timer() - .apply { - schedule(object : TimerTask() { - override fun run() { - stopSharingLocation(roomId) - timers.remove(this@apply) - } - }, durationMillis) - } - .also { - timers.add(it) - } - } - - fun stopSharingLocation(roomId: String) { + private fun stopSharingLocation(roomId: String) { Timber.i("### LocationSharingService.stopSharingLocation for $roomId") removeRoomArgs(roomId) tryToDestroyMe() @@ -177,9 +167,9 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { } private fun destroyMe() { + jobs.forEach { it.cancel() } + jobs.clear() locationTracker.removeCallback(this) - timers.forEach { it.cancel() } - timers.clear() stopSelf() } @@ -202,6 +192,21 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { beaconIds.forEach { roomArgsMap.remove(it) } } + private fun listenForLiveSummaryChanges(roomId: String, eventId: String) { + activeSessionHolder + .getSafeActiveSession() + ?.let { session -> + mainHandler.post { + val job = getLiveLocationShareSummaryUseCase.execute(roomId, eventId) + .distinctUntilChangedBy { it.isActive } + .filter { it.isActive == false } + .onEach { stopSharingLocation(roomId) } + .launchIn(session.coroutineScope) + jobs.add(job) + } + } + } + private fun launchInIO(block: suspend CoroutineScope.(Session) -> Unit) = activeSessionHolder .getSafeActiveSession() diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt index 33d4fbbb49..817216f0b4 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt @@ -55,10 +55,6 @@ class LocationSharingServiceConnection @Inject constructor( removeCallback(callback) } - fun stopLiveLocationSharing(roomId: String) { - locationSharingService?.stopSharingLocation(roomId) - } - override fun onServiceConnected(className: ComponentName, binder: IBinder) { locationSharingService = (binder as LocationSharingService.LocalBinder).getService().also { it.callback = this diff --git a/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt b/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt index 0687189925..d2696f62fd 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt @@ -16,6 +16,7 @@ package im.vector.app.features.location.live +import androidx.annotation.MainThread import androidx.lifecycle.asFlow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow @@ -23,13 +24,16 @@ import kotlinx.coroutines.flow.mapNotNull import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary +import timber.log.Timber import javax.inject.Inject class GetLiveLocationShareSummaryUseCase @Inject constructor( private val session: Session, ) { + @MainThread fun execute(roomId: String, eventId: String): Flow { + Timber.d("getting flow for roomId=$roomId and eventId=$eventId") return session.getRoom(roomId) ?.locationSharingService() ?.getLiveLocationShareSummary(eventId) diff --git a/vector/src/main/java/im/vector/app/features/location/live/StopLiveLocationShareUseCase.kt b/vector/src/main/java/im/vector/app/features/location/live/StopLiveLocationShareUseCase.kt index f4ad48089c..b5e0e6ef02 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/StopLiveLocationShareUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/StopLiveLocationShareUseCase.kt @@ -16,24 +16,17 @@ package im.vector.app.features.location.live -import im.vector.app.features.location.LocationSharingServiceConnection import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import javax.inject.Inject class StopLiveLocationShareUseCase @Inject constructor( - private val locationSharingServiceConnection: LocationSharingServiceConnection, private val session: Session ) { suspend fun execute(roomId: String): UpdateLiveLocationShareResult? { - val result = sendStoppedBeaconInfo(session, roomId) - when (result) { - is UpdateLiveLocationShareResult.Success -> locationSharingServiceConnection.stopLiveLocationSharing(roomId) - else -> Unit - } - return result + return sendStoppedBeaconInfo(session, roomId) } private suspend fun sendStoppedBeaconInfo(session: Session, roomId: String): UpdateLiveLocationShareResult? { diff --git a/vector/src/test/java/im/vector/app/features/location/live/StopLiveLocationShareUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/location/live/StopLiveLocationShareUseCaseTest.kt index 9ebddc76eb..e111771683 100644 --- a/vector/src/test/java/im/vector/app/features/location/live/StopLiveLocationShareUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/location/live/StopLiveLocationShareUseCaseTest.kt @@ -16,7 +16,6 @@ package im.vector.app.features.location.live -import im.vector.app.test.fakes.FakeLocationSharingServiceConnection import im.vector.app.test.fakes.FakeSession import io.mockk.unmockkAll import kotlinx.coroutines.test.runTest @@ -30,11 +29,9 @@ private const val AN_EVENT_ID = "event_id" class StopLiveLocationShareUseCaseTest { - private val fakeLocationSharingServiceConnection = FakeLocationSharingServiceConnection() private val fakeSession = FakeSession() private val stopLiveLocationShareUseCase = StopLiveLocationShareUseCase( - locationSharingServiceConnection = fakeLocationSharingServiceConnection.instance, session = fakeSession ) @@ -45,7 +42,6 @@ class StopLiveLocationShareUseCaseTest { @Test fun `given a room id when calling use case then the current live is stopped with success`() = runTest { - fakeLocationSharingServiceConnection.givenStopLiveLocationSharing() val updateLiveResult = UpdateLiveLocationShareResult.Success(AN_EVENT_ID) fakeSession.roomService() .getRoom(A_ROOM_ID) @@ -55,7 +51,6 @@ class StopLiveLocationShareUseCaseTest { val result = stopLiveLocationShareUseCase.execute(A_ROOM_ID) result shouldBeEqualTo updateLiveResult - fakeLocationSharingServiceConnection.verifyStopLiveLocationSharing(A_ROOM_ID) } @Test @@ -70,6 +65,5 @@ class StopLiveLocationShareUseCaseTest { val result = stopLiveLocationShareUseCase.execute(A_ROOM_ID) result shouldBeEqualTo updateLiveResult - fakeLocationSharingServiceConnection.verifyStopLiveLocationSharingNotCalled(A_ROOM_ID) } } diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingServiceConnection.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingServiceConnection.kt index d0d148a9e2..db27a894f9 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingServiceConnection.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeLocationSharingServiceConnection.kt @@ -34,16 +34,4 @@ class FakeLocationSharingServiceConnection { fun verifyBind(callback: LocationSharingServiceConnection.Callback) { verify { instance.bind(callback) } } - - fun givenStopLiveLocationSharing() { - every { instance.stopLiveLocationSharing(any()) } just runs - } - - fun verifyStopLiveLocationSharing(roomId: String) { - verify { instance.stopLiveLocationSharing(roomId) } - } - - fun verifyStopLiveLocationSharingNotCalled(roomId: String) { - verify(inverse = true) { instance.stopLiveLocationSharing(roomId) } - } } From 519d43ceb78fc23c7f899cdf0033c2eb4c1055d8 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 24 Jun 2022 14:33:19 +0200 Subject: [PATCH 30/71] Simplify loop to remove room args --- .../vector/app/features/location/LocationSharingService.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index 3ba5031200..ce3933cc36 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt @@ -186,10 +186,9 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { @Synchronized private fun removeRoomArgs(roomId: String) { - val beaconIds = roomArgsMap + roomArgsMap .filter { it.value.roomId == roomId } - .map { it.key } - beaconIds.forEach { roomArgsMap.remove(it) } + .forEach { roomArgsMap.remove(it.key) } } private fun listenForLiveSummaryChanges(roomId: String, eventId: String) { From 945026730c8cb29d26f86e35b5d260ea03b70168 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 24 Jun 2022 15:12:38 +0200 Subject: [PATCH 31/71] Use ActiveSessionHolder in stop live use case --- .../im/vector/app/core/di/ActiveSessionHolder.kt | 12 ++++++------ .../location/live/StopLiveLocationShareUseCase.kt | 11 ++++++----- .../live/StopLiveLocationShareUseCaseTest.kt | 14 +++++++++----- .../app/test/fakes/FakeActiveSessionHolder.kt | 2 +- 4 files changed, 22 insertions(+), 17 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt index ef7f0896b8..21b4e287c6 100644 --- a/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt +++ b/vector/src/main/java/im/vector/app/core/di/ActiveSessionHolder.kt @@ -44,11 +44,11 @@ class ActiveSessionHolder @Inject constructor( private val guardServiceStarter: GuardServiceStarter ) { - private var activeSession: AtomicReference = AtomicReference() + private var activeSessionReference: AtomicReference = AtomicReference() fun setActiveSession(session: Session) { Timber.w("setActiveSession of ${session.myUserId}") - activeSession.set(session) + activeSessionReference.set(session) activeSessionDataSource.post(Option.just(session)) keyRequestHandler.start(session) @@ -68,7 +68,7 @@ class ActiveSessionHolder @Inject constructor( it.removeListener(sessionListener) } - activeSession.set(null) + activeSessionReference.set(null) activeSessionDataSource.post(Option.empty()) keyRequestHandler.stop() @@ -80,15 +80,15 @@ class ActiveSessionHolder @Inject constructor( } fun hasActiveSession(): Boolean { - return activeSession.get() != null + return activeSessionReference.get() != null } fun getSafeActiveSession(): Session? { - return activeSession.get() + return activeSessionReference.get() } fun getActiveSession(): Session { - return activeSession.get() + return activeSessionReference.get() ?: throw IllegalStateException("You should authenticate before using this") } diff --git a/vector/src/main/java/im/vector/app/features/location/live/StopLiveLocationShareUseCase.kt b/vector/src/main/java/im/vector/app/features/location/live/StopLiveLocationShareUseCase.kt index b5e0e6ef02..402c7ffb15 100644 --- a/vector/src/main/java/im/vector/app/features/location/live/StopLiveLocationShareUseCase.kt +++ b/vector/src/main/java/im/vector/app/features/location/live/StopLiveLocationShareUseCase.kt @@ -16,21 +16,22 @@ package im.vector.app.features.location.live -import org.matrix.android.sdk.api.session.Session +import im.vector.app.core.di.ActiveSessionHolder import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import javax.inject.Inject class StopLiveLocationShareUseCase @Inject constructor( - private val session: Session + private val activeSessionHolder: ActiveSessionHolder ) { suspend fun execute(roomId: String): UpdateLiveLocationShareResult? { - return sendStoppedBeaconInfo(session, roomId) + return sendStoppedBeaconInfo(roomId) } - private suspend fun sendStoppedBeaconInfo(session: Session, roomId: String): UpdateLiveLocationShareResult? { - return session.getRoom(roomId) + private suspend fun sendStoppedBeaconInfo(roomId: String): UpdateLiveLocationShareResult? { + return activeSessionHolder.getActiveSession() + .getRoom(roomId) ?.locationSharingService() ?.stopLiveLocationShare() } diff --git a/vector/src/test/java/im/vector/app/features/location/live/StopLiveLocationShareUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/location/live/StopLiveLocationShareUseCaseTest.kt index e111771683..36fef4fd7b 100644 --- a/vector/src/test/java/im/vector/app/features/location/live/StopLiveLocationShareUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/features/location/live/StopLiveLocationShareUseCaseTest.kt @@ -16,7 +16,7 @@ package im.vector.app.features.location.live -import im.vector.app.test.fakes.FakeSession +import im.vector.app.test.fakes.FakeActiveSessionHolder import io.mockk.unmockkAll import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo @@ -29,10 +29,10 @@ private const val AN_EVENT_ID = "event_id" class StopLiveLocationShareUseCaseTest { - private val fakeSession = FakeSession() + private val fakeActiveSessionHolder = FakeActiveSessionHolder() private val stopLiveLocationShareUseCase = StopLiveLocationShareUseCase( - session = fakeSession + activeSessionHolder = fakeActiveSessionHolder.instance ) @After @@ -43,7 +43,9 @@ class StopLiveLocationShareUseCaseTest { @Test fun `given a room id when calling use case then the current live is stopped with success`() = runTest { val updateLiveResult = UpdateLiveLocationShareResult.Success(AN_EVENT_ID) - fakeSession.roomService() + fakeActiveSessionHolder + .fakeSession + .roomService() .getRoom(A_ROOM_ID) .locationSharingService() .givenStopLiveLocationShareReturns(updateLiveResult) @@ -57,7 +59,9 @@ class StopLiveLocationShareUseCaseTest { fun `given a room id and error during the process when calling use case then result is failure`() = runTest { val error = Throwable() val updateLiveResult = UpdateLiveLocationShareResult.Failure(error) - fakeSession.roomService() + fakeActiveSessionHolder + .fakeSession + .roomService() .getRoom(A_ROOM_ID) .locationSharingService() .givenStopLiveLocationShareReturns(updateLiveResult) diff --git a/vector/src/test/java/im/vector/app/test/fakes/FakeActiveSessionHolder.kt b/vector/src/test/java/im/vector/app/test/fakes/FakeActiveSessionHolder.kt index d0825a0043..70cbab6a3e 100644 --- a/vector/src/test/java/im/vector/app/test/fakes/FakeActiveSessionHolder.kt +++ b/vector/src/test/java/im/vector/app/test/fakes/FakeActiveSessionHolder.kt @@ -23,7 +23,7 @@ import io.mockk.mockk import org.matrix.android.sdk.api.session.Session class FakeActiveSessionHolder( - private val fakeSession: FakeSession = FakeSession() + val fakeSession: FakeSession = FakeSession() ) { val instance = mockk { every { getActiveSession() } returns fakeSession From 532bc18b1e5ac8a6baa79350c7fbc551e9ca8656 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 24 Jun 2022 16:52:16 +0300 Subject: [PATCH 32/71] Refactor poll item view state factory. --- .../timeline/factory/MessageItemFactory.kt | 28 +- .../timeline/factory/PollItemFactory.kt | 135 --------- .../factory/PollItemViewStateFactory.kt | 185 ++++++++++++ .../timeline/item/MessageInformationData.kt | 5 +- .../poll/{PollState.kt => PollViewState.kt} | 16 +- .../timeline/factory/PollItemFactoryTest.kt | 282 ------------------ 6 files changed, 223 insertions(+), 428 deletions(-) delete mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt rename vector/src/main/java/im/vector/app/features/poll/{PollState.kt => PollViewState.kt} (66%) delete mode 100644 vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index fba6ffbe51..d2032bb4c4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -53,6 +53,8 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem import im.vector.app.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_ +import im.vector.app.features.home.room.detail.timeline.item.PollItem +import im.vector.app.features.home.room.detail.timeline.item.PollItem_ import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_ import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem @@ -128,7 +130,7 @@ class MessageItemFactory @Inject constructor( private val vectorPreferences: VectorPreferences, private val urlMapProvider: UrlMapProvider, private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory, - private val pollItemFactory: PollItemFactory, + private val pollItemViewStateFactory: PollItemViewStateFactory, ) { // TODO inject this properly? @@ -188,7 +190,7 @@ class MessageItemFactory @Inject constructor( is MessageFileContent -> buildFileMessageItem(messageContent, highlight, attributes) is MessageAudioContent -> buildAudioContent(params, messageContent, informationData, highlight, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessagePollContent -> pollItemFactory.create(messageContent, informationData, highlight, callback, attributes) + is MessagePollContent -> buildPollItem(messageContent, informationData, highlight, callback, attributes) is MessageLocationContent -> buildLocationItem(messageContent, informationData, highlight, attributes) is MessageBeaconInfoContent -> liveLocationShareMessageItemFactory.create(params.event, highlight, attributes) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) @@ -224,6 +226,28 @@ class MessageItemFactory @Inject constructor( .leftGuideline(avatarSizeProvider.leftGuideline) } + private fun buildPollItem( + pollContent: MessagePollContent, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes, + ): PollItem { + val pollViewState = pollItemViewStateFactory.create(pollContent, informationData, callback) + + return PollItem_() + .attributes(attributes) + .eventId(informationData.eventId) + .pollQuestion(pollViewState.question) + .canVote(pollViewState.canVote) + .totalVotesText(pollViewState.totalVotes) + .optionViewStates(pollViewState.optionViewStates) + .edited(informationData.hasBeenEdited) + .highlighted(highlight) + .leftGuideline(avatarSizeProvider.leftGuideline) + .callback(callback) + } + private fun buildAudioMessageItem( params: TimelineItemFactoryParams, messageContent: MessageAudioContent, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt deleted file mode 100644 index 05e945c193..0000000000 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactory.kt +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright (c) 2022 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.factory - -import androidx.annotation.VisibleForTesting -import im.vector.app.R -import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.resources.StringProvider -import im.vector.app.core.utils.DimensionConverter -import im.vector.app.features.home.room.detail.timeline.TimelineEventController -import im.vector.app.features.home.room.detail.timeline.factory.MessageItemFactoryHelper.annotateWithEdited -import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider -import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem -import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData -import im.vector.app.features.home.room.detail.timeline.item.PollItem_ -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState -import im.vector.app.features.home.room.detail.timeline.item.PollResponseData -import im.vector.app.features.poll.PollState -import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent -import org.matrix.android.sdk.api.session.room.model.message.PollAnswer -import javax.inject.Inject - -class PollItemFactory @Inject constructor( - private val stringProvider: StringProvider, - private val avatarSizeProvider: AvatarSizeProvider, - private val colorProvider: ColorProvider, - private val dimensionConverter: DimensionConverter, -) { - - fun create( - pollContent: MessagePollContent, - informationData: MessageInformationData, - highlight: Boolean, - callback: TimelineEventController.Callback?, - attributes: AbsMessageItem.Attributes, - ): VectorEpoxyModel<*>? { - val pollResponseSummary = informationData.pollResponseAggregatedSummary - val pollState = createPollState(informationData, pollResponseSummary, pollContent) - val pollCreationInfo = pollContent.getBestPollCreationInfo() - val questionText = pollCreationInfo?.question?.getBestQuestion().orEmpty() - val question = createPollQuestion(informationData, questionText, callback) - val optionViewStates = pollCreationInfo?.answers?.mapToOptions(pollState, informationData) - val totalVotesText = createTotalVotesText(pollState, pollResponseSummary) - - return PollItem_() - .attributes(attributes) - .eventId(informationData.eventId) - .pollQuestion(question) - .canVote(pollState.isVotable()) - .totalVotesText(totalVotesText) - .optionViewStates(optionViewStates) - .edited(informationData.hasBeenEdited) - .highlighted(highlight) - .leftGuideline(avatarSizeProvider.leftGuideline) - .callback(callback) - } - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun createPollState( - informationData: MessageInformationData, - pollResponseSummary: PollResponseData?, - pollContent: MessagePollContent, - ): PollState = when { - !informationData.sendState.isSent() -> PollState.Sending - pollResponseSummary?.isClosed.orFalse() -> PollState.Ended - pollContent.getBestPollCreationInfo()?.isUndisclosed().orFalse() -> PollState.Undisclosed - pollResponseSummary?.myVote?.isNotEmpty().orFalse() -> PollState.Voted(pollResponseSummary?.totalVotes ?: 0) - else -> PollState.Ready - } - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal fun List.mapToOptions( - pollState: PollState, - informationData: MessageInformationData, - ) = map { answer -> - val pollResponseSummary = informationData.pollResponseAggregatedSummary - val winnerVoteCount = pollResponseSummary?.winnerVoteCount - val optionId = answer.id ?: "" - val optionAnswer = answer.getBestAnswer() ?: "" - val voteSummary = pollResponseSummary?.votes?.get(answer.id) - val voteCount = voteSummary?.total ?: 0 - val votePercentage = voteSummary?.percentage ?: 0.0 - val isMyVote = pollResponseSummary?.myVote == answer.id - val isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount - - when (pollState) { - PollState.Sending -> PollOptionViewState.PollSending(optionId, optionAnswer) - PollState.Ready -> PollOptionViewState.PollReady(optionId, optionAnswer) - is PollState.Voted -> PollOptionViewState.PollVoted(optionId, optionAnswer, voteCount, votePercentage, isMyVote) - PollState.Undisclosed -> PollOptionViewState.PollUndisclosed(optionId, optionAnswer, isMyVote) - PollState.Ended -> PollOptionViewState.PollEnded(optionId, optionAnswer, voteCount, votePercentage, isWinner) - } - } - - private fun createPollQuestion( - informationData: MessageInformationData, - question: String, - callback: TimelineEventController.Callback?, - ) = if (informationData.hasBeenEdited) { - annotateWithEdited(stringProvider, colorProvider, dimensionConverter, question, callback, informationData) - } else { - question - }.toEpoxyCharSequence() - - private fun createTotalVotesText( - pollState: PollState, - pollResponseSummary: PollResponseData?, - ): String { - val votes = pollResponseSummary?.totalVotes ?: 0 - return when { - pollState is PollState.Ended -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, votes, votes) - pollState is PollState.Undisclosed -> "" - pollState is PollState.Voted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, votes, votes) - votes == 0 -> stringProvider.getString(R.string.poll_no_votes_cast) - else -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, votes, votes) - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt new file mode 100644 index 0000000000..8365f0710e --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2022 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.factory + +import im.vector.app.R +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.utils.DimensionConverter +import im.vector.app.features.home.room.detail.timeline.TimelineEventController +import im.vector.app.features.home.room.detail.timeline.factory.MessageItemFactoryHelper.annotateWithEdited +import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState +import im.vector.app.features.home.room.detail.timeline.item.PollResponseData +import im.vector.app.features.poll.PollViewState +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence +import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent +import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo +import javax.inject.Inject + +class PollItemViewStateFactory @Inject constructor( + private val stringProvider: StringProvider, + private val colorProvider: ColorProvider, + private val dimensionConverter: DimensionConverter, +) { + + fun create( + pollContent: MessagePollContent, + informationData: MessageInformationData, + callback: TimelineEventController.Callback?, + ): PollViewState { + val pollCreationInfo = pollContent.getBestPollCreationInfo() + + val questionText = pollCreationInfo?.question?.getBestQuestion().orEmpty() + val question = createPollQuestion(informationData, questionText, callback) + + val pollResponseSummary = informationData.pollResponseAggregatedSummary + val winnerVoteCount = pollResponseSummary?.winnerVoteCount + val totalVotes = pollResponseSummary?.totalVotes ?: 0 + + return when { + !informationData.sendState.isSent() -> { + createSendingPollViewState(question, pollCreationInfo) + } + informationData.pollResponseAggregatedSummary?.isClosed.orFalse() -> { + createEndedPollViewState(question, pollCreationInfo, pollResponseSummary, totalVotes, winnerVoteCount) + } + pollContent.getBestPollCreationInfo()?.isUndisclosed().orFalse() -> { + createUndisclosedPollViewState(question, pollCreationInfo, pollResponseSummary) + } + informationData.pollResponseAggregatedSummary?.myVote?.isNotEmpty().orFalse() -> { + createVotedPollViewState(question, pollCreationInfo, pollResponseSummary, totalVotes) + } + else -> { + createReadyPollViewState(question, pollCreationInfo, totalVotes) + } + } + } + + private fun createSendingPollViewState(question: EpoxyCharSequence, pollCreationInfo: PollCreationInfo?): PollViewState { + return PollViewState( + question = question, + totalVotes = stringProvider.getString(R.string.poll_no_votes_cast), + canVote = false, + optionViewStates = pollCreationInfo?.answers?.map { answer -> + PollOptionViewState.PollSending( + optionId = answer.id ?: "", + optionAnswer = answer.getBestAnswer() ?: "" + ) + }, + ) + } + + private fun createEndedPollViewState( + question: EpoxyCharSequence, + pollCreationInfo: PollCreationInfo?, + pollResponseSummary: PollResponseData?, + totalVotes: Int, + winnerVoteCount: Int?, + ): PollViewState { + return PollViewState( + question = question, + totalVotes = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, totalVotes, totalVotes), + canVote = false, + optionViewStates = pollCreationInfo?.answers?.map { answer -> + val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "") + PollOptionViewState.PollEnded( + optionId = answer.id ?: "", + optionAnswer = answer.getBestAnswer() ?: "", + voteCount = voteSummary?.total ?: 0, + votePercentage = voteSummary?.percentage ?: 0.0, + isWinner = winnerVoteCount != 0 && voteSummary?.total == winnerVoteCount + ) + }, + ) + } + + private fun createUndisclosedPollViewState( + question: EpoxyCharSequence, + pollCreationInfo: PollCreationInfo?, + pollResponseSummary: PollResponseData? + ): PollViewState { + return PollViewState( + question = question, + totalVotes = "", + canVote = true, + optionViewStates = pollCreationInfo?.answers?.map { answer -> + val isMyVote = pollResponseSummary?.myVote == answer.id + PollOptionViewState.PollUndisclosed( + optionId = answer.id ?: "", + optionAnswer = answer.getBestAnswer() ?: "", + isSelected = isMyVote + ) + }, + ) + } + + private fun createVotedPollViewState( + question: EpoxyCharSequence, + pollCreationInfo: PollCreationInfo?, + pollResponseSummary: PollResponseData?, + totalVotes: Int + ): PollViewState { + return PollViewState( + question = question, + totalVotes = stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, totalVotes, totalVotes), + canVote = true, + optionViewStates = pollCreationInfo?.answers?.map { answer -> + val isMyVote = pollResponseSummary?.myVote == answer.id + val voteSummary = pollResponseSummary?.getVoteSummaryOfAnOption(answer.id ?: "") + PollOptionViewState.PollVoted( + optionId = answer.id ?: "", + optionAnswer = answer.getBestAnswer() ?: "", + voteCount = voteSummary?.total ?: 0, + votePercentage = voteSummary?.percentage ?: 0.0, + isSelected = isMyVote + ) + }, + ) + } + + private fun createReadyPollViewState(question: EpoxyCharSequence, pollCreationInfo: PollCreationInfo?, totalVotes: Int): PollViewState { + val totalVotesText = if (totalVotes == 0) { + stringProvider.getString(R.string.poll_no_votes_cast) + } else { + stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, totalVotes, totalVotes) + } + return PollViewState( + question = question, + totalVotes = totalVotesText, + canVote = true, + optionViewStates = pollCreationInfo?.answers?.map { answer -> + PollOptionViewState.PollReady( + optionId = answer.id ?: "", + optionAnswer = answer.getBestAnswer() ?: "" + ) + }, + ) + } + + private fun createPollQuestion( + informationData: MessageInformationData, + question: String, + callback: TimelineEventController.Callback?, + ) = if (informationData.hasBeenEdited) { + annotateWithEdited(stringProvider, colorProvider, dimensionConverter, question, callback, informationData) + } else { + question + }.toEpoxyCharSequence() +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt index 554dd0ada8..9b24720c88 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -91,7 +91,10 @@ data class PollResponseData( val totalVotes: Int = 0, val winnerVoteCount: Int = 0, val isClosed: Boolean = false -) : Parcelable +) : Parcelable { + + fun getVoteSummaryOfAnOption(optionId: String) = votes?.get(optionId) +} @Parcelize data class PollVoteSummaryData( diff --git a/vector/src/main/java/im/vector/app/features/poll/PollState.kt b/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt similarity index 66% rename from vector/src/main/java/im/vector/app/features/poll/PollState.kt rename to vector/src/main/java/im/vector/app/features/poll/PollViewState.kt index 93cdb0ecbe..01947d8850 100644 --- a/vector/src/main/java/im/vector/app/features/poll/PollState.kt +++ b/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt @@ -16,12 +16,12 @@ package im.vector.app.features.poll -sealed interface PollState { - object Sending : PollState - object Ready : PollState - data class Voted(val votes: Int) : PollState - object Undisclosed : PollState - object Ended : PollState +import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence - fun isVotable() = this !is Sending && this !is Ended -} +data class PollViewState( + val question: EpoxyCharSequence, + val totalVotes: String, + val canVote: Boolean, + val optionViewStates: List?, +) diff --git a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt b/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt deleted file mode 100644 index be397e25ea..0000000000 --- a/vector/src/test/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemFactoryTest.kt +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Copyright (c) 2022 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.factory - -import com.airbnb.mvrx.test.MvRxTestRule -import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData -import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState -import im.vector.app.features.home.room.detail.timeline.item.PollResponseData -import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryData -import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout -import im.vector.app.features.poll.PollState -import io.mockk.mockk -import io.mockk.unmockkAll -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runTest -import org.amshove.kluent.shouldBe -import org.amshove.kluent.shouldBeEqualTo -import org.junit.After -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent -import org.matrix.android.sdk.api.session.room.model.message.PollAnswer -import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo -import org.matrix.android.sdk.api.session.room.model.message.PollQuestion -import org.matrix.android.sdk.api.session.room.model.message.PollType -import org.matrix.android.sdk.api.session.room.send.SendState - -private val A_MESSAGE_INFORMATION_DATA = MessageInformationData( - eventId = "eventId", - senderId = "senderId", - ageLocalTS = 0, - avatarUrl = "", - sendState = SendState.SENT, - messageLayout = TimelineMessageLayout.Default(showAvatar = true, showDisplayName = true, showTimestamp = true), - reactionsSummary = ReactionsSummaryData(), - sentByMe = true, -) - -private val A_POLL_RESPONSE_DATA = PollResponseData( - myVote = null, - votes = emptyMap(), -) - -private val A_POLL_CONTENT = MessagePollContent( - unstablePollCreationInfo = PollCreationInfo( - question = PollQuestion( - unstableQuestion = "What is your favourite coffee?" - ), - kind = PollType.UNDISCLOSED_UNSTABLE, - maxSelections = 1, - answers = listOf( - PollAnswer( - id = "5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", - unstableAnswer = "Double Espresso" - ), - PollAnswer( - id = "ec1a4db0-46d8-4d7a-9bb6-d80724715938", - unstableAnswer = "Macchiato" - ), - PollAnswer( - id = "3677ca8e-061b-40ab-bffe-b22e4e88fcad", - unstableAnswer = "Iced Coffee" - ), - ) - ) -) - -class PollItemFactoryTest { - - private val testDispatcher = UnconfinedTestDispatcher() - - @get:Rule - val mvRxTestRule = MvRxTestRule( - testDispatcher = testDispatcher // See https://github.com/airbnb/mavericks/issues/599 - ) - - private lateinit var pollItemFactory: PollItemFactory - - @Before - fun setup() { - // We are not going to test any UI related code - pollItemFactory = PollItemFactory( - stringProvider = mockk(), - avatarSizeProvider = mockk(), - colorProvider = mockk(), - dimensionConverter = mockk(), - ) - } - - @After - fun tearDown() { - unmockkAll() - } - - @Test - fun `given a sending poll state then PollState is Sending`() = runTest { - val sendingPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(sendState = SendState.SENDING) - pollItemFactory.createPollState( - informationData = sendingPollInformationData, - pollResponseSummary = A_POLL_RESPONSE_DATA, - pollContent = A_POLL_CONTENT, - ) shouldBe PollState.Sending - } - - @Test - fun `given a sent poll state when poll is closed then PollState is Ended`() = runTest { - val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true) - - pollItemFactory.createPollState( - informationData = A_MESSAGE_INFORMATION_DATA, - pollResponseSummary = closedPollSummary, - pollContent = A_POLL_CONTENT, - ) shouldBe PollState.Ended - } - - @Test - fun `given a sent poll when undisclosed poll type is selected then PollState is Undisclosed`() = runTest { - pollItemFactory.createPollState( - informationData = A_MESSAGE_INFORMATION_DATA, - pollResponseSummary = A_POLL_RESPONSE_DATA, - pollContent = A_POLL_CONTENT, - ) shouldBe PollState.Undisclosed - } - - @Test - fun `given a sent poll when my vote exists then PollState is Voted`() = runTest { - val votedPollData = A_POLL_RESPONSE_DATA.copy( - totalVotes = 1, - myVote = "5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", - ) - val disclosedPollContent = A_POLL_CONTENT.copy( - unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( - kind = PollType.DISCLOSED_UNSTABLE - ) - ) - - pollItemFactory.createPollState( - informationData = A_MESSAGE_INFORMATION_DATA, - pollResponseSummary = votedPollData, - pollContent = disclosedPollContent, - ) shouldBeEqualTo PollState.Voted(1) - } - - @Test - fun `given a sent poll when poll type is disclosed then PollState is Ready`() = runTest { - val disclosedPollContent = A_POLL_CONTENT.copy( - unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy( - kind = PollType.DISCLOSED_UNSTABLE - ) - ) - - pollItemFactory.createPollState( - informationData = A_MESSAGE_INFORMATION_DATA, - pollResponseSummary = A_POLL_RESPONSE_DATA, - pollContent = disclosedPollContent, - ) shouldBe PollState.Ready - } - - @Test - fun `given a sending poll then all option view states is PollSending`() = runTest { - with(pollItemFactory) { - A_POLL_CONTENT - .getBestPollCreationInfo() - ?.answers - ?.mapToOptions(PollState.Sending, A_MESSAGE_INFORMATION_DATA) - ?.forEachIndexed { index, pollOptionViewState -> - A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> - pollOptionViewState shouldBeEqualTo PollOptionViewState.PollSending(option.id ?: "", option.getBestAnswer() ?: "") - } - } - } - } - - @Test - fun `given a sent poll then all option view states is PollReady`() = runTest { - with(pollItemFactory) { - A_POLL_CONTENT - .getBestPollCreationInfo() - ?.answers - ?.mapToOptions(PollState.Sending, A_MESSAGE_INFORMATION_DATA) - ?.forEachIndexed { index, pollOptionViewState -> - A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> - pollOptionViewState shouldBeEqualTo PollOptionViewState.PollSending(option.id ?: "", option.getBestAnswer() ?: "") - } - } - } - } - - @Test - fun `given a sent poll when a vote is cast then all option view states is PollVoted`() = runTest { - with(pollItemFactory) { - A_POLL_CONTENT - .getBestPollCreationInfo() - ?.answers - ?.mapToOptions(PollState.Voted(1), A_MESSAGE_INFORMATION_DATA) - ?.forEachIndexed { index, pollOptionViewState -> - A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> - val voteSummary = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.votes?.get(option.id) - pollOptionViewState shouldBeEqualTo PollOptionViewState.PollVoted( - optionId = option.id ?: "", - optionAnswer = option.getBestAnswer() ?: "", - voteCount = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.totalVotes ?: 0, - votePercentage = voteSummary?.percentage ?: 0.0, - isSelected = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.myVote == option.id, - ) - } - } - } - } - - @Test - fun `given a sent poll when the poll is undisclosed then all option view states is PollUndisclosed`() = runTest { - with(pollItemFactory) { - A_POLL_CONTENT - .getBestPollCreationInfo() - ?.answers - ?.mapToOptions(PollState.Undisclosed, A_MESSAGE_INFORMATION_DATA) - ?.forEachIndexed { index, pollOptionViewState -> - A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> - pollOptionViewState shouldBeEqualTo PollOptionViewState.PollUndisclosed( - optionId = option.id ?: "", - optionAnswer = option.getBestAnswer() ?: "", - isSelected = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.myVote == option.id, - ) - } - } - } - } - - @Test - fun `given an ended poll then all option view states is Ended`() = runTest { - with(pollItemFactory) { - A_POLL_CONTENT - .getBestPollCreationInfo() - ?.answers - ?.mapToOptions(PollState.Ended, A_MESSAGE_INFORMATION_DATA) - ?.forEachIndexed { index, pollOptionViewState -> - A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.get(index)?.let { option -> - val voteSummary = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.votes?.get(option.id) - val voteCount = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.totalVotes ?: 0 - val winnerVoteCount = A_MESSAGE_INFORMATION_DATA.pollResponseAggregatedSummary?.winnerVoteCount ?: 0 - pollOptionViewState shouldBeEqualTo PollOptionViewState.PollEnded( - optionId = option.id ?: "", - optionAnswer = option.getBestAnswer() ?: "", - voteCount = voteCount, - votePercentage = voteSummary?.percentage ?: 0.0, - isWinner = winnerVoteCount != 0 && voteCount == winnerVoteCount, - ) - } - } - } - } - - @Test - fun `given a poll state when it is not Sending and not Ended then the poll is votable`() = runTest { - val sendingPollState = PollState.Sending - sendingPollState.isVotable() shouldBe false - val readyPollState = PollState.Ready - readyPollState.isVotable() shouldBe true - val votedPollState = PollState.Voted(1) - votedPollState.isVotable() shouldBe true - val undisclosedPollState = PollState.Undisclosed - undisclosedPollState.isVotable() shouldBe true - var endedPollState = PollState.Ended - endedPollState.isVotable() shouldBe false - } -} From e63fa2d83ffd7bdbe6bc462a4418390d048a8536 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 24 Jun 2022 17:28:59 +0300 Subject: [PATCH 33/71] Move epoxy related poll functions back to MessageItemFactory. --- .../timeline/factory/MessageItemFactory.kt | 12 +++++++- .../factory/PollItemViewStateFactory.kt | 30 ++++--------------- .../vector/app/features/poll/PollViewState.kt | 3 +- 3 files changed, 18 insertions(+), 27 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index d2032bb4c4..00c7d41160 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -238,7 +238,7 @@ class MessageItemFactory @Inject constructor( return PollItem_() .attributes(attributes) .eventId(informationData.eventId) - .pollQuestion(pollViewState.question) + .pollQuestion(createPollQuestion(informationData, pollViewState.question, callback)) .canVote(pollViewState.canVote) .totalVotesText(pollViewState.totalVotes) .optionViewStates(pollViewState.optionViewStates) @@ -248,6 +248,16 @@ class MessageItemFactory @Inject constructor( .callback(callback) } + private fun createPollQuestion( + informationData: MessageInformationData, + question: String, + callback: TimelineEventController.Callback?, + ) = if (informationData.hasBeenEdited) { + annotateWithEdited(stringProvider, colorProvider, dimensionConverter, question, callback, informationData) + } else { + question + }.toEpoxyCharSequence() + private fun buildAudioMessageItem( params: TimelineItemFactoryParams, messageContent: MessageAudioContent, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt index 8365f0710e..acedd79b1c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt @@ -17,17 +17,12 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.R -import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider -import im.vector.app.core.utils.DimensionConverter import im.vector.app.features.home.room.detail.timeline.TimelineEventController -import im.vector.app.features.home.room.detail.timeline.factory.MessageItemFactoryHelper.annotateWithEdited import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState import im.vector.app.features.home.room.detail.timeline.item.PollResponseData import im.vector.app.features.poll.PollViewState -import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence -import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo @@ -35,8 +30,6 @@ import javax.inject.Inject class PollItemViewStateFactory @Inject constructor( private val stringProvider: StringProvider, - private val colorProvider: ColorProvider, - private val dimensionConverter: DimensionConverter, ) { fun create( @@ -46,8 +39,7 @@ class PollItemViewStateFactory @Inject constructor( ): PollViewState { val pollCreationInfo = pollContent.getBestPollCreationInfo() - val questionText = pollCreationInfo?.question?.getBestQuestion().orEmpty() - val question = createPollQuestion(informationData, questionText, callback) + val question = pollCreationInfo?.question?.getBestQuestion().orEmpty() val pollResponseSummary = informationData.pollResponseAggregatedSummary val winnerVoteCount = pollResponseSummary?.winnerVoteCount @@ -72,7 +64,7 @@ class PollItemViewStateFactory @Inject constructor( } } - private fun createSendingPollViewState(question: EpoxyCharSequence, pollCreationInfo: PollCreationInfo?): PollViewState { + private fun createSendingPollViewState(question: String, pollCreationInfo: PollCreationInfo?): PollViewState { return PollViewState( question = question, totalVotes = stringProvider.getString(R.string.poll_no_votes_cast), @@ -87,7 +79,7 @@ class PollItemViewStateFactory @Inject constructor( } private fun createEndedPollViewState( - question: EpoxyCharSequence, + question: String, pollCreationInfo: PollCreationInfo?, pollResponseSummary: PollResponseData?, totalVotes: Int, @@ -111,7 +103,7 @@ class PollItemViewStateFactory @Inject constructor( } private fun createUndisclosedPollViewState( - question: EpoxyCharSequence, + question: String, pollCreationInfo: PollCreationInfo?, pollResponseSummary: PollResponseData? ): PollViewState { @@ -131,7 +123,7 @@ class PollItemViewStateFactory @Inject constructor( } private fun createVotedPollViewState( - question: EpoxyCharSequence, + question: String, pollCreationInfo: PollCreationInfo?, pollResponseSummary: PollResponseData?, totalVotes: Int @@ -154,7 +146,7 @@ class PollItemViewStateFactory @Inject constructor( ) } - private fun createReadyPollViewState(question: EpoxyCharSequence, pollCreationInfo: PollCreationInfo?, totalVotes: Int): PollViewState { + private fun createReadyPollViewState(question: String, pollCreationInfo: PollCreationInfo?, totalVotes: Int): PollViewState { val totalVotesText = if (totalVotes == 0) { stringProvider.getString(R.string.poll_no_votes_cast) } else { @@ -172,14 +164,4 @@ class PollItemViewStateFactory @Inject constructor( }, ) } - - private fun createPollQuestion( - informationData: MessageInformationData, - question: String, - callback: TimelineEventController.Callback?, - ) = if (informationData.hasBeenEdited) { - annotateWithEdited(stringProvider, colorProvider, dimensionConverter, question, callback, informationData) - } else { - question - }.toEpoxyCharSequence() } diff --git a/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt b/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt index 01947d8850..0f01d58c96 100644 --- a/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt +++ b/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt @@ -17,10 +17,9 @@ package im.vector.app.features.poll import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState -import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence data class PollViewState( - val question: EpoxyCharSequence, + val question: String, val totalVotes: String, val canVote: Boolean, val optionViewStates: List?, From f57c46de9a0859d513008803f1a83f08cce1cb51 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 24 Jun 2022 17:08:14 +0200 Subject: [PATCH 34/71] Remove non necessary @Synchronized annotations in LocationSharingServiceConnection --- .../app/features/location/LocationSharingServiceConnection.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt index 817216f0b4..d28778b708 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt @@ -74,22 +74,18 @@ class LocationSharingServiceConnection @Inject constructor( forwardErrorToCallbacks(error) } - @Synchronized private fun addCallback(callback: Callback) { callbacks.add(callback) } - @Synchronized private fun removeCallback(callback: Callback) { callbacks.remove(callback) } - @Synchronized private fun onCallbackActionNoArg(action: Callback.() -> Unit) { callbacks.forEach(action) } - @Synchronized private fun forwardErrorToCallbacks(error: Throwable) { callbacks.forEach { it.onLocationServiceError(error) } } From c581564bb18f52805fca9d430b32d88ea23f4945 Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 24 Jun 2022 17:09:33 +0200 Subject: [PATCH 35/71] Remove non necessary main Handler in LocationSharingService --- .../location/LocationSharingService.kt | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index ce3933cc36..3aaf444c9d 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt @@ -18,7 +18,6 @@ package im.vector.app.features.location import android.content.Intent import android.os.Binder -import android.os.Handler import android.os.IBinder import android.os.Parcelable import dagger.hilt.android.AndroidEntryPoint @@ -64,7 +63,6 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { private val roomArgsMap = mutableMapOf() var callback: Callback? = null private val jobs = mutableListOf() - private val mainHandler by lazy { Handler(mainLooper) } override fun onCreate() { super.onCreate() @@ -86,7 +84,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { startForeground(roomArgs.roomId.hashCode(), notification) // Send beacon info state event - launchInIO { session -> + launchWithActiveSession { session -> sendStartingLiveBeaconInfo(session, roomArgs) } } @@ -141,7 +139,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { beaconInfoEventId: String, locationData: LocationData ) { - launchInIO { session -> + launchWithActiveSession { session -> session.getRoom(roomId) ?.locationSharingService() ?.sendLiveLocation( @@ -195,23 +193,20 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { activeSessionHolder .getSafeActiveSession() ?.let { session -> - mainHandler.post { - val job = getLiveLocationShareSummaryUseCase.execute(roomId, eventId) - .distinctUntilChangedBy { it.isActive } - .filter { it.isActive == false } - .onEach { stopSharingLocation(roomId) } - .launchIn(session.coroutineScope) - jobs.add(job) - } + val job = getLiveLocationShareSummaryUseCase.execute(roomId, eventId) + .distinctUntilChangedBy { it.isActive } + .filter { it.isActive == false } + .onEach { stopSharingLocation(roomId) } + .launchIn(session.coroutineScope) + jobs.add(job) } } - private fun launchInIO(block: suspend CoroutineScope.(Session) -> Unit) = + private fun launchWithActiveSession(block: suspend CoroutineScope.(Session) -> Unit) = activeSessionHolder .getSafeActiveSession() ?.let { session -> session.coroutineScope.launch( - context = session.coroutineDispatchers.io, block = { block(session) } ) } From d3fb12da194906f15a70bf05d86950eed983618c Mon Sep 17 00:00:00 2001 From: Maxime NATUREL Date: Fri, 24 Jun 2022 17:37:17 +0200 Subject: [PATCH 36/71] Copy lists/maps when iterating to avoid concurrent exceptions --- .../im/vector/app/features/location/LocationSharingService.kt | 4 ++-- .../app/features/location/LocationSharingServiceConnection.kt | 4 ++-- .../java/im/vector/app/features/location/LocationTracker.kt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index 3aaf444c9d..07213ae992 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt @@ -129,7 +129,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { Timber.i("### LocationSharingService.onLocationUpdate. Uncertainty: ${locationData.uncertainty}") // Emit location update to all rooms in which live location sharing is active - roomArgsMap.forEach { item -> + roomArgsMap.toMap().forEach { item -> sendLiveLocation(item.value.roomId, item.key, locationData) } } @@ -184,7 +184,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { @Synchronized private fun removeRoomArgs(roomId: String) { - roomArgsMap + roomArgsMap.toMap() .filter { it.value.roomId == roomId } .forEach { roomArgsMap.remove(it.key) } } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt index d28778b708..db79564462 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingServiceConnection.kt @@ -83,10 +83,10 @@ class LocationSharingServiceConnection @Inject constructor( } private fun onCallbackActionNoArg(action: Callback.() -> Unit) { - callbacks.forEach(action) + callbacks.toList().forEach(action) } private fun forwardErrorToCallbacks(error: Throwable) { - callbacks.forEach { it.onLocationServiceError(error) } + callbacks.toList().forEach { it.onLocationServiceError(error) } } } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt index cdf13a7004..013014ca2a 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationTracker.kt @@ -217,7 +217,7 @@ class LocationTracker @Inject constructor( @Synchronized private fun onNoLocationProviderAvailable() { - callbacks.forEach { + callbacks.toList().forEach { try { it.onNoLocationProviderAvailable() } catch (error: Exception) { @@ -228,7 +228,7 @@ class LocationTracker @Inject constructor( @Synchronized private fun onLocationUpdate(locationData: LocationData) { - callbacks.forEach { + callbacks.toList().forEach { try { it.onLocationUpdate(locationData) } catch (error: Exception) { From 622ada71254f62a4849253afa3560f014dd0d339 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 16 Jun 2022 16:42:15 +0200 Subject: [PATCH 37/71] ensure ageLocalTs is set --- .../verification/VerificationMessageProcessor.kt | 2 +- .../database/helper/ThreadSummaryHelper.kt | 2 +- .../sdk/internal/database/mapper/EventMapper.kt | 2 +- .../session/room/membership/LoadRoomMembersTask.kt | 2 +- .../relation/threads/FetchThreadTimelineTask.kt | 3 ++- .../room/timeline/TokenChunkEventPersistor.kt | 4 ++-- .../session/sync/handler/room/RoomSyncHandler.kt | 14 ++++++++------ .../sync/handler/room/ThreadsAwarenessHandler.kt | 6 ++++-- 8 files changed, 20 insertions(+), 15 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt index 9f123f0c08..821663bcff 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/verification/VerificationMessageProcessor.kt @@ -62,7 +62,7 @@ internal class VerificationMessageProcessor @Inject constructor( // If the request is in the future by more than 5 minutes or more than 10 minutes in the past, // the message should be ignored by the receiver. - if (event.ageLocalTs != null && !VerificationService.isValidRequest(event.ageLocalTs, clock.epochMillis())) return Unit.also { + if (!VerificationService.isValidRequest(event.ageLocalTs, clock.epochMillis())) return Unit.also { Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated age:$event.ageLocalTs ms") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt index 79a99cdfac..0754d0127f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/helper/ThreadSummaryHelper.kt @@ -271,7 +271,7 @@ private fun HashMap.addSenderState(realm: Realm, roo * Create an EventEntity for the root thread event or get an existing one. */ private fun createEventEntity(realm: Realm, roomId: String, event: Event, currentTimeMillis: Long): EventEntity { - val ageLocalTs = event.unsignedData?.age?.let { currentTimeMillis - it } + val ageLocalTs = event.unsignedData?.age?.let { currentTimeMillis - it } ?: currentTimeMillis return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt index 5b60c53642..0f0a847c78 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/EventMapper.kt @@ -130,7 +130,7 @@ internal fun EventEntity.asDomain(castJsonNumbers: Boolean = false): Event { internal fun Event.toEntity( roomId: String, sendState: SendState, - ageLocalTs: Long?, + ageLocalTs: Long, contentToInject: String? = null ): EventEntity { return EventMapper.map(this, roomId).apply { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt index 15d0889255..f33c8d29be 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt @@ -114,7 +114,7 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( if (roomMemberEvent.eventId == null || roomMemberEvent.stateKey == null || roomMemberEvent.type == null) { continue } - val ageLocalTs = roomMemberEvent.unsignedData?.age?.let { now - it } + val ageLocalTs = roomMemberEvent.unsignedData?.age?.let { now - it } ?: now val eventEntity = roomMemberEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) CurrentStateEventEntity.getOrCreate( realm, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt index bad734173e..23f92befb1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/relation/threads/FetchThreadTimelineTask.kt @@ -209,7 +209,8 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor( * Create an EventEntity to be added in the TimelineEventEntity. */ private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity { - val ageLocalTs = event.unsignedData?.age?.let { clock.epochMillis() - it } + val now = clock.epochMillis() + val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt index fd1703dbc8..36552a21f5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TokenChunkEventPersistor.kt @@ -142,7 +142,7 @@ internal class TokenChunkEventPersistor @Inject constructor( val now = clock.epochMillis() stateEvents?.forEach { stateEvent -> - val ageLocalTs = stateEvent.unsignedData?.age?.let { now - it } + val ageLocalTs = stateEvent.unsignedData?.age?.let { now - it } ?: now val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) currentChunk.addStateEvent(roomId, stateEventEntity, direction) if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null) { @@ -155,7 +155,7 @@ internal class TokenChunkEventPersistor @Inject constructor( if (event.eventId == null || event.senderId == null) { return@forEach } - val ageLocalTs = event.unsignedData?.age?.let { now - it } + val ageLocalTs = event.unsignedData?.age?.let { now - it } ?: now val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION) if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) { val contentToUse = if (direction == PaginationDirection.BACKWARDS) { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt index f99fe96410..52bdf637ef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt @@ -244,7 +244,7 @@ internal class RoomSyncHandler @Inject constructor( if (event.eventId == null || event.stateKey == null || event.type == null) { continue } - val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } + val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } ?: syncLocalTimestampMillis val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) Timber.v("## received state event ${event.type} and key ${event.stateKey}") CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { @@ -306,7 +306,7 @@ internal class RoomSyncHandler @Inject constructor( if (event.stateKey == null || event.type == null) { return@forEach } - val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } + val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } ?: syncLocalTimestampMillis val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { eventId = eventEntity.eventId @@ -336,7 +336,7 @@ internal class RoomSyncHandler @Inject constructor( if (event.eventId == null || event.stateKey == null || event.type == null) { continue } - val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } + val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } ?: syncLocalTimestampMillis val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { eventId = event.eventId @@ -348,7 +348,7 @@ internal class RoomSyncHandler @Inject constructor( if (event.eventId == null || event.senderId == null || event.type == null) { continue } - val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } + val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } ?: syncLocalTimestampMillis val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) if (event.stateKey != null) { CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { @@ -401,7 +401,10 @@ internal class RoomSyncHandler @Inject constructor( for (rawEvent in eventList) { // It's annoying roomId is not there, but lot of code rely on it. // And had to do it now as copy would delete all decryption results.. - val event = rawEvent.copy(roomId = roomId) + val ageLocalTs = rawEvent.unsignedData?.age?.let { syncLocalTimestampMillis - it } ?: syncLocalTimestampMillis + val event = rawEvent.copy(roomId = roomId).also { + it.ageLocalTs = ageLocalTs + } if (event.eventId == null || event.senderId == null || event.type == null) { continue } @@ -423,7 +426,6 @@ internal class RoomSyncHandler @Inject constructor( contentToInject = threadsAwarenessHandler.makeEventThreadAware(realm, roomId, event) } - val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs, contentToInject).copyToRealmOrIgnore(realm, insertType) if (event.stateKey != null) { CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt index 8c7557a5b8..70553359ff 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/ThreadsAwarenessHandler.kt @@ -53,6 +53,7 @@ import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask import org.matrix.android.sdk.internal.util.awaitTransaction +import org.matrix.android.sdk.internal.util.time.Clock import javax.inject.Inject /** @@ -64,7 +65,8 @@ internal class ThreadsAwarenessHandler @Inject constructor( private val permalinkFactory: PermalinkFactory, @SessionDatabase private val monarchy: Monarchy, private val lightweightSettingsStorage: LightweightSettingsStorage, - private val getEventTask: GetEventTask + private val getEventTask: GetEventTask, + private val clock: Clock, ) { // This caching is responsible to improve the performance when we receive a root event @@ -120,7 +122,7 @@ internal class ThreadsAwarenessHandler @Inject constructor( private suspend fun fetchThreadsEvents(threadsToFetch: Map) { val eventEntityList = threadsToFetch.mapNotNull { (eventId, roomId) -> fetchEvent(eventId, roomId)?.let { - it.toEntity(roomId, SendState.SYNCED, it.ageLocalTs) + it.toEntity(roomId, SendState.SYNCED, it.ageLocalTs ?: clock.epochMillis()) } } From 142c87314ca4754f59888016d52589f4bf1c59a1 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 16 Jun 2022 16:44:30 +0200 Subject: [PATCH 38/71] show option to accept other verif not ready --- .../crypto/verification/VerificationAction.kt | 2 ++ .../VerificationBottomSheetViewModel.kt | 21 +++++++++++++++++++ .../VerificationChooseMethodController.kt | 17 +++++++++++++++ .../VerificationChooseMethodFragment.kt | 12 +++++++++++ .../VerificationChooseMethodViewModel.kt | 6 ++++-- 5 files changed, 56 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt index c4ae2d278b..1b18117cf3 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt @@ -30,6 +30,8 @@ sealed class VerificationAction : VectorViewModelAction { data class GotItConclusion(val verified: Boolean) : VerificationAction() object SkipVerification : VerificationAction() object VerifyFromPassphrase : VerificationAction() + object ReadyPendingVerification : VerificationAction() + object CancelPendingVerification : VerificationAction() data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction() object CancelledFromSsss : VerificationAction() object SecuredStorageHasBeenReset : VerificationAction() diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt index 46f7adb911..b8146b8041 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -360,6 +360,27 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( as? SasVerificationTransaction) ?.shortCodeDoesNotMatch() } + is VerificationAction.ReadyPendingVerification -> { + state.pendingRequest.invoke()?.let { request -> + // will only be there for dm verif + if (state.roomId != null) { + session.cryptoService().verificationService() + .readyPendingVerificationInDMs( + supportedVerificationMethodsProvider.provide(), + state.otherUserId, + state.roomId, + request.transactionId ?: "" + ) + } + } + } + is VerificationAction.CancelPendingVerification -> { + state.pendingRequest.invoke()?.let { + session.cryptoService().verificationService() + .cancelVerificationRequest(it) + } + _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss) + } is VerificationAction.GotItConclusion -> { if (state.isVerificationRequired && !action.verified) { // we should go back to first screen diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt index acc8cf61b9..85dbd7d462 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt @@ -21,6 +21,7 @@ import im.vector.app.R import im.vector.app.core.epoxy.bottomSheetDividerItem import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.genericButtonItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationQrCodeItem @@ -108,6 +109,20 @@ class VerificationChooseMethodController @Inject constructor( iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) listener { host.listener?.doVerifyBySas() } } + } else if (!state.isReadied) { + // a bit of a special case, if you tapped on the timeline cell but not on a button + genericButtonItem { + id("accept_request") + textColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) + text(host.stringProvider.getString(R.string.action_accept)) + buttonClickAction { host.listener?.acceptRequest() } + } + genericButtonItem { + id("decline_request") + textColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) + text(host.stringProvider.getString(R.string.action_decline)) + buttonClickAction { host.listener?.declineRequest() } + } } if (state.isMe && state.canCrossSign) { @@ -131,5 +146,7 @@ class VerificationChooseMethodController @Inject constructor( fun openCamera() fun doVerifyBySas() fun onClickOnWasNotMe() + fun acceptRequest() + fun declineRequest() } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodFragment.kt index cf6bcc58c0..f8f2145406 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodFragment.kt @@ -100,6 +100,18 @@ class VerificationChooseMethodFragment @Inject constructor( sharedViewModel.itWasNotMe() } + override fun acceptRequest() { + withState(viewModel) { + sharedViewModel.handle(VerificationAction.ReadyPendingVerification) + } + } + + override fun declineRequest() { + withState(viewModel) { + sharedViewModel.handle(VerificationAction.CancelPendingVerification) + } + } + private fun doOpenQRCodeScanner() { QrCodeScannerActivity.startForResult(requireActivity(), scanActivityResultLauncher) } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt index a1f902f8f4..dec0a773df 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt @@ -44,7 +44,8 @@ data class VerificationChooseMethodViewState( val qrCodeText: String? = null, val sasModeAvailable: Boolean = false, val isMe: Boolean = false, - val canCrossSign: Boolean = false + val canCrossSign: Boolean = false, + val isReadied: Boolean = false ) : MavericksState class VerificationChooseMethodViewModel @AssistedInject constructor( @@ -81,7 +82,8 @@ class VerificationChooseMethodViewModel @AssistedInject constructor( copy( otherCanShowQrCode = pvr?.otherCanShowQrCode().orFalse(), otherCanScanQrCode = pvr?.otherCanScanQrCode().orFalse(), - sasModeAvailable = pvr?.isSasSupported().orFalse() + sasModeAvailable = pvr?.isSasSupported().orFalse(), + isReadied = pvr?.isReady ?: false, ) } } From c4c62acdaaae1163ca363249fe77f177e83a8197 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 17 Jun 2022 10:49:56 +0200 Subject: [PATCH 39/71] Add change log --- changelog.d/6328.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6328.bugfix diff --git a/changelog.d/6328.bugfix b/changelog.d/6328.bugfix new file mode 100644 index 0000000000..7a41996e57 --- /dev/null +++ b/changelog.d/6328.bugfix @@ -0,0 +1 @@ +Fix | Some user verification requests couldn't be accepted/declined From 9929d6a4ebf2655139b27625bc321d314911f602 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 27 Jun 2022 10:13:18 +0200 Subject: [PATCH 40/71] Update button design --- .../ButtonPositiveDestructiveButtonBarItem.kt | 59 +++++++++++++++++++ .../VerificationChooseMethodController.kt | 19 +++--- .../item_positive_destrutive_buttons.xml | 25 ++++++++ 3 files changed, 91 insertions(+), 12 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/ui/list/ButtonPositiveDestructiveButtonBarItem.kt create mode 100644 vector/src/main/res/layout/item_positive_destrutive_buttons.xml diff --git a/vector/src/main/java/im/vector/app/core/ui/list/ButtonPositiveDestructiveButtonBarItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/ButtonPositiveDestructiveButtonBarItem.kt new file mode 100644 index 0000000000..95c1a4457d --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/list/ButtonPositiveDestructiveButtonBarItem.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.vector.app.core.ui.list + +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.google.android.material.button.MaterialButton +import im.vector.app.R +import im.vector.app.core.epoxy.ClickListener +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.epoxy.onClick +import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence + +/** + * A generic button list item. + */ +@EpoxyModelClass(layout = R.layout.item_positive_destrutive_buttons) +abstract class ButtonPositiveDestructiveButtonBarItem : VectorEpoxyModel() { + + @EpoxyAttribute + var positiveText: EpoxyCharSequence? = null + + @EpoxyAttribute + var destructiveText: EpoxyCharSequence? = null + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var positiveButtonClickAction: ClickListener? = null + + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var destructiveButtonClickAction: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + positiveText?.charSequence?.let { holder.positiveButton.text = it } + destructiveText?.charSequence?.let { holder.destructiveButton.text = it } + + holder.positiveButton.onClick(positiveButtonClickAction) + holder.destructiveButton.onClick(destructiveButtonClickAction) + } + + class Holder : VectorEpoxyHolder() { + val destructiveButton by bind(R.id.destructive_button) + val positiveButton by bind(R.id.positive_button) + } +} diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt index 85dbd7d462..d8fb5b81c4 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/choose/VerificationChooseMethodController.kt @@ -21,7 +21,7 @@ import im.vector.app.R import im.vector.app.core.epoxy.bottomSheetDividerItem import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider -import im.vector.app.core.ui.list.genericButtonItem +import im.vector.app.core.ui.list.buttonPositiveDestructiveButtonBarItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationQrCodeItem @@ -111,17 +111,12 @@ class VerificationChooseMethodController @Inject constructor( } } else if (!state.isReadied) { // a bit of a special case, if you tapped on the timeline cell but not on a button - genericButtonItem { - id("accept_request") - textColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary)) - text(host.stringProvider.getString(R.string.action_accept)) - buttonClickAction { host.listener?.acceptRequest() } - } - genericButtonItem { - id("decline_request") - textColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) - text(host.stringProvider.getString(R.string.action_decline)) - buttonClickAction { host.listener?.declineRequest() } + buttonPositiveDestructiveButtonBarItem { + id("accept_decline") + positiveText(host.stringProvider.getString(R.string.action_accept).toEpoxyCharSequence()) + destructiveText(host.stringProvider.getString(R.string.action_decline).toEpoxyCharSequence()) + positiveButtonClickAction { host.listener?.acceptRequest() } + destructiveButtonClickAction { host.listener?.declineRequest() } } } diff --git a/vector/src/main/res/layout/item_positive_destrutive_buttons.xml b/vector/src/main/res/layout/item_positive_destrutive_buttons.xml new file mode 100644 index 0000000000..1e0ab34458 --- /dev/null +++ b/vector/src/main/res/layout/item_positive_destrutive_buttons.xml @@ -0,0 +1,25 @@ + + + +