diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f6a1906394..1c0491fda4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,8 +8,9 @@ on: # Enrich gradle.properties for CI/CD env: CI_GRADLE_ARG_PROPERTIES: > - -Porg.gradle.jvmargs=-Xmx2g + -Porg.gradle.jvmargs=-Xmx4g -Porg.gradle.parallel=false + --no-daemon jobs: debug: diff --git a/.github/workflows/post-pr.yml b/.github/workflows/post-pr.yml index 6040fd5f78..a7f1d6f204 100644 --- a/.github/workflows/post-pr.yml +++ b/.github/workflows/post-pr.yml @@ -13,6 +13,7 @@ env: CI_GRADLE_ARG_PROPERTIES: > -Porg.gradle.jvmargs=-Xmx4g -Porg.gradle.parallel=false + --no-daemon jobs: diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 89abac5b72..d7f5ce8b57 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -9,6 +9,8 @@ on: env: CI_GRADLE_ARG_PROPERTIES: > -Porg.gradle.jvmargs=-Xmx4g + -Porg.gradle.parallel=false + --no-daemon jobs: check: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e803319423..1a9cc5c239 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,8 +8,9 @@ on: # Enrich gradle.properties for CI/CD env: CI_GRADLE_ARG_PROPERTIES: > - -Porg.gradle.jvmargs=-Xmx2g + -Porg.gradle.jvmargs=-Xmx4g -Porg.gradle.parallel=false + --no-daemon jobs: tests: diff --git a/build.gradle b/build.gradle index c6654499ca..0244080ad0 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 @@ -43,7 +43,7 @@ plugins { id "io.gitlab.arturbosch.detekt" version "1.20.0" // Dependency Analysis - id 'com.autonomousapps.dependency-analysis' version "1.8.0" + id 'com.autonomousapps.dependency-analysis' version "1.9.0" } // https://github.com/jeremylong/DependencyCheck @@ -267,6 +267,8 @@ dependencyAnalysis { onUnusedDependencies { // False positives exclude( + "androidx.fragment:fragment-testing", + "com.facebook.soloader:soloader", "com.vanniktech:emoji-google", "com.vanniktech:emoji-material", "org.maplibre.gl:android-plugin-annotation-v9", 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. 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 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 diff --git a/changelog.d/6364.feature b/changelog.d/6364.feature new file mode 100644 index 0000000000..207d6d141b --- /dev/null +++ b/changelog.d/6364.feature @@ -0,0 +1 @@ +[Location sharing] - Stop any active live before starting a new one 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 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 diff --git a/changelog.d/6396.doc b/changelog.d/6396.doc new file mode 100644 index 0000000000..9b876d74af --- /dev/null +++ b/changelog.d/6396.doc @@ -0,0 +1 @@ +Update the PR process doc to come back to one reviewer with optional additional reviewers. \ No newline at end of file diff --git a/dependencies.gradle b/dependencies.gradle index 297e17be0a..db9278b975 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -21,6 +21,7 @@ def markwon = "4.6.2" def moshi = "1.13.0" def lifecycle = "2.4.1" def flowBinding = "1.2.0" +def flipper = "0.151.1" def epoxy = "4.6.2" def mavericks = "2.7.0" def glide = "4.13.2" @@ -91,6 +92,10 @@ ext.libs = [ 'hiltAndroidTesting' : "com.google.dagger:hilt-android-testing:$dagger", 'hiltCompiler' : "com.google.dagger:hilt-compiler:$dagger" ], + flipper : [ + 'flipper' : "com.facebook.flipper:flipper:$flipper", + 'flipperNetworkPlugin' : "com.facebook.flipper:flipper-network-plugin:$flipper", + ], squareup : [ 'moshi' : "com.squareup.moshi:moshi:$moshi", 'moshiKt' : "com.squareup.moshi:moshi-kotlin:$moshi", diff --git a/docs/pull_request.md b/docs/pull_request.md index d2d2bb7a3b..eebf2814a9 100644 --- a/docs/pull_request.md +++ b/docs/pull_request.md @@ -83,15 +83,16 @@ Exceptions can occur: ##### PR Review Assignment -We use automatic assignment for PR reviews. A PR is automatically routed by GitHub to 2 team members using the round robin algorithm. The process is the following: +We use automatic assignment for PR reviews. **A PR is automatically routed by GitHub to one team member** using the round robin algorithm. Additional reviewers can be used for complex changes or when the first reviewer is not confident enough on the changes. +The process is the following: -- The PR creator can assign specific people if they have another Android developer in their team or they think a specific reviewer should take a look at the PR. -- If there are missing reviewers, the PR creator assigns the [element-android-reviewers](https://github.com/orgs/vector-im/teams/element-android-reviewers) team as a reviewer. -- GitHub automatically assigns other reviewers. If one of the chosen reviewers is not available (holiday, etc.), remove them and set again the team, GitHub will select another reviewer. +- The PR creator selects the [element-android-reviewers](https://github.com/orgs/vector-im/teams/element-android-reviewers) team as a reviewer. +- GitHub automatically assign the reviewer. If the reviewer is not available (holiday, etc.), remove them and set again the team, GitHub will select another reviewer. +- Alternatively, the PR creator can directly assign specific people if they have another Android developer in their team or they think a specific reviewer should take a look at their PR. - Reviewers get a notification to make the review: they review the code following the good practice (see the rest of this document). - After making their own review, if they feel not confident enough, they can ask another person for a full review, or they can tag someone within a PR comment to check specific lines. -For PRs coming from the community, the issue wrangler can assign either the team [element-android-reviewers](https://github.com/orgs/vector-im/teams/element-android-reviewers) or any members directly. +For PRs coming from the community, the issue wrangler can assign either the team [element-android-reviewers](https://github.com/orgs/vector-im/teams/element-android-reviewers) or any member directly. ##### PR review time @@ -102,6 +103,7 @@ Some tips to achieve it: - Set up your GH notifications correctly - Check your pulls page: [https://github.com/pulls](https://github.com/pulls) - Check your pending assigned PRs before starting or resuming your day to day tasks +- If you are busy with high priority tasks, inform the author. They will find another developer It is hard to define a deadline for a review. It depends on the PR size and the complexity. Let's start with a goal of 24h (working day!) for a PR smaller than 500 lines. If bigger, the submitter and the reviewer should discuss. 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..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,9 +16,11 @@ 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 +import org.matrix.android.sdk.api.util.Optional /** * Manage all location sharing related features. @@ -59,5 +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/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..e0a7846167 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;u=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..30420fd3c7 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;u=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/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/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..0a6d4bf833 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 = currentTimeMillis - (event.unsignedData?.age ?: 0) 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/database/migration/MigrateSessionTo030.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo030.kt index b9c611f5dd..5d24b1433c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo030.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/migration/MigrateSessionTo030.kt @@ -52,6 +52,10 @@ internal class MigrateSessionTo030(realm: DynamicRealm) : RealmMigrator(realm, 3 timelineEvents.deleteAllFromRealm() } chunks.deleteAllFromRealm() - Timber.d("MigrateSessionTo030: $nbOfDeletedChunks deleted chunk(s), $nbOfDeletedTimelineEvents deleted TimelineEvent(s) and $nbOfDeletedEvents deleted Event(s).") + Timber.d( + "MigrateSessionTo030: $nbOfDeletedChunks deleted chunk(s)," + + " $nbOfDeletedTimelineEvents deleted TimelineEvent(s)" + + " and $nbOfDeletedEvents deleted Event(s)." + ) } } 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..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 @@ -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) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 271e82a1e0..c4d37d124b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -51,10 +51,14 @@ import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDire import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask +import org.matrix.android.sdk.internal.session.room.location.CheckIfExistingActiveLiveTask +import org.matrix.android.sdk.internal.session.room.location.DefaultCheckIfExistingActiveLiveTask +import org.matrix.android.sdk.internal.session.room.location.DefaultGetActiveBeaconInfoForUserTask import org.matrix.android.sdk.internal.session.room.location.DefaultSendLiveLocationTask import org.matrix.android.sdk.internal.session.room.location.DefaultSendStaticLocationTask import org.matrix.android.sdk.internal.session.room.location.DefaultStartLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.DefaultStopLiveLocationShareTask +import org.matrix.android.sdk.internal.session.room.location.GetActiveBeaconInfoForUserTask import org.matrix.android.sdk.internal.session.room.location.SendLiveLocationTask import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask @@ -319,4 +323,10 @@ internal abstract class RoomModule { @Binds abstract fun bindSendLiveLocationTask(task: DefaultSendLiveLocationTask): SendLiveLocationTask + + @Binds + abstract fun bindGetActiveBeaconInfoForUserTask(task: DefaultGetActiveBeaconInfoForUserTask): GetActiveBeaconInfoForUserTask + + @Binds + abstract fun bindCheckIfExistingActiveLiveTask(task: DefaultCheckIfExistingActiveLiveTask): CheckIfExistingActiveLiveTask } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/CheckIfExistingActiveLiveTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/CheckIfExistingActiveLiveTask.kt new file mode 100644 index 0000000000..228a046f53 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/CheckIfExistingActiveLiveTask.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.location + +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface CheckIfExistingActiveLiveTask : Task { + data class Params( + val roomId: String, + ) +} + +internal class DefaultCheckIfExistingActiveLiveTask @Inject constructor( + private val getActiveBeaconInfoForUserTask: GetActiveBeaconInfoForUserTask, +) : CheckIfExistingActiveLiveTask { + + override suspend fun execute(params: CheckIfExistingActiveLiveTask.Params): Boolean { + val getActiveBeaconTaskParams = GetActiveBeaconInfoForUserTask.Params( + roomId = params.roomId + ) + return getActiveBeaconInfoForUserTask.execute(getActiveBeaconTaskParams) + ?.getClearContent() + ?.toModel() + ?.isLive + .orFalse() + } +} 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..20320cad23 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,9 +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.findRunningLiveInRoom +import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase internal class DefaultLocationSharingService @AssistedInject constructor( @@ -37,6 +41,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor( private val sendLiveLocationTask: SendLiveLocationTask, private val startLiveLocationShareTask: StartLiveLocationShareTask, private val stopLiveLocationShareTask: StopLiveLocationShareTask, + private val checkIfExistingActiveLiveTask: CheckIfExistingActiveLiveTask, private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper, ) : LocationSharingService { @@ -68,6 +73,13 @@ internal class DefaultLocationSharingService @AssistedInject constructor( } override suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult { + // Ensure to stop any active live before starting a new one + if (checkIfExistingActiveLive()) { + val result = stopLiveLocationShare() + if (result is UpdateLiveLocationShareResult.Failure) { + return result + } + } val params = StartLiveLocationShareTask.Params( roomId = roomId, timeoutMillis = timeoutMillis @@ -75,6 +87,13 @@ internal class DefaultLocationSharingService @AssistedInject constructor( return startLiveLocationShareTask.execute(params) } + private suspend fun checkIfExistingActiveLive(): Boolean { + val params = CheckIfExistingActiveLiveTask.Params( + roomId = roomId + ) + return checkIfExistingActiveLiveTask.execute(params) + } + override suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult { val params = StopLiveLocationShareTask.Params( roomId = roomId, @@ -88,4 +107,15 @@ internal class DefaultLocationSharingService @AssistedInject constructor( 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/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt new file mode 100644 index 0000000000..a8d955af1d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/GetActiveBeaconInfoForUserTask.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.location + +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.query.QueryStringValue +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent +import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource +import org.matrix.android.sdk.internal.task.Task +import javax.inject.Inject + +internal interface GetActiveBeaconInfoForUserTask : Task { + data class Params( + val roomId: String, + ) +} + +internal class DefaultGetActiveBeaconInfoForUserTask @Inject constructor( + @UserId private val userId: String, + private val stateEventDataSource: StateEventDataSource, +) : GetActiveBeaconInfoForUserTask { + + override suspend fun execute(params: GetActiveBeaconInfoForUserTask.Params): Event? { + return EventType.STATE_ROOM_BEACON_INFO + .mapNotNull { + stateEventDataSource.getStateEvent( + roomId = params.roomId, + eventType = it, + stateKey = QueryStringValue.Equals(userId) + ) + } + .firstOrNull { beaconInfoEvent -> + beaconInfoEvent.getClearContent()?.toModel()?.isLive.orFalse() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt index d0e7ff3f82..da5fd76940 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/location/StopLiveLocationShareTask.kt @@ -16,17 +16,13 @@ package org.matrix.android.sdk.internal.session.room.location -import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent -import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.session.room.state.SendStateTask -import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource import org.matrix.android.sdk.internal.task.Task import javax.inject.Inject @@ -37,13 +33,12 @@ internal interface StopLiveLocationShareTask : Task() ?: return getResultForIncorrectBeaconInfoEvent() val updatedContent = content.copy(isLive = false).toContent() @@ -68,17 +63,10 @@ internal class DefaultStopLiveLocationShareTask @Inject constructor( private fun getResultForIncorrectBeaconInfoEvent() = UpdateLiveLocationShareResult.Failure(Exception("incorrect last beacon info event")) - private fun getLiveLocationBeaconInfoForUser(userId: String, roomId: String): Event? { - return EventType.STATE_ROOM_BEACON_INFO - .mapNotNull { - stateEventDataSource.getStateEvent( - roomId = roomId, - eventType = it, - stateKey = QueryStringValue.Equals(userId) - ) - } - .firstOrNull { beaconInfoEvent -> - beaconInfoEvent.getClearContent()?.toModel()?.isLive.orFalse() - } + private suspend fun getActiveLiveLocationBeaconInfoForUser(roomId: String): Event? { + val params = GetActiveBeaconInfoForUserTask.Params( + roomId = roomId + ) + return getActiveBeaconInfoForUserTask.execute(params) } } 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 b7670499ae..7052eb23e2 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 @@ -116,7 +116,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 = now - (roomMemberEvent.unsignedData?.age ?: 0) 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..bac810f424 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 = now - (event.unsignedData?.age ?: 0) 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/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/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt index aef9e24c8b..7c662444e4 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/GetEventTask.kt @@ -61,7 +61,7 @@ internal class DefaultGetEventTask @Inject constructor( } } - event.ageLocalTs = event.unsignedData?.age?.let { clock.epochMillis() - it } + event.ageLocalTs = clock.epochMillis() - (event.unsignedData?.age ?: 0) return event } 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..ea22f8cd78 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 = now - (stateEvent.unsignedData?.age ?: 0) 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 = now - (event.unsignedData?.age ?: 0) 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..30e1ec6679 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 = syncLocalTimestampMillis - (event.unsignedData?.age ?: 0) 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 = syncLocalTimestampMillis - (event.unsignedData?.age ?: 0) 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 = syncLocalTimestampMillis - (event.unsignedData?.age ?: 0) 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 = syncLocalTimestampMillis - (event.unsignedData?.age ?: 0) 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 = syncLocalTimestampMillis - (rawEvent.unsignedData?.age ?: 0) + 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()) } } 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/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultCheckIfExistingActiveLiveTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultCheckIfExistingActiveLiveTaskTest.kt new file mode 100644 index 0000000000..3198392eab --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultCheckIfExistingActiveLiveTaskTest.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.location + +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.Test +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent +import org.matrix.android.sdk.test.fakes.FakeGetActiveBeaconInfoForUserTask + +private const val A_USER_ID = "user-id" +private const val A_ROOM_ID = "room-id" +private const val A_TIMEOUT = 15_000L +private const val AN_EPOCH = 1655210176L + +@ExperimentalCoroutinesApi +class DefaultCheckIfExistingActiveLiveTaskTest { + + private val fakeGetActiveBeaconInfoForUserTask = FakeGetActiveBeaconInfoForUserTask() + + private val defaultCheckIfExistingActiveLiveTask = DefaultCheckIfExistingActiveLiveTask( + getActiveBeaconInfoForUserTask = fakeGetActiveBeaconInfoForUserTask + ) + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given parameters and existing active live event when calling the task then result is true`() = runTest { + val params = CheckIfExistingActiveLiveTask.Params( + roomId = A_ROOM_ID + ) + val currentStateEvent = Event( + stateKey = A_USER_ID, + content = MessageBeaconInfoContent( + timeout = A_TIMEOUT, + isLive = true, + unstableTimestampMillis = AN_EPOCH + ).toContent() + ) + fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent) + + val result = defaultCheckIfExistingActiveLiveTask.execute(params) + + result shouldBeEqualTo true + val expectedGetActiveBeaconParams = GetActiveBeaconInfoForUserTask.Params( + roomId = params.roomId + ) + fakeGetActiveBeaconInfoForUserTask.verifyExecute(expectedGetActiveBeaconParams) + } + + @Test + fun `given parameters and no existing active live event when calling the task then result is false`() = runTest { + val params = CheckIfExistingActiveLiveTask.Params( + roomId = A_ROOM_ID + ) + val inactiveEvents = listOf( + // no event + null, + // null content + Event( + stateKey = A_USER_ID, + content = null + ), + // inactive live + Event( + stateKey = A_USER_ID, + content = MessageBeaconInfoContent( + timeout = A_TIMEOUT, + isLive = false, + unstableTimestampMillis = AN_EPOCH + ).toContent() + ) + ) + + inactiveEvents.forEach { currentStateEvent -> + fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent) + + val result = defaultCheckIfExistingActiveLiveTask.execute(params) + + result shouldBeEqualTo false + } + } +} diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt new file mode 100644 index 0000000000..588bfaa979 --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultGetActiveBeaconInfoForUserTaskTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.session.room.location + +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.Test +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toContent +import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent +import org.matrix.android.sdk.test.fakes.FakeStateEventDataSource + +private const val A_USER_ID = "user-id" +private const val A_ROOM_ID = "room-id" +private const val A_TIMEOUT = 15_000L +private const val AN_EPOCH = 1655210176L + +@ExperimentalCoroutinesApi +class DefaultGetActiveBeaconInfoForUserTaskTest { + + private val fakeStateEventDataSource = FakeStateEventDataSource() + + private val defaultGetActiveBeaconInfoForUserTask = DefaultGetActiveBeaconInfoForUserTask( + userId = A_USER_ID, + stateEventDataSource = fakeStateEventDataSource.instance + ) + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun `given parameters and no error when calling the task then result is computed`() = runTest { + val currentStateEvent = Event( + stateKey = A_USER_ID, + content = MessageBeaconInfoContent( + timeout = A_TIMEOUT, + isLive = true, + unstableTimestampMillis = AN_EPOCH + ).toContent() + ) + fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent) + val params = GetActiveBeaconInfoForUserTask.Params( + roomId = A_ROOM_ID + ) + + val result = defaultGetActiveBeaconInfoForUserTask.execute(params) + + result shouldBeEqualTo currentStateEvent + fakeStateEventDataSource.verifyGetStateEvent( + roomId = params.roomId, + eventType = EventType.STATE_ROOM_BEACON_INFO.first(), + stateKey = A_USER_ID + ) + } +} 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..de91206531 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,18 +16,27 @@ 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 @@ -46,24 +55,30 @@ 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() private val startLiveLocationShareTask = mockk() private val stopLiveLocationShareTask = mockk() + private val checkIfExistingActiveLiveTask = mockk() private val fakeLiveLocationShareAggregatedSummaryMapper = mockk() private val defaultLocationSharingService = DefaultLocationSharingService( - roomId = fakeRoomId, + roomId = A_ROOM_ID, monarchy = fakeMonarchy.instance, sendStaticLocationTask = sendStaticLocationTask, sendLiveLocationTask = sendLiveLocationTask, startLiveLocationShareTask = startLiveLocationShareTask, stopLiveLocationShareTask = stopLiveLocationShareTask, + checkIfExistingActiveLiveTask = checkIfExistingActiveLiveTask, liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper ) + @Before + fun setUp() { + mockkStatic("androidx.lifecycle.Transformations") + } + @After fun tearDown() { unmockkAll() @@ -117,17 +132,65 @@ internal class DefaultLocationSharingServiceTest { } @Test - fun `live location share can be started with a given timeout`() = runTest { + fun `given existing active live can be stopped when starting a live then the current live is stopped and the new live is started`() = runTest { + coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns true + coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success("stopped-event-id") coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID) val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT) result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) - val expectedParams = StartLiveLocationShareTask.Params( + val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params( + roomId = A_ROOM_ID + ) + coVerify { checkIfExistingActiveLiveTask.execute(expectedCheckExistingParams) } + val expectedStopParams = StopLiveLocationShareTask.Params( + roomId = A_ROOM_ID + ) + coVerify { stopLiveLocationShareTask.execute(expectedStopParams) } + val expectedStartParams = StartLiveLocationShareTask.Params( roomId = A_ROOM_ID, timeoutMillis = A_TIMEOUT ) - coVerify { startLiveLocationShareTask.execute(expectedParams) } + coVerify { startLiveLocationShareTask.execute(expectedStartParams) } + } + + @Test + fun `given existing active live cannot be stopped when starting a live then the result is failure`() = runTest { + coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns true + val error = Throwable() + coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Failure(error) + + val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT) + + result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error) + val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params( + roomId = A_ROOM_ID + ) + coVerify { checkIfExistingActiveLiveTask.execute(expectedCheckExistingParams) } + val expectedStopParams = StopLiveLocationShareTask.Params( + roomId = A_ROOM_ID + ) + coVerify { stopLiveLocationShareTask.execute(expectedStopParams) } + } + + @Test + fun `given no existing active live when starting a live then the new live is started`() = runTest { + coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns false + coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID) + + val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT) + + result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) + val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params( + roomId = A_ROOM_ID + ) + coVerify { checkIfExistingActiveLiveTask.execute(expectedCheckExistingParams) } + val expectedStartParams = StartLiveLocationShareTask.Params( + roomId = A_ROOM_ID, + timeoutMillis = A_TIMEOUT + ) + coVerify { startLiveLocationShareTask.execute(expectedStartParams) } } @Test @@ -154,7 +217,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) @@ -168,4 +231,38 @@ internal class DefaultLocationSharingServiceTest { result shouldBeEqualTo listOf(summary) } + + @Test + fun `given an event id when getting livedata on corresponding live summary then it is correctly computed`() { + val entity = LiveLocationShareAggregatedSummaryEntity() + val summary = LiveLocationShareAggregatedSummary( + userId = "", + isActive = true, + endOfLiveTimestampMillis = 123, + lastLocationDataContent = null + ) + + fakeMonarchy.givenWhere() + .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.getLiveLocationShareSummary(AN_EVENT_ID).value + + result shouldBeEqualTo summary.toOptional() + } } diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt index 7cb5abff62..1abf179ccf 100644 --- a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/internal/session/room/location/DefaultStopLiveLocationShareTaskTest.kt @@ -27,11 +27,10 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.toContent import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult -import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent import org.matrix.android.sdk.internal.session.room.state.SendStateTask +import org.matrix.android.sdk.test.fakes.FakeGetActiveBeaconInfoForUserTask import org.matrix.android.sdk.test.fakes.FakeSendStateTask -import org.matrix.android.sdk.test.fakes.FakeStateEventDataSource private const val A_USER_ID = "user-id" private const val A_ROOM_ID = "room-id" @@ -43,12 +42,11 @@ private const val AN_EPOCH = 1655210176L class DefaultStopLiveLocationShareTaskTest { private val fakeSendStateTask = FakeSendStateTask() - private val fakeStateEventDataSource = FakeStateEventDataSource() + private val fakeGetActiveBeaconInfoForUserTask = FakeGetActiveBeaconInfoForUserTask() private val defaultStopLiveLocationShareTask = DefaultStopLiveLocationShareTask( - userId = A_USER_ID, sendStateTask = fakeSendStateTask, - stateEventDataSource = fakeStateEventDataSource.instance + getActiveBeaconInfoForUserTask = fakeGetActiveBeaconInfoForUserTask ) @After @@ -67,7 +65,7 @@ class DefaultStopLiveLocationShareTaskTest { unstableTimestampMillis = AN_EPOCH ).toContent() ) - fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent) + fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent) fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID) val result = defaultStopLiveLocationShareTask.execute(params) @@ -78,20 +76,21 @@ class DefaultStopLiveLocationShareTaskTest { isLive = false, unstableTimestampMillis = AN_EPOCH ).toContent() - val expectedParams = SendStateTask.Params( + val expectedSendParams = SendStateTask.Params( roomId = params.roomId, stateKey = A_USER_ID, eventType = EventType.STATE_ROOM_BEACON_INFO.first(), body = expectedBeaconContent ) fakeSendStateTask.verifyExecuteRetry( - params = expectedParams, + params = expectedSendParams, remainingRetry = 3 ) - fakeStateEventDataSource.verifyGetStateEvent( - roomId = params.roomId, - eventType = EventType.STATE_ROOM_BEACON_INFO.first(), - stateKey = A_USER_ID + val expectedGetBeaconParams = GetActiveBeaconInfoForUserTask.Params( + roomId = params.roomId + ) + fakeGetActiveBeaconInfoForUserTask.verifyExecute( + expectedGetBeaconParams ) } @@ -109,18 +108,15 @@ class DefaultStopLiveLocationShareTaskTest { unstableTimestampMillis = AN_EPOCH ).toContent() ), - // incorrect content + // null content Event( stateKey = A_USER_ID, - content = MessageAudioContent( - msgType = "", - body = "" - ).toContent() + content = null ) ) incorrectCurrentStateEvents.forEach { currentStateEvent -> - fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent) + fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent) fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID) val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID) @@ -141,7 +137,7 @@ class DefaultStopLiveLocationShareTaskTest { unstableTimestampMillis = AN_EPOCH ).toContent() ) - fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent) + fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent) fakeSendStateTask.givenExecuteRetryReturns("") val result = defaultStopLiveLocationShareTask.execute(params) @@ -160,7 +156,7 @@ class DefaultStopLiveLocationShareTaskTest { unstableTimestampMillis = AN_EPOCH ).toContent() ) - fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent) + fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent) val error = Throwable() fakeSendStateTask.givenExecuteRetryThrows(error) diff --git a/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGetActiveBeaconInfoForUserTask.kt b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGetActiveBeaconInfoForUserTask.kt new file mode 100644 index 0000000000..dc4a48908a --- /dev/null +++ b/matrix-sdk-android/src/test/java/org/matrix/android/sdk/test/fakes/FakeGetActiveBeaconInfoForUserTask.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.test.fakes + +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.internal.session.room.location.GetActiveBeaconInfoForUserTask + +internal class FakeGetActiveBeaconInfoForUserTask : GetActiveBeaconInfoForUserTask by mockk() { + + fun givenExecuteReturns(event: Event?) { + coEvery { execute(any()) } returns event + } + + fun verifyExecute(params: GetActiveBeaconInfoForUserTask.Params) { + coVerify { execute(params) } + } +} 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/vector/build.gradle b/vector/build.gradle index f3435178e9..f9ca36f1b5 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -372,7 +372,6 @@ dependencies { implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.10.0" implementation libs.squareup.moshi - implementation libs.squareup.moshiKt kapt libs.squareup.moshiKotlin // Lifecycle @@ -534,10 +533,10 @@ dependencies { } // Flipper, debug builds only - debugImplementation('com.facebook.flipper:flipper:0.150.0') { + debugImplementation(libs.flipper.flipper) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } - debugImplementation('com.facebook.flipper:flipper-network-plugin:0.150.0') { + debugImplementation(libs.flipper.flipperNetworkPlugin) { exclude group: 'com.facebook.fbjni', module: 'fbjni' } debugImplementation 'com.facebook.soloader:soloader:0.10.3' 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/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/call/dialpad/DialPadLookup.kt b/vector/src/main/java/im/vector/app/features/call/dialpad/DialPadLookup.kt index e835a74fd6..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,6 +22,7 @@ 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 timber.log.Timber import javax.inject.Inject class DialPadLookup @Inject constructor( @@ -42,18 +43,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.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 + 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) } } 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..f2a0a7d7e9 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.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 @@ -108,6 +109,15 @@ class VerificationChooseMethodController @Inject constructor( iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) listener { host.listener?.doVerifyBySas() } } + } else if (!state.isReadySent) { + // a bit of a special case, if you tapped on the timeline cell but not on a button + 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() } + } } if (state.isMe && state.canCrossSign) { @@ -131,5 +141,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..3d3766f430 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,14 @@ class VerificationChooseMethodFragment @Inject constructor( sharedViewModel.itWasNotMe() } + override fun acceptRequest() { + sharedViewModel.handle(VerificationAction.ReadyPendingVerification) + } + + override fun declineRequest() { + 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..0f78dd52cb 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 isReadySent: 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(), + isReadySent = pvr?.isReady.orFalse(), ) } } 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/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..853fef8bc8 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 @@ -59,12 +59,6 @@ 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 +75,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 +100,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 +134,7 @@ class MessageItemFactory @Inject constructor( private val vectorPreferences: VectorPreferences, private val urlMapProvider: UrlMapProvider, private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory, + private val pollItemViewStateFactory: PollItemViewStateFactory, ) { // TODO inject this properly? @@ -251,62 +237,21 @@ class MessageItemFactory @Inject constructor( 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) + val pollViewState = pollItemViewStateFactory.create(pollContent, informationData) return PollItem_() .attributes(attributes) .eventId(informationData.eventId) - .pollQuestion(question) - .canVote(pollState.isVotable()) - .totalVotesText(totalVotesText) - .optionViewStates(optionViewStates) + .pollQuestion(createPollQuestion(informationData, pollViewState.question, callback)) + .canVote(pollViewState.canVote) + .totalVotesText(pollViewState.totalVotes) + .optionViewStates(pollViewState.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, @@ -317,20 +262,6 @@ class MessageItemFactory @Inject constructor( 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, 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..8da0f2d279 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/PollItemViewStateFactory.kt @@ -0,0 +1,165 @@ +/* + * 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.StringProvider +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 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, +) { + + fun create( + pollContent: MessagePollContent, + informationData: MessageInformationData, + ): PollViewState { + val pollCreationInfo = pollContent.getBestPollCreationInfo() + + val question = pollCreationInfo?.question?.getBestQuestion().orEmpty() + + 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: String, 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: String, + 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: String, + 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: String, + 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: String, 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() ?: "" + ) + }, + ) + } +} 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/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/main/java/im/vector/app/features/location/LocationSharingService.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingService.kt index ef612eeec2..8073aaaa35 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 @@ -23,17 +23,21 @@ 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 +53,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,37 +61,50 @@ 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 var startInProgress = false override fun onCreate() { super.onCreate() - Timber.i("### LocationSharingService.onCreate") + Timber.i("onCreate") + initLocationTracking() + } + + private fun initLocationTracking() { // Start tracking location locationTracker.addCallback(this) locationTracker.start() + + launchWithActiveSession { session -> + val job = locationTracker.locations + .onEach(this@LocationSharingService::onLocationUpdate) + .launchIn(session.coroutineScope) + jobs.add(job) + } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + startInProgress = true + val roomArgs = intent?.getParcelableExtra(EXTRA_ROOM_ARGS) as? RoomArgs - Timber.i("### LocationSharingService.onStartCommand. sessionId - roomId ${roomArgs?.sessionId} - ${roomArgs?.roomId}") + Timber.i("onStartCommand. sessionId - roomId ${roomArgs?.sessionId} - ${roomArgs?.roomId}") if (roomArgs != null) { // Show a sticky notification 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 -> + launchWithActiveSession { session -> sendStartingLiveBeaconInfo(session, roomArgs) } } + startInProgress = false + return START_STICKY } @@ -100,7 +118,8 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { ?.let { result -> when (result) { is UpdateLiveLocationShareResult.Success -> { - roomArgsMap[result.beaconEventId] = roomArgs + addRoomArgs(result.beaconEventId, roomArgs) + listenForLiveSummaryChanges(roomArgs.roomId, result.beaconEventId) locationTracker.requestLastKnownLocation() } is UpdateLiveLocationShareResult.Failure -> { @@ -110,55 +129,19 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { } } ?: run { - Timber.w("### LocationSharingService.sendStartingLiveBeaconInfo error, no received beacon info id") + Timber.w("sendStartingLiveBeaconInfo error, no received beacon info id") tryToDestroyMe() } } - 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) - } + private fun stopSharingLocation(beaconEventId: String) { + Timber.i("stopSharingLocation for beacon $beaconEventId") + removeRoomArgs(beaconEventId) + tryToDestroyMe() } - fun stopSharingLocation(roomId: String) { - Timber.i("### LocationSharingService.stopSharingLocation for $roomId") - - 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 - } - } - } - - 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}") + private fun onLocationUpdate(locationData: LocationData) { + Timber.i("onLocationUpdate. Uncertainty: ${locationData.uncertainty}") // Emit location update to all rooms in which live location sharing is active roomArgsMap.toMap().forEach { item -> @@ -171,7 +154,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { beaconInfoEventId: String, locationData: LocationData ) { - launchInIO { session -> + launchWithActiveSession { session -> session.getRoom(roomId) ?.locationSharingService() ?.sendLiveLocation( @@ -189,31 +172,46 @@ class LocationSharingService : VectorService(), LocationTracker.Callback { } private fun tryToDestroyMe() { - if (roomArgsMap.isEmpty()) { - Timber.i("### LocationSharingService. Destroying self, time is up for all rooms") - destroyMe() + if (startInProgress.not() && roomArgsMap.isEmpty()) { + Timber.i("Destroying self, time is up for all rooms") + stopSelf() } } - private fun destroyMe() { - locationTracker.removeCallback(this) - timers.forEach { it.cancel() } - timers.clear() - stopSelf() - } - override fun onDestroy() { super.onDestroy() - Timber.i("### LocationSharingService.onDestroy") - destroyMe() + Timber.i("onDestroy") + jobs.forEach { it.cancel() } + jobs.clear() + locationTracker.removeCallback(this) } - private fun launchInIO(block: suspend CoroutineScope.(Session) -> Unit) = + private fun addRoomArgs(beaconEventId: String, roomArgs: RoomArgs) { + Timber.i("adding roomArgs for beaconEventId: $beaconEventId") + roomArgsMap[beaconEventId] = roomArgs + } + + private fun removeRoomArgs(beaconEventId: String) { + Timber.i("removing roomArgs for beaconEventId: $beaconEventId") + roomArgsMap.remove(beaconEventId) + } + + private fun listenForLiveSummaryChanges(roomId: String, beaconEventId: String) { + launchWithActiveSession { session -> + val job = getLiveLocationShareSummaryUseCase.execute(roomId, beaconEventId) + .distinctUntilChangedBy { it.isActive } + .filter { it.isActive == false } + .onEach { stopSharingLocation(beaconEventId) } + .launchIn(session.coroutineScope) + jobs.add(job) + } + } + + private fun launchWithActiveSession(block: suspend CoroutineScope.(Session) -> Unit) = activeSessionHolder .getSafeActiveSession() ?.let { session -> session.coroutineScope.launch( - context = session.coroutineDispatchers.io, block = { block(session) } ) } 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..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 @@ -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,12 +51,8 @@ class LocationSharingServiceConnection @Inject constructor( } } - fun unbind() { - callback = null - } - - fun stopLiveLocationSharing(roomId: String) { - locationSharingService?.stopSharingLocation(roomId) + fun unbind(callback: Callback) { + removeCallback(callback) } override fun onServiceConnected(className: ComponentName, binder: IBinder) { @@ -62,17 +60,33 @@ 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) + } + + private fun addCallback(callback: Callback) { + callbacks.add(callback) + } + + private fun removeCallback(callback: Callback) { + callbacks.remove(callback) + } + + private fun onCallbackActionNoArg(action: Callback.() -> Unit) { + callbacks.toList().forEach(action) + } + + private fun forwardErrorToCallbacks(error: Throwable) { + callbacks.toList().forEach { it.onLocationServiceError(error) } } } diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt index 30476d064f..b9a2dc830c 100644 --- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt @@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getUser import org.matrix.android.sdk.api.util.toMatrixItem +import timber.log.Timber /** * Sampling period to compare target location and user location. @@ -65,13 +66,20 @@ class LocationSharingViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by hiltMavericksViewModelFactory() init { - locationTracker.addCallback(this) - locationTracker.start() + initLocationTracking() setUserItem() updatePin() compareTargetAndUserLocation() } + private fun initLocationTracking() { + locationTracker.addCallback(this) + locationTracker.locations + .onEach(::onLocationUpdate) + .launchIn(viewModelScope) + locationTracker.start() + } + private fun setUserItem() { setState { copy(userItem = session.getUser(session.myUserId)?.toMatrixItem()) } } @@ -172,7 +180,8 @@ class LocationSharingViewModel @AssistedInject constructor( ) } - override fun onLocationUpdate(locationData: LocationData) { + private fun onLocationUpdate(locationData: LocationData) { + Timber.d("onLocationUpdate()") setState { copy(lastKnownUserLocation = locationData) } 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..aa05fe764b 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 @@ -25,28 +25,27 @@ import androidx.annotation.VisibleForTesting import androidx.core.content.getSystemService import androidx.core.location.LocationListenerCompat import im.vector.app.BuildConfig -import im.vector.app.core.utils.Debouncer -import im.vector.app.core.utils.createBackgroundHandler +import im.vector.app.core.di.ActiveSessionHolder +import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton -private const val BKG_HANDLER_NAME = "LocationTracker.BKG_HANDLER_NAME" -private const val LOCATION_DEBOUNCE_ID = "LocationTracker.LOCATION_DEBOUNCE_ID" - @Singleton class LocationTracker @Inject constructor( - context: Context + context: Context, + private val activeSessionHolder: ActiveSessionHolder ) : LocationListenerCompat { private val locationManager = context.getSystemService() interface Callback { - /** - * Called on every location update. - */ - fun onLocationUpdate(locationData: LocationData) - /** * Called when no location provider is available to request location updates. */ @@ -62,9 +61,16 @@ class LocationTracker @Inject constructor( @VisibleForTesting var hasLocationFromGPSProvider = false - private var lastLocation: LocationData? = null + private val _locations = MutableSharedFlow(replay = 1) - private val debouncer = Debouncer(createBackgroundHandler(BKG_HANDLER_NAME)) + /** + * SharedFlow to collect location updates. + */ + val locations = _locations.asSharedFlow() + .onEach { Timber.d("new location emitted") } + .debounce(MIN_TIME_TO_UPDATE_LOCATION_MILLIS) + .onEach { Timber.d("new location emitted after debounce") } + .map { it.toLocationData() } @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) fun start() { @@ -119,33 +125,35 @@ class LocationTracker @Inject constructor( } @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) + @VisibleForTesting fun stop() { Timber.d("stop()") locationManager?.removeUpdates(this) - synchronized(this) { - callbacks.clear() - } - debouncer.cancelAll() + callbacks.clear() hasLocationFromGPSProvider = false hasLocationFromFusedProvider = false } /** - * Request the last known location. It will be given async through Callback. - * Please ensure adding a callback to receive the value. + * Request the last known location. It will be given async through corresponding flow. + * Please ensure collecting the flow before calling this method. */ fun requestLastKnownLocation() { - lastLocation?.let { locationData -> onLocationUpdate(locationData) } + Timber.d("requestLastKnownLocation") + activeSessionHolder.getSafeActiveSession()?.coroutineScope?.launch { + _locations.replayCache.firstOrNull()?.let { + Timber.d("emitting last location from cache") + _locations.emit(it) + } + } } - @Synchronized fun addCallback(callback: Callback) { if (!callbacks.contains(callback)) { callbacks.add(callback) } } - @Synchronized fun removeCallback(callback: Callback) { callbacks.remove(callback) if (callbacks.size == 0) { @@ -183,21 +191,19 @@ class LocationTracker @Inject constructor( } } - debouncer.debounce(LOCATION_DEBOUNCE_ID, MIN_TIME_TO_UPDATE_LOCATION_MILLIS) { - notifyLocation(location) - } + notifyLocation(location) } private fun notifyLocation(location: Location) { - if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { - Timber.d("notify location: $location") - } else { - Timber.d("notify location: ${location.provider}") - } + activeSessionHolder.getSafeActiveSession()?.coroutineScope?.launch { + if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { + Timber.d("notify location: $location") + } else { + Timber.d("notify location: ${location.provider}") + } - val locationData = location.toLocationData() - lastLocation = locationData - onLocationUpdate(locationData) + _locations.emit(location) + } } override fun onProviderDisabled(provider: String) { @@ -215,9 +221,8 @@ class LocationTracker @Inject constructor( } } - @Synchronized private fun onNoLocationProviderAvailable() { - callbacks.forEach { + callbacks.toList().forEach { try { it.onNoLocationProviderAvailable() } catch (error: Exception) { @@ -226,17 +231,6 @@ class LocationTracker @Inject constructor( } } - @Synchronized - private fun onLocationUpdate(locationData: LocationData) { - callbacks.forEach { - try { - it.onLocationUpdate(locationData) - } catch (error: Exception) { - Timber.e(error, "error in onLocationUpdate callback $it") - } - } - } - private fun Location.toLocationData(): LocationData { return LocationData(latitude, longitude, accuracy.toDouble()) } 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 new file mode 100644 index 0000000000..0d8b70ccda --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/GetLiveLocationShareSummaryUseCase.kt @@ -0,0 +1,43 @@ +/* + * 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 kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.withContext +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, +) { + + suspend fun execute(roomId: String, eventId: String): Flow = withContext(session.coroutineDispatchers.main) { + Timber.d("getting flow for roomId=$roomId and eventId=$eventId") + session.getRoom(roomId) + ?.locationSharingService() + ?.getLiveLocationShareSummary(eventId) + ?.asFlow() + ?.mapNotNull { it.getOrNull() } + ?: emptyFlow() + } +} 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..402c7ffb15 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/location/live/StopLiveLocationShareUseCase.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 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 activeSessionHolder: ActiveSessionHolder +) { + + suspend fun execute(roomId: String): UpdateLiveLocationShareResult? { + return sendStoppedBeaconInfo(roomId) + } + + private suspend fun sendStoppedBeaconInfo(roomId: String): UpdateLiveLocationShareResult? { + return activeSessionHolder.getActiveSession() + .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/main/java/im/vector/app/features/poll/PollState.kt b/vector/src/main/java/im/vector/app/features/poll/PollViewState.kt similarity index 71% 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..0f01d58c96 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,11 @@ 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 - fun isVotable() = this !is Sending && this !is Ended -} +data class PollViewState( + val question: String, + val totalVotes: String, + val canVote: Boolean, + val optionViewStates: List?, +) 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..d21a4cba20 --- /dev/null +++ b/vector/src/main/res/layout/item_positive_destrutive_buttons.xml @@ -0,0 +1,24 @@ + + + +