Merge branch 'develop' into feature/fga/load_room_members_by_chunk

This commit is contained in:
ganfra 2022-06-29 11:45:52 +02:00 committed by GitHub
commit 1a33f6e094
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
81 changed files with 1720 additions and 429 deletions
.github/workflows
build.gradle
changelog.d
dependencies.gradle
docs
matrix-sdk-android/src
vector

View file

@ -8,8 +8,9 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: env:
CI_GRADLE_ARG_PROPERTIES: > CI_GRADLE_ARG_PROPERTIES: >
-Porg.gradle.jvmargs=-Xmx2g -Porg.gradle.jvmargs=-Xmx4g
-Porg.gradle.parallel=false -Porg.gradle.parallel=false
--no-daemon
jobs: jobs:
debug: debug:

View file

@ -13,6 +13,7 @@ env:
CI_GRADLE_ARG_PROPERTIES: > CI_GRADLE_ARG_PROPERTIES: >
-Porg.gradle.jvmargs=-Xmx4g -Porg.gradle.jvmargs=-Xmx4g
-Porg.gradle.parallel=false -Porg.gradle.parallel=false
--no-daemon
jobs: jobs:

View file

@ -9,6 +9,8 @@ on:
env: env:
CI_GRADLE_ARG_PROPERTIES: > CI_GRADLE_ARG_PROPERTIES: >
-Porg.gradle.jvmargs=-Xmx4g -Porg.gradle.jvmargs=-Xmx4g
-Porg.gradle.parallel=false
--no-daemon
jobs: jobs:
check: check:

View file

@ -8,8 +8,9 @@ on:
# Enrich gradle.properties for CI/CD # Enrich gradle.properties for CI/CD
env: env:
CI_GRADLE_ARG_PROPERTIES: > CI_GRADLE_ARG_PROPERTIES: >
-Porg.gradle.jvmargs=-Xmx2g -Porg.gradle.jvmargs=-Xmx4g
-Porg.gradle.parallel=false -Porg.gradle.parallel=false
--no-daemon
jobs: jobs:
tests: tests:

View file

@ -29,7 +29,7 @@ buildscript {
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5' classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5'
classpath "com.likethesalad.android:stem-plugin:2.1.1" classpath "com.likethesalad.android:stem-plugin:2.1.1"
classpath 'org.owasp:dependency-check-gradle:7.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" classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
@ -43,7 +43,7 @@ plugins {
id "io.gitlab.arturbosch.detekt" version "1.20.0" id "io.gitlab.arturbosch.detekt" version "1.20.0"
// Dependency Analysis // 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 // https://github.com/jeremylong/DependencyCheck
@ -267,6 +267,8 @@ dependencyAnalysis {
onUnusedDependencies { onUnusedDependencies {
// False positives // False positives
exclude( exclude(
"androidx.fragment:fragment-testing",
"com.facebook.soloader:soloader",
"com.vanniktech:emoji-google", "com.vanniktech:emoji-google",
"com.vanniktech:emoji-material", "com.vanniktech:emoji-material",
"org.maplibre.gl:android-plugin-annotation-v9", "org.maplibre.gl:android-plugin-annotation-v9",

1
changelog.d/6101.bugfix Normal file
View file

@ -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.

1
changelog.d/6328.bugfix Normal file
View file

@ -0,0 +1 @@
Fix | Some user verification requests couldn't be accepted/declined

1
changelog.d/6349.bugfix Normal file
View file

@ -0,0 +1 @@
[Location sharing] Fix stop of a live not possible from another device

1
changelog.d/6364.feature Normal file
View file

@ -0,0 +1 @@
[Location sharing] - Stop any active live before starting a new one

1
changelog.d/6366.misc Normal file
View file

@ -0,0 +1 @@
Poll view state unit tests

1
changelog.d/6375.bugfix Normal file
View file

@ -0,0 +1 @@
[Location Share] - Adding missing prefix "u=" for uncertainty in geo URI

1
changelog.d/6396.doc Normal file
View file

@ -0,0 +1 @@
Update the PR process doc to come back to one reviewer with optional additional reviewers.

View file

@ -21,6 +21,7 @@ def markwon = "4.6.2"
def moshi = "1.13.0" def moshi = "1.13.0"
def lifecycle = "2.4.1" def lifecycle = "2.4.1"
def flowBinding = "1.2.0" def flowBinding = "1.2.0"
def flipper = "0.151.1"
def epoxy = "4.6.2" def epoxy = "4.6.2"
def mavericks = "2.7.0" def mavericks = "2.7.0"
def glide = "4.13.2" def glide = "4.13.2"
@ -91,6 +92,10 @@ ext.libs = [
'hiltAndroidTesting' : "com.google.dagger:hilt-android-testing:$dagger", 'hiltAndroidTesting' : "com.google.dagger:hilt-android-testing:$dagger",
'hiltCompiler' : "com.google.dagger:hilt-compiler:$dagger" 'hiltCompiler' : "com.google.dagger:hilt-compiler:$dagger"
], ],
flipper : [
'flipper' : "com.facebook.flipper:flipper:$flipper",
'flipperNetworkPlugin' : "com.facebook.flipper:flipper-network-plugin:$flipper",
],
squareup : [ squareup : [
'moshi' : "com.squareup.moshi:moshi:$moshi", 'moshi' : "com.squareup.moshi:moshi:$moshi",
'moshiKt' : "com.squareup.moshi:moshi-kotlin:$moshi", 'moshiKt' : "com.squareup.moshi:moshi-kotlin:$moshi",

View file

@ -83,15 +83,16 @@ Exceptions can occur:
##### PR Review Assignment ##### 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. - The PR creator selects the [element-android-reviewers](https://github.com/orgs/vector-im/teams/element-android-reviewers) team as a reviewer.
- 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 assign the reviewer. If the reviewer is not available (holiday, etc.), remove them and set again the team, GitHub will select another 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. - 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). - 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. - 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 ##### PR review time
@ -102,6 +103,7 @@ Some tips to achieve it:
- Set up your GH notifications correctly - Set up your GH notifications correctly
- Check your pulls page: [https://github.com/pulls](https://github.com/pulls) - 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 - 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. 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.

View file

@ -16,9 +16,11 @@
package org.matrix.android.sdk.api.session.room.location package org.matrix.android.sdk.api.session.room.location
import androidx.annotation.MainThread
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary 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.Cancelable
import org.matrix.android.sdk.api.util.Optional
/** /**
* Manage all location sharing related features. * Manage all location sharing related features.
@ -59,5 +61,13 @@ interface LocationSharingService {
/** /**
* Returns a LiveData on the list of current running live location shares. * Returns a LiveData on the list of current running live location shares.
*/ */
@MainThread
fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>> fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>>
/**
* 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<Optional<LiveLocationShareAggregatedSummary>>
} }

View file

@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LocationInfo( 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, @Json(name = "uri") val geoUri: String? = null,

View file

@ -35,7 +35,7 @@ data class MessageLocationContent(
@Json(name = "body") override val body: String, @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, @Json(name = "geo_uri") val geoUri: String,

View file

@ -25,4 +25,7 @@ data class PollCreationInfo(
@Json(name = "kind") val kind: PollType? = PollType.DISCLOSED_UNSTABLE, @Json(name = "kind") val kind: PollType? = PollType.DISCLOSED_UNSTABLE,
@Json(name = "max_selections") val maxSelections: Int = 1, @Json(name = "max_selections") val maxSelections: Int = 1,
@Json(name = "answers") val answers: List<PollAnswer>? = null @Json(name = "answers") val answers: List<PollAnswer>? = null
) ) {
fun isUndisclosed() = kind in listOf(PollType.UNDISCLOSED_UNSTABLE, PollType.UNDISCLOSED)
}

View file

@ -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, // 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. // 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") Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated age:$event.ageLocalTs ms")
} }

View file

@ -271,7 +271,7 @@ private fun HashMap<String, RoomMemberContent?>.addSenderState(realm: Realm, roo
* Create an EventEntity for the root thread event or get an existing one. * 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 { 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) return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
} }

View file

@ -130,7 +130,7 @@ internal fun EventEntity.asDomain(castJsonNumbers: Boolean = false): Event {
internal fun Event.toEntity( internal fun Event.toEntity(
roomId: String, roomId: String,
sendState: SendState, sendState: SendState,
ageLocalTs: Long?, ageLocalTs: Long,
contentToInject: String? = null contentToInject: String? = null
): EventEntity { ): EventEntity {
return EventMapper.map(this, roomId).apply { return EventMapper.map(this, roomId).apply {

View file

@ -52,6 +52,10 @@ internal class MigrateSessionTo030(realm: DynamicRealm) : RealmMigrator(realm, 3
timelineEvents.deleteAllFromRealm() timelineEvents.deleteAllFromRealm()
} }
chunks.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)."
)
} }
} }

View file

@ -76,7 +76,7 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveIn
realm: Realm, realm: Realm,
roomId: String, roomId: String,
userId: String, userId: String,
ignoredEventId: String ignoredEventId: String,
): List<LiveLocationShareAggregatedSummaryEntity> { ): List<LiveLocationShareAggregatedSummaryEntity> {
return LiveLocationShareAggregatedSummaryEntity return LiveLocationShareAggregatedSummaryEntity
.whereRoomId(realm, roomId = roomId) .whereRoomId(realm, roomId = roomId)

View file

@ -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.GetPublicRoomTask
import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask 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.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.DefaultSendLiveLocationTask
import org.matrix.android.sdk.internal.session.room.location.DefaultSendStaticLocationTask 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.DefaultStartLiveLocationShareTask
import org.matrix.android.sdk.internal.session.room.location.DefaultStopLiveLocationShareTask 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.SendLiveLocationTask
import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask
import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask
@ -319,4 +323,10 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindSendLiveLocationTask(task: DefaultSendLiveLocationTask): SendLiveLocationTask abstract fun bindSendLiveLocationTask(task: DefaultSendLiveLocationTask): SendLiveLocationTask
@Binds
abstract fun bindGetActiveBeaconInfoForUserTask(task: DefaultGetActiveBeaconInfoForUserTask): GetActiveBeaconInfoForUserTask
@Binds
abstract fun bindCheckIfExistingActiveLiveTask(task: DefaultCheckIfExistingActiveLiveTask): CheckIfExistingActiveLiveTask
} }

View file

@ -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<CheckIfExistingActiveLiveTask.Params, Boolean> {
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<MessageBeaconInfoContent>()
?.isLive
.orFalse()
}
}

View file

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.session.room.location package org.matrix.android.sdk.internal.session.room.location
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory 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.location.UpdateLiveLocationShareResult
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary 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.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.mapper.LiveLocationShareAggregatedSummaryMapper
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity 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.findRunningLiveInRoom
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
internal class DefaultLocationSharingService @AssistedInject constructor( internal class DefaultLocationSharingService @AssistedInject constructor(
@ -37,6 +41,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
private val sendLiveLocationTask: SendLiveLocationTask, private val sendLiveLocationTask: SendLiveLocationTask,
private val startLiveLocationShareTask: StartLiveLocationShareTask, private val startLiveLocationShareTask: StartLiveLocationShareTask,
private val stopLiveLocationShareTask: StopLiveLocationShareTask, private val stopLiveLocationShareTask: StopLiveLocationShareTask,
private val checkIfExistingActiveLiveTask: CheckIfExistingActiveLiveTask,
private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper, private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper,
) : LocationSharingService { ) : LocationSharingService {
@ -68,6 +73,13 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
} }
override suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult { 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( val params = StartLiveLocationShareTask.Params(
roomId = roomId, roomId = roomId,
timeoutMillis = timeoutMillis timeoutMillis = timeoutMillis
@ -75,6 +87,13 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
return startLiveLocationShareTask.execute(params) return startLiveLocationShareTask.execute(params)
} }
private suspend fun checkIfExistingActiveLive(): Boolean {
val params = CheckIfExistingActiveLiveTask.Params(
roomId = roomId
)
return checkIfExistingActiveLiveTask.execute(params)
}
override suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult { override suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult {
val params = StopLiveLocationShareTask.Params( val params = StopLiveLocationShareTask.Params(
roomId = roomId, roomId = roomId,
@ -88,4 +107,15 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
liveLocationShareAggregatedSummaryMapper liveLocationShareAggregatedSummaryMapper
) )
} }
override fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData<Optional<LiveLocationShareAggregatedSummary>> {
return Transformations.map(
monarchy.findAllMappedWithChanges(
{ LiveLocationShareAggregatedSummaryEntity.where(it, roomId = roomId, eventId = beaconInfoEventId) },
liveLocationShareAggregatedSummaryMapper
)
) {
it.firstOrNull().toOptional()
}
}
} }

View file

@ -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<GetActiveBeaconInfoForUserTask.Params, Event?> {
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<MessageBeaconInfoContent>()?.isLive.orFalse()
}
}
}

View file

@ -16,17 +16,13 @@
package org.matrix.android.sdk.internal.session.room.location 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.Event
import org.matrix.android.sdk.api.session.events.model.EventType 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.toContent
import org.matrix.android.sdk.api.session.events.model.toModel 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.location.UpdateLiveLocationShareResult
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent 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.SendStateTask
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import javax.inject.Inject import javax.inject.Inject
@ -37,13 +33,12 @@ internal interface StopLiveLocationShareTask : Task<StopLiveLocationShareTask.Pa
} }
internal class DefaultStopLiveLocationShareTask @Inject constructor( internal class DefaultStopLiveLocationShareTask @Inject constructor(
@UserId private val userId: String,
private val sendStateTask: SendStateTask, private val sendStateTask: SendStateTask,
private val stateEventDataSource: StateEventDataSource, private val getActiveBeaconInfoForUserTask: GetActiveBeaconInfoForUserTask,
) : StopLiveLocationShareTask { ) : StopLiveLocationShareTask {
override suspend fun execute(params: StopLiveLocationShareTask.Params): UpdateLiveLocationShareResult { override suspend fun execute(params: StopLiveLocationShareTask.Params): UpdateLiveLocationShareResult {
val beaconInfoStateEvent = getLiveLocationBeaconInfoForUser(userId, params.roomId) ?: return getResultForIncorrectBeaconInfoEvent() val beaconInfoStateEvent = getActiveLiveLocationBeaconInfoForUser(params.roomId) ?: return getResultForIncorrectBeaconInfoEvent()
val stateKey = beaconInfoStateEvent.stateKey ?: return getResultForIncorrectBeaconInfoEvent() val stateKey = beaconInfoStateEvent.stateKey ?: return getResultForIncorrectBeaconInfoEvent()
val content = beaconInfoStateEvent.getClearContent()?.toModel<MessageBeaconInfoContent>() ?: return getResultForIncorrectBeaconInfoEvent() val content = beaconInfoStateEvent.getClearContent()?.toModel<MessageBeaconInfoContent>() ?: return getResultForIncorrectBeaconInfoEvent()
val updatedContent = content.copy(isLive = false).toContent() val updatedContent = content.copy(isLive = false).toContent()
@ -68,17 +63,10 @@ internal class DefaultStopLiveLocationShareTask @Inject constructor(
private fun getResultForIncorrectBeaconInfoEvent() = private fun getResultForIncorrectBeaconInfoEvent() =
UpdateLiveLocationShareResult.Failure(Exception("incorrect last beacon info event")) UpdateLiveLocationShareResult.Failure(Exception("incorrect last beacon info event"))
private fun getLiveLocationBeaconInfoForUser(userId: String, roomId: String): Event? { private suspend fun getActiveLiveLocationBeaconInfoForUser(roomId: String): Event? {
return EventType.STATE_ROOM_BEACON_INFO val params = GetActiveBeaconInfoForUserTask.Params(
.mapNotNull { roomId = roomId
stateEventDataSource.getStateEvent( )
roomId = roomId, return getActiveBeaconInfoForUserTask.execute(params)
eventType = it,
stateKey = QueryStringValue.Equals(userId)
)
}
.firstOrNull { beaconInfoEvent ->
beaconInfoEvent.getClearContent()?.toModel<MessageBeaconInfoContent>()?.isLive.orFalse()
}
} }
} }

View file

@ -116,7 +116,7 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
if (roomMemberEvent.eventId == null || roomMemberEvent.stateKey == null || roomMemberEvent.type == null) { if (roomMemberEvent.eventId == null || roomMemberEvent.stateKey == null || roomMemberEvent.type == null) {
continue 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) val eventEntity = roomMemberEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
CurrentStateEventEntity.getOrCreate( CurrentStateEventEntity.getOrCreate(
realm, realm,

View file

@ -209,7 +209,8 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
* Create an EventEntity to be added in the TimelineEventEntity. * Create an EventEntity to be added in the TimelineEventEntity.
*/ */
private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity { 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) return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
} }

View file

@ -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. * Uncertainty of the location is in meters and not required.
*/ */
private fun buildGeoUri(latitude: Double, longitude: Double, uncertainty: Double?): String { private fun buildGeoUri(latitude: Double, longitude: Double, uncertainty: Double?): String {
@ -718,7 +718,7 @@ internal class LocalEchoEventFactory @Inject constructor(
append(",") append(",")
append(longitude) append(longitude)
uncertainty?.let { uncertainty?.let {
append(";") append(";u=")
append(it) append(it)
} }
} }

View file

@ -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 return event
} }

View file

@ -142,7 +142,7 @@ internal class TokenChunkEventPersistor @Inject constructor(
val now = clock.epochMillis() val now = clock.epochMillis()
stateEvents?.forEach { stateEvent -> 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) val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
currentChunk.addStateEvent(roomId, stateEventEntity, direction) currentChunk.addStateEvent(roomId, stateEventEntity, direction)
if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null) { 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) { if (event.eventId == null || event.senderId == null) {
return@forEach 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) val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) { if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) {
val contentToUse = if (direction == PaginationDirection.BACKWARDS) { val contentToUse = if (direction == PaginationDirection.BACKWARDS) {

View file

@ -244,7 +244,7 @@ internal class RoomSyncHandler @Inject constructor(
if (event.eventId == null || event.stateKey == null || event.type == null) { if (event.eventId == null || event.stateKey == null || event.type == null) {
continue 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) val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
Timber.v("## received state event ${event.type} and key ${event.stateKey}") Timber.v("## received state event ${event.type} and key ${event.stateKey}")
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { 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) { if (event.stateKey == null || event.type == null) {
return@forEach 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) val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
eventId = eventEntity.eventId eventId = eventEntity.eventId
@ -336,7 +336,7 @@ internal class RoomSyncHandler @Inject constructor(
if (event.eventId == null || event.stateKey == null || event.type == null) { if (event.eventId == null || event.stateKey == null || event.type == null) {
continue 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) val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
eventId = event.eventId eventId = event.eventId
@ -348,7 +348,7 @@ internal class RoomSyncHandler @Inject constructor(
if (event.eventId == null || event.senderId == null || event.type == null) { if (event.eventId == null || event.senderId == null || event.type == null) {
continue 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) val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
if (event.stateKey != null) { if (event.stateKey != null) {
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
@ -401,7 +401,10 @@ internal class RoomSyncHandler @Inject constructor(
for (rawEvent in eventList) { for (rawEvent in eventList) {
// It's annoying roomId is not there, but lot of code rely on it. // 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.. // 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) { if (event.eventId == null || event.senderId == null || event.type == null) {
continue continue
} }
@ -423,7 +426,6 @@ internal class RoomSyncHandler @Inject constructor(
contentToInject = threadsAwarenessHandler.makeEventThreadAware(realm, roomId, event) 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) val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs, contentToInject).copyToRealmOrIgnore(realm, insertType)
if (event.stateKey != null) { if (event.stateKey != null) {
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {

View file

@ -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.send.LocalEchoEventFactory
import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask 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.awaitTransaction
import org.matrix.android.sdk.internal.util.time.Clock
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -64,7 +65,8 @@ internal class ThreadsAwarenessHandler @Inject constructor(
private val permalinkFactory: PermalinkFactory, private val permalinkFactory: PermalinkFactory,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val lightweightSettingsStorage: LightweightSettingsStorage, 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 // 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<String, String>) { private suspend fun fetchThreadsEvents(threadsToFetch: Map<String, String>) {
val eventEntityList = threadsToFetch.mapNotNull { (eventId, roomId) -> val eventEntityList = threadsToFetch.mapNotNull { (eventId, roomId) ->
fetchEvent(eventId, roomId)?.let { fetchEvent(eventId, roomId)?.let {
it.toEntity(roomId, SendState.SYNCED, it.ageLocalTs) it.toEntity(roomId, SendState.SYNCED, it.ageLocalTs ?: clock.epochMillis())
} }
} }

View file

@ -46,7 +46,7 @@ private const val A_TIMEOUT_MILLIS = 15 * 60 * 1000L
private const val A_LATITUDE = 40.05 private const val A_LATITUDE = 40.05
private const val A_LONGITUDE = 29.24 private const val A_LONGITUDE = 29.24
private const val A_UNCERTAINTY = 30.0 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 { internal class LiveLocationAggregationProcessorTest {

View file

@ -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
}
}
}

View file

@ -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
)
}
}

View file

@ -16,18 +16,27 @@
package org.matrix.android.sdk.internal.session.room.location 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.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.unmockkAll import io.mockk.unmockkAll
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.After import org.junit.After
import org.junit.Before
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult 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.session.room.model.livelocation.LiveLocationShareAggregatedSummary
import org.matrix.android.sdk.api.util.Cancelable 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.mapper.LiveLocationShareAggregatedSummaryMapper
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
@ -46,24 +55,30 @@ private const val A_TIMEOUT = 15_000L
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
internal class DefaultLocationSharingServiceTest { internal class DefaultLocationSharingServiceTest {
private val fakeRoomId = A_ROOM_ID
private val fakeMonarchy = FakeMonarchy() private val fakeMonarchy = FakeMonarchy()
private val sendStaticLocationTask = mockk<SendStaticLocationTask>() private val sendStaticLocationTask = mockk<SendStaticLocationTask>()
private val sendLiveLocationTask = mockk<SendLiveLocationTask>() private val sendLiveLocationTask = mockk<SendLiveLocationTask>()
private val startLiveLocationShareTask = mockk<StartLiveLocationShareTask>() private val startLiveLocationShareTask = mockk<StartLiveLocationShareTask>()
private val stopLiveLocationShareTask = mockk<StopLiveLocationShareTask>() private val stopLiveLocationShareTask = mockk<StopLiveLocationShareTask>()
private val checkIfExistingActiveLiveTask = mockk<CheckIfExistingActiveLiveTask>()
private val fakeLiveLocationShareAggregatedSummaryMapper = mockk<LiveLocationShareAggregatedSummaryMapper>() private val fakeLiveLocationShareAggregatedSummaryMapper = mockk<LiveLocationShareAggregatedSummaryMapper>()
private val defaultLocationSharingService = DefaultLocationSharingService( private val defaultLocationSharingService = DefaultLocationSharingService(
roomId = fakeRoomId, roomId = A_ROOM_ID,
monarchy = fakeMonarchy.instance, monarchy = fakeMonarchy.instance,
sendStaticLocationTask = sendStaticLocationTask, sendStaticLocationTask = sendStaticLocationTask,
sendLiveLocationTask = sendLiveLocationTask, sendLiveLocationTask = sendLiveLocationTask,
startLiveLocationShareTask = startLiveLocationShareTask, startLiveLocationShareTask = startLiveLocationShareTask,
stopLiveLocationShareTask = stopLiveLocationShareTask, stopLiveLocationShareTask = stopLiveLocationShareTask,
checkIfExistingActiveLiveTask = checkIfExistingActiveLiveTask,
liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper
) )
@Before
fun setUp() {
mockkStatic("androidx.lifecycle.Transformations")
}
@After @After
fun tearDown() { fun tearDown() {
unmockkAll() unmockkAll()
@ -117,17 +132,65 @@ internal class DefaultLocationSharingServiceTest {
} }
@Test @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) coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT) val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID) 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, roomId = A_ROOM_ID,
timeoutMillis = A_TIMEOUT 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 @Test
@ -154,7 +217,7 @@ internal class DefaultLocationSharingServiceTest {
) )
fakeMonarchy.givenWhere<LiveLocationShareAggregatedSummaryEntity>() fakeMonarchy.givenWhere<LiveLocationShareAggregatedSummaryEntity>()
.givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, fakeRoomId) .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, A_ROOM_ID)
.givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true) .givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
.givenIsNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID) .givenIsNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID)
.givenIsNotNull(LiveLocationShareAggregatedSummaryEntityFields.LAST_LOCATION_CONTENT) .givenIsNotNull(LiveLocationShareAggregatedSummaryEntityFields.LAST_LOCATION_CONTENT)
@ -168,4 +231,38 @@ internal class DefaultLocationSharingServiceTest {
result shouldBeEqualTo listOf(summary) 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<LiveLocationShareAggregatedSummaryEntity>()
.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<Function<List<LiveLocationShareAggregatedSummary>, Optional<LiveLocationShareAggregatedSummary>>>()
every {
Transformations.map(
liveData,
capture(mapper)
)
} answers {
val value = secondArg<Function<List<LiveLocationShareAggregatedSummary>, Optional<LiveLocationShareAggregatedSummary>>>().apply(listOf(summary))
MutableLiveData(value)
}
val result = defaultLocationSharingService.getLiveLocationShareSummary(AN_EVENT_ID).value
result shouldBeEqualTo summary.toOptional()
}
} }

View file

@ -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.EventType
import org.matrix.android.sdk.api.session.events.model.toContent 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.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.api.session.room.model.message.MessageBeaconInfoContent
import org.matrix.android.sdk.internal.session.room.state.SendStateTask 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.FakeSendStateTask
import org.matrix.android.sdk.test.fakes.FakeStateEventDataSource
private const val A_USER_ID = "user-id" private const val A_USER_ID = "user-id"
private const val A_ROOM_ID = "room-id" private const val A_ROOM_ID = "room-id"
@ -43,12 +42,11 @@ private const val AN_EPOCH = 1655210176L
class DefaultStopLiveLocationShareTaskTest { class DefaultStopLiveLocationShareTaskTest {
private val fakeSendStateTask = FakeSendStateTask() private val fakeSendStateTask = FakeSendStateTask()
private val fakeStateEventDataSource = FakeStateEventDataSource() private val fakeGetActiveBeaconInfoForUserTask = FakeGetActiveBeaconInfoForUserTask()
private val defaultStopLiveLocationShareTask = DefaultStopLiveLocationShareTask( private val defaultStopLiveLocationShareTask = DefaultStopLiveLocationShareTask(
userId = A_USER_ID,
sendStateTask = fakeSendStateTask, sendStateTask = fakeSendStateTask,
stateEventDataSource = fakeStateEventDataSource.instance getActiveBeaconInfoForUserTask = fakeGetActiveBeaconInfoForUserTask
) )
@After @After
@ -67,7 +65,7 @@ class DefaultStopLiveLocationShareTaskTest {
unstableTimestampMillis = AN_EPOCH unstableTimestampMillis = AN_EPOCH
).toContent() ).toContent()
) )
fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent) fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID) fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
val result = defaultStopLiveLocationShareTask.execute(params) val result = defaultStopLiveLocationShareTask.execute(params)
@ -78,20 +76,21 @@ class DefaultStopLiveLocationShareTaskTest {
isLive = false, isLive = false,
unstableTimestampMillis = AN_EPOCH unstableTimestampMillis = AN_EPOCH
).toContent() ).toContent()
val expectedParams = SendStateTask.Params( val expectedSendParams = SendStateTask.Params(
roomId = params.roomId, roomId = params.roomId,
stateKey = A_USER_ID, stateKey = A_USER_ID,
eventType = EventType.STATE_ROOM_BEACON_INFO.first(), eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
body = expectedBeaconContent body = expectedBeaconContent
) )
fakeSendStateTask.verifyExecuteRetry( fakeSendStateTask.verifyExecuteRetry(
params = expectedParams, params = expectedSendParams,
remainingRetry = 3 remainingRetry = 3
) )
fakeStateEventDataSource.verifyGetStateEvent( val expectedGetBeaconParams = GetActiveBeaconInfoForUserTask.Params(
roomId = params.roomId, roomId = params.roomId
eventType = EventType.STATE_ROOM_BEACON_INFO.first(), )
stateKey = A_USER_ID fakeGetActiveBeaconInfoForUserTask.verifyExecute(
expectedGetBeaconParams
) )
} }
@ -109,18 +108,15 @@ class DefaultStopLiveLocationShareTaskTest {
unstableTimestampMillis = AN_EPOCH unstableTimestampMillis = AN_EPOCH
).toContent() ).toContent()
), ),
// incorrect content // null content
Event( Event(
stateKey = A_USER_ID, stateKey = A_USER_ID,
content = MessageAudioContent( content = null
msgType = "",
body = ""
).toContent()
) )
) )
incorrectCurrentStateEvents.forEach { currentStateEvent -> incorrectCurrentStateEvents.forEach { currentStateEvent ->
fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent) fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID) fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID) val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID)
@ -141,7 +137,7 @@ class DefaultStopLiveLocationShareTaskTest {
unstableTimestampMillis = AN_EPOCH unstableTimestampMillis = AN_EPOCH
).toContent() ).toContent()
) )
fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent) fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
fakeSendStateTask.givenExecuteRetryReturns("") fakeSendStateTask.givenExecuteRetryReturns("")
val result = defaultStopLiveLocationShareTask.execute(params) val result = defaultStopLiveLocationShareTask.execute(params)
@ -160,7 +156,7 @@ class DefaultStopLiveLocationShareTaskTest {
unstableTimestampMillis = AN_EPOCH unstableTimestampMillis = AN_EPOCH
).toContent() ).toContent()
) )
fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent) fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
val error = Throwable() val error = Throwable()
fakeSendStateTask.givenExecuteRetryThrows(error) fakeSendStateTask.givenExecuteRetryThrows(error)

View file

@ -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) }
}
}

View file

@ -16,6 +16,7 @@
package org.matrix.android.sdk.test.fakes package org.matrix.android.sdk.test.fakes
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.mockk.MockKVerificationScope import io.mockk.MockKVerificationScope
@ -60,10 +61,11 @@ internal class FakeMonarchy {
realmEntities: List<T>, realmEntities: List<T>,
mappedResult: List<R>, mappedResult: List<R>,
mapper: Monarchy.Mapper<R, T> mapper: Monarchy.Mapper<R, T>
) { ): LiveData<List<R>> {
every { mapper.map(any()) } returns mockk() every { mapper.map(any()) } returns mockk()
val monarchyQuery = slot<Monarchy.Query<T>>() val monarchyQuery = slot<Monarchy.Query<T>>()
val monarchyMapper = slot<Monarchy.Mapper<R, T>>() val monarchyMapper = slot<Monarchy.Mapper<R, T>>()
val result = MutableLiveData(mappedResult)
every { every {
instance.findAllMappedWithChanges(capture(monarchyQuery), capture(monarchyMapper)) instance.findAllMappedWithChanges(capture(monarchyQuery), capture(monarchyMapper))
} answers { } answers {
@ -71,7 +73,8 @@ internal class FakeMonarchy {
realmEntities.forEach { realmEntities.forEach {
monarchyMapper.captured.map(it) monarchyMapper.captured.map(it)
} }
MutableLiveData(mappedResult) result
} }
return result
} }
} }

View file

@ -372,7 +372,6 @@ dependencies {
implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.10.0" implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.10.0"
implementation libs.squareup.moshi implementation libs.squareup.moshi
implementation libs.squareup.moshiKt
kapt libs.squareup.moshiKotlin kapt libs.squareup.moshiKotlin
// Lifecycle // Lifecycle
@ -534,10 +533,10 @@ dependencies {
} }
// Flipper, debug builds only // Flipper, debug builds only
debugImplementation('com.facebook.flipper:flipper:0.150.0') { debugImplementation(libs.flipper.flipper) {
exclude group: 'com.facebook.fbjni', module: 'fbjni' 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' exclude group: 'com.facebook.fbjni', module: 'fbjni'
} }
debugImplementation 'com.facebook.soloader:soloader:0.10.3' debugImplementation 'com.facebook.soloader:soloader:0.10.3'

View file

@ -44,11 +44,11 @@ class ActiveSessionHolder @Inject constructor(
private val guardServiceStarter: GuardServiceStarter private val guardServiceStarter: GuardServiceStarter
) { ) {
private var activeSession: AtomicReference<Session?> = AtomicReference() private var activeSessionReference: AtomicReference<Session?> = AtomicReference()
fun setActiveSession(session: Session) { fun setActiveSession(session: Session) {
Timber.w("setActiveSession of ${session.myUserId}") Timber.w("setActiveSession of ${session.myUserId}")
activeSession.set(session) activeSessionReference.set(session)
activeSessionDataSource.post(Option.just(session)) activeSessionDataSource.post(Option.just(session))
keyRequestHandler.start(session) keyRequestHandler.start(session)
@ -68,7 +68,7 @@ class ActiveSessionHolder @Inject constructor(
it.removeListener(sessionListener) it.removeListener(sessionListener)
} }
activeSession.set(null) activeSessionReference.set(null)
activeSessionDataSource.post(Option.empty()) activeSessionDataSource.post(Option.empty())
keyRequestHandler.stop() keyRequestHandler.stop()
@ -80,15 +80,15 @@ class ActiveSessionHolder @Inject constructor(
} }
fun hasActiveSession(): Boolean { fun hasActiveSession(): Boolean {
return activeSession.get() != null return activeSessionReference.get() != null
} }
fun getSafeActiveSession(): Session? { fun getSafeActiveSession(): Session? {
return activeSession.get() return activeSessionReference.get()
} }
fun getActiveSession(): Session { fun getActiveSession(): Session {
return activeSession.get() return activeSessionReference.get()
?: throw IllegalStateException("You should authenticate before using this") ?: throw IllegalStateException("You should authenticate before using this")
} }

View file

@ -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<ButtonPositiveDestructiveButtonBarItem.Holder>() {
@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<MaterialButton>(R.id.destructive_button)
val positiveButton by bind<MaterialButton>(R.id.positive_button)
}
}

View file

@ -22,6 +22,7 @@ import im.vector.app.features.call.vectorCallService
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.createdirect.DirectRoomHelper import im.vector.app.features.createdirect.DirectRoomHelper
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class DialPadLookup @Inject constructor( class DialPadLookup @Inject constructor(
@ -42,18 +43,23 @@ class DialPadLookup @Inject constructor(
val sipUserId = thirdPartyUser.userId val sipUserId = thirdPartyUser.userId
val nativeLookupResults = session.sipNativeLookup(thirdPartyUser.userId) val nativeLookupResults = session.sipNativeLookup(thirdPartyUser.userId)
// If I have a native user I check for an existing native room with him... // 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 val nativeUserId = nativeLookupResults.first().userId
if (nativeUserId == session.myUserId) { if (nativeUserId == session.myUserId) {
throw Failure.NumberIsYours throw Failure.NumberIsYours
} }
session.roomService().getExistingDirectRoomWithUser(nativeUserId) var nativeRoomId = session.roomService().getExistingDirectRoomWithUser(nativeUserId)
// if there is not, just create a DM with the sip user if (nativeRoomId == null) {
?: directRoomHelper.ensureDMExists(sipUserId) // if there is no existing native room with the existing native user,
} else { // just create a DM with the native user
// do the same if there is no corresponding native user. nativeRoomId = directRoomHelper.ensureDMExists(nativeUserId)
directRoomHelper.ensureDMExists(sipUserId) }
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)
} }
} }

View file

@ -30,6 +30,8 @@ sealed class VerificationAction : VectorViewModelAction {
data class GotItConclusion(val verified: Boolean) : VerificationAction() data class GotItConclusion(val verified: Boolean) : VerificationAction()
object SkipVerification : VerificationAction() object SkipVerification : VerificationAction()
object VerifyFromPassphrase : VerificationAction() object VerifyFromPassphrase : VerificationAction()
object ReadyPendingVerification : VerificationAction()
object CancelPendingVerification : VerificationAction()
data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction() data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction()
object CancelledFromSsss : VerificationAction() object CancelledFromSsss : VerificationAction()
object SecuredStorageHasBeenReset : VerificationAction() object SecuredStorageHasBeenReset : VerificationAction()

View file

@ -360,6 +360,27 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
as? SasVerificationTransaction) as? SasVerificationTransaction)
?.shortCodeDoesNotMatch() ?.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 -> { is VerificationAction.GotItConclusion -> {
if (state.isVerificationRequired && !action.verified) { if (state.isVerificationRequired && !action.verified) {
// we should go back to first screen // we should go back to first screen

View file

@ -21,6 +21,7 @@ import im.vector.app.R
import im.vector.app.core.epoxy.bottomSheetDividerItem import im.vector.app.core.epoxy.bottomSheetDividerItem
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider 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.bottomSheetVerificationActionItem
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationQrCodeItem 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)) iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
listener { host.listener?.doVerifyBySas() } 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) { if (state.isMe && state.canCrossSign) {
@ -131,5 +141,7 @@ class VerificationChooseMethodController @Inject constructor(
fun openCamera() fun openCamera()
fun doVerifyBySas() fun doVerifyBySas()
fun onClickOnWasNotMe() fun onClickOnWasNotMe()
fun acceptRequest()
fun declineRequest()
} }
} }

View file

@ -100,6 +100,14 @@ class VerificationChooseMethodFragment @Inject constructor(
sharedViewModel.itWasNotMe() sharedViewModel.itWasNotMe()
} }
override fun acceptRequest() {
sharedViewModel.handle(VerificationAction.ReadyPendingVerification)
}
override fun declineRequest() {
sharedViewModel.handle(VerificationAction.CancelPendingVerification)
}
private fun doOpenQRCodeScanner() { private fun doOpenQRCodeScanner() {
QrCodeScannerActivity.startForResult(requireActivity(), scanActivityResultLauncher) QrCodeScannerActivity.startForResult(requireActivity(), scanActivityResultLauncher)
} }

View file

@ -44,7 +44,8 @@ data class VerificationChooseMethodViewState(
val qrCodeText: String? = null, val qrCodeText: String? = null,
val sasModeAvailable: Boolean = false, val sasModeAvailable: Boolean = false,
val isMe: Boolean = false, val isMe: Boolean = false,
val canCrossSign: Boolean = false val canCrossSign: Boolean = false,
val isReadySent: Boolean = false
) : MavericksState ) : MavericksState
class VerificationChooseMethodViewModel @AssistedInject constructor( class VerificationChooseMethodViewModel @AssistedInject constructor(
@ -81,7 +82,8 @@ class VerificationChooseMethodViewModel @AssistedInject constructor(
copy( copy(
otherCanShowQrCode = pvr?.otherCanShowQrCode().orFalse(), otherCanShowQrCode = pvr?.otherCanShowQrCode().orFalse(),
otherCanScanQrCode = pvr?.otherCanScanQrCode().orFalse(), otherCanScanQrCode = pvr?.otherCanScanQrCode().orFalse(),
sasModeAvailable = pvr?.isSasSupported().orFalse() sasModeAvailable = pvr?.isSasSupported().orFalse(),
isReadySent = pvr?.isReady.orFalse(),
) )
} }
} }

View file

@ -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.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.home.room.typing.TypingHelper import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.location.LocationSharingServiceConnection 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.notifications.NotificationDrawerManager
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.raw.wellknown.getOutboundSessionKeySharingStrategyOrDefault 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.getRoom
import org.matrix.android.sdk.api.session.room.getStateEvent 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.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.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
@ -133,8 +135,9 @@ class TimelineViewModel @AssistedInject constructor(
private val decryptionFailureTracker: DecryptionFailureTracker, private val decryptionFailureTracker: DecryptionFailureTracker,
private val notificationDrawerManager: NotificationDrawerManager, private val notificationDrawerManager: NotificationDrawerManager,
private val locationSharingServiceConnection: LocationSharingServiceConnection, private val locationSharingServiceConnection: LocationSharingServiceConnection,
private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase,
timelineFactory: TimelineFactory, timelineFactory: TimelineFactory,
appStateHandler: AppStateHandler appStateHandler: AppStateHandler,
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), ) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener, LocationSharingServiceConnection.Callback { Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener, LocationSharingServiceConnection.Callback {
@ -1139,7 +1142,12 @@ class TimelineViewModel @AssistedInject constructor(
} }
private fun handleStopLiveLocationSharing() { 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() { 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 // we should also mark it as read here, for the scenario that the user
// is already in the thread timeline // is already in the thread timeline
markThreadTimelineAsReadLocal() markThreadTimelineAsReadLocal()
locationSharingServiceConnection.unbind() locationSharingServiceConnection.unbind(this)
super.onCleared() super.onCleared()
} }
} }

View file

@ -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.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.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.RedactedMessageItem_ import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem 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.location.toLocationData
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer 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.settings.VectorPreferences
import im.vector.app.features.voice.AudioWaveformView import im.vector.app.features.voice.AudioWaveformView
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import me.gujun.android.span.span import me.gujun.android.span.span
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl 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.Session
import org.matrix.android.sdk.api.session.crypto.attachments.toElementToDecrypt import org.matrix.android.sdk.api.session.crypto.attachments.toElementToDecrypt
import org.matrix.android.sdk.api.session.events.model.RelationType 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.MessageType
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent 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.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.getFileUrl
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
@ -149,6 +134,7 @@ class MessageItemFactory @Inject constructor(
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val urlMapProvider: UrlMapProvider, private val urlMapProvider: UrlMapProvider,
private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory, private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory,
private val pollItemViewStateFactory: PollItemViewStateFactory,
) { ) {
// TODO inject this properly? // TODO inject this properly?
@ -251,62 +237,21 @@ class MessageItemFactory @Inject constructor(
callback: TimelineEventController.Callback?, callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes, attributes: AbsMessageItem.Attributes,
): PollItem { ): PollItem {
val pollResponseSummary = informationData.pollResponseAggregatedSummary val pollViewState = pollItemViewStateFactory.create(pollContent, informationData)
val pollState = createPollState(informationData, pollResponseSummary, pollContent)
val pollCreationInfo = pollContent.getBestPollCreationInfo()
val questionText = pollCreationInfo?.question?.getBestQuestion().orEmpty()
val question = createPollQuestion(informationData, questionText, callback)
val optionViewStates = pollCreationInfo?.answers?.mapToOptions(pollState, informationData)
val totalVotesText = createTotalVotesText(pollState, pollResponseSummary)
return PollItem_() return PollItem_()
.attributes(attributes) .attributes(attributes)
.eventId(informationData.eventId) .eventId(informationData.eventId)
.pollQuestion(question) .pollQuestion(createPollQuestion(informationData, pollViewState.question, callback))
.canVote(pollState.isVotable()) .canVote(pollViewState.canVote)
.totalVotesText(totalVotesText) .totalVotesText(pollViewState.totalVotes)
.optionViewStates(optionViewStates) .optionViewStates(pollViewState.optionViewStates)
.edited(informationData.hasBeenEdited) .edited(informationData.hasBeenEdited)
.highlighted(highlight) .highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.callback(callback) .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<PollAnswer>.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( private fun createPollQuestion(
informationData: MessageInformationData, informationData: MessageInformationData,
question: String, question: String,
@ -317,20 +262,6 @@ class MessageItemFactory @Inject constructor(
question question
}.toEpoxyCharSequence() }.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( private fun buildAudioMessageItem(
params: TimelineItemFactoryParams, params: TimelineItemFactoryParams,
messageContent: MessageAudioContent, messageContent: MessageAudioContent,

View file

@ -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() ?: ""
)
},
)
}
}

View file

@ -91,7 +91,10 @@ data class PollResponseData(
val totalVotes: Int = 0, val totalVotes: Int = 0,
val winnerVoteCount: Int = 0, val winnerVoteCount: Int = 0,
val isClosed: Boolean = false val isClosed: Boolean = false
) : Parcelable ) : Parcelable {
fun getVoteSummaryOfAnOption(optionId: String) = votes?.get(optionId)
}
@Parcelize @Parcelize
data class PollVoteSummaryData( data class PollVoteSummaryData(

View file

@ -30,7 +30,7 @@ data class LocationData(
/** /**
* Creates location data from a MessageLocationContent. * 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 * @return location data or null if geo uri is not valid
*/ */
fun MessageLocationContent.toLocationData(): LocationData? { fun MessageLocationContent.toLocationData(): LocationData? {
@ -39,7 +39,7 @@ fun MessageLocationContent.toLocationData(): LocationData? {
/** /**
* Creates location data from a geoUri String. * 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 * @return location data or null if geo uri is null or not valid
*/ */
fun String?.toLocationData(): LocationData? { fun String?.toLocationData(): LocationData? {

View file

@ -23,17 +23,21 @@ import android.os.Parcelable
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.services.VectorService 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.notifications.NotificationUtils
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import kotlinx.coroutines.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.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoom import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
import timber.log.Timber import timber.log.Timber
import java.util.Timer
import java.util.TimerTask
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -49,6 +53,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
@Inject lateinit var notificationUtils: NotificationUtils @Inject lateinit var notificationUtils: NotificationUtils
@Inject lateinit var locationTracker: LocationTracker @Inject lateinit var locationTracker: LocationTracker
@Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var getLiveLocationShareSummaryUseCase: GetLiveLocationShareSummaryUseCase
private val binder = LocalBinder() 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. * Keep track of a map between beacon event Id starting the live and RoomArgs.
*/ */
private val roomArgsMap = mutableMapOf<String, RoomArgs>() private val roomArgsMap = mutableMapOf<String, RoomArgs>()
private val timers = mutableListOf<Timer>()
var callback: Callback? = null var callback: Callback? = null
private val jobs = mutableListOf<Job>()
private var startInProgress = false
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
Timber.i("### LocationSharingService.onCreate") Timber.i("onCreate")
initLocationTracking()
}
private fun initLocationTracking() {
// Start tracking location // Start tracking location
locationTracker.addCallback(this) locationTracker.addCallback(this)
locationTracker.start() 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 { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startInProgress = true
val roomArgs = intent?.getParcelableExtra(EXTRA_ROOM_ARGS) as? RoomArgs 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) { if (roomArgs != null) {
// Show a sticky notification // Show a sticky notification
val notification = notificationUtils.buildLiveLocationSharingNotification() val notification = notificationUtils.buildLiveLocationSharingNotification()
startForeground(roomArgs.roomId.hashCode(), notification) startForeground(roomArgs.roomId.hashCode(), notification)
// Schedule a timer to stop sharing
scheduleTimer(roomArgs.roomId, roomArgs.durationMillis)
// Send beacon info state event // Send beacon info state event
launchInIO { session -> launchWithActiveSession { session ->
sendStartingLiveBeaconInfo(session, roomArgs) sendStartingLiveBeaconInfo(session, roomArgs)
} }
} }
startInProgress = false
return START_STICKY return START_STICKY
} }
@ -100,7 +118,8 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
?.let { result -> ?.let { result ->
when (result) { when (result) {
is UpdateLiveLocationShareResult.Success -> { is UpdateLiveLocationShareResult.Success -> {
roomArgsMap[result.beaconEventId] = roomArgs addRoomArgs(result.beaconEventId, roomArgs)
listenForLiveSummaryChanges(roomArgs.roomId, result.beaconEventId)
locationTracker.requestLastKnownLocation() locationTracker.requestLastKnownLocation()
} }
is UpdateLiveLocationShareResult.Failure -> { is UpdateLiveLocationShareResult.Failure -> {
@ -110,55 +129,19 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
} }
} }
?: run { ?: run {
Timber.w("### LocationSharingService.sendStartingLiveBeaconInfo error, no received beacon info id") Timber.w("sendStartingLiveBeaconInfo error, no received beacon info id")
tryToDestroyMe() tryToDestroyMe()
} }
} }
private fun scheduleTimer(roomId: String, durationMillis: Long) { private fun stopSharingLocation(beaconEventId: String) {
Timer() Timber.i("stopSharingLocation for beacon $beaconEventId")
.apply { removeRoomArgs(beaconEventId)
schedule(object : TimerTask() { tryToDestroyMe()
override fun run() {
stopSharingLocation(roomId)
timers.remove(this@apply)
}
}, durationMillis)
}
.also {
timers.add(it)
}
} }
fun stopSharingLocation(roomId: String) { private fun onLocationUpdate(locationData: LocationData) {
Timber.i("### LocationSharingService.stopSharingLocation for $roomId") Timber.i("onLocationUpdate. Uncertainty: ${locationData.uncertainty}")
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}")
// Emit location update to all rooms in which live location sharing is active // Emit location update to all rooms in which live location sharing is active
roomArgsMap.toMap().forEach { item -> roomArgsMap.toMap().forEach { item ->
@ -171,7 +154,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
beaconInfoEventId: String, beaconInfoEventId: String,
locationData: LocationData locationData: LocationData
) { ) {
launchInIO { session -> launchWithActiveSession { session ->
session.getRoom(roomId) session.getRoom(roomId)
?.locationSharingService() ?.locationSharingService()
?.sendLiveLocation( ?.sendLiveLocation(
@ -189,31 +172,46 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
} }
private fun tryToDestroyMe() { private fun tryToDestroyMe() {
if (roomArgsMap.isEmpty()) { if (startInProgress.not() && roomArgsMap.isEmpty()) {
Timber.i("### LocationSharingService. Destroying self, time is up for all rooms") Timber.i("Destroying self, time is up for all rooms")
destroyMe() stopSelf()
} }
} }
private fun destroyMe() {
locationTracker.removeCallback(this)
timers.forEach { it.cancel() }
timers.clear()
stopSelf()
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
Timber.i("### LocationSharingService.onDestroy") Timber.i("onDestroy")
destroyMe() 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 activeSessionHolder
.getSafeActiveSession() .getSafeActiveSession()
?.let { session -> ?.let { session ->
session.coroutineScope.launch( session.coroutineScope.launch(
context = session.coroutineDispatchers.io,
block = { block(session) } block = { block(session) }
) )
} }

View file

@ -22,7 +22,9 @@ import android.content.Intent
import android.content.ServiceConnection import android.content.ServiceConnection
import android.os.IBinder import android.os.IBinder
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LocationSharingServiceConnection @Inject constructor( class LocationSharingServiceConnection @Inject constructor(
private val context: Context private val context: Context
) : ServiceConnection, LocationSharingService.Callback { ) : ServiceConnection, LocationSharingService.Callback {
@ -33,12 +35,12 @@ class LocationSharingServiceConnection @Inject constructor(
fun onLocationServiceError(error: Throwable) fun onLocationServiceError(error: Throwable)
} }
private var callback: Callback? = null private val callbacks = mutableSetOf<Callback>()
private var isBound = false private var isBound = false
private var locationSharingService: LocationSharingService? = null private var locationSharingService: LocationSharingService? = null
fun bind(callback: Callback) { fun bind(callback: Callback) {
this.callback = callback addCallback(callback)
if (isBound) { if (isBound) {
callback.onLocationServiceRunning() callback.onLocationServiceRunning()
@ -49,12 +51,8 @@ class LocationSharingServiceConnection @Inject constructor(
} }
} }
fun unbind() { fun unbind(callback: Callback) {
callback = null removeCallback(callback)
}
fun stopLiveLocationSharing(roomId: String) {
locationSharingService?.stopSharingLocation(roomId)
} }
override fun onServiceConnected(className: ComponentName, binder: IBinder) { override fun onServiceConnected(className: ComponentName, binder: IBinder) {
@ -62,17 +60,33 @@ class LocationSharingServiceConnection @Inject constructor(
it.callback = this it.callback = this
} }
isBound = true isBound = true
callback?.onLocationServiceRunning() onCallbackActionNoArg(Callback::onLocationServiceRunning)
} }
override fun onServiceDisconnected(className: ComponentName) { override fun onServiceDisconnected(className: ComponentName) {
isBound = false isBound = false
locationSharingService?.callback = null locationSharingService?.callback = null
locationSharingService = null locationSharingService = null
callback?.onLocationServiceStopped() onCallbackActionNoArg(Callback::onLocationServiceStopped)
} }
override fun onServiceError(error: Throwable) { 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) }
} }
} }

View file

@ -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.getRoom
import org.matrix.android.sdk.api.session.getUser import org.matrix.android.sdk.api.session.getUser
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import timber.log.Timber
/** /**
* Sampling period to compare target location and user location. * Sampling period to compare target location and user location.
@ -65,13 +66,20 @@ class LocationSharingViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<LocationSharingViewModel, LocationSharingViewState> by hiltMavericksViewModelFactory() companion object : MavericksViewModelFactory<LocationSharingViewModel, LocationSharingViewState> by hiltMavericksViewModelFactory()
init { init {
locationTracker.addCallback(this) initLocationTracking()
locationTracker.start()
setUserItem() setUserItem()
updatePin() updatePin()
compareTargetAndUserLocation() compareTargetAndUserLocation()
} }
private fun initLocationTracking() {
locationTracker.addCallback(this)
locationTracker.locations
.onEach(::onLocationUpdate)
.launchIn(viewModelScope)
locationTracker.start()
}
private fun setUserItem() { private fun setUserItem() {
setState { copy(userItem = session.getUser(session.myUserId)?.toMatrixItem()) } 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 { setState {
copy(lastKnownUserLocation = locationData) copy(lastKnownUserLocation = locationData)
} }

View file

@ -25,28 +25,27 @@ import androidx.annotation.VisibleForTesting
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.core.location.LocationListenerCompat import androidx.core.location.LocationListenerCompat
import im.vector.app.BuildConfig import im.vector.app.BuildConfig
import im.vector.app.core.utils.Debouncer import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.utils.createBackgroundHandler 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 timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton 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 @Singleton
class LocationTracker @Inject constructor( class LocationTracker @Inject constructor(
context: Context context: Context,
private val activeSessionHolder: ActiveSessionHolder
) : LocationListenerCompat { ) : LocationListenerCompat {
private val locationManager = context.getSystemService<LocationManager>() private val locationManager = context.getSystemService<LocationManager>()
interface Callback { interface Callback {
/**
* Called on every location update.
*/
fun onLocationUpdate(locationData: LocationData)
/** /**
* Called when no location provider is available to request location updates. * Called when no location provider is available to request location updates.
*/ */
@ -62,9 +61,16 @@ class LocationTracker @Inject constructor(
@VisibleForTesting @VisibleForTesting
var hasLocationFromGPSProvider = false var hasLocationFromGPSProvider = false
private var lastLocation: LocationData? = null private val _locations = MutableSharedFlow<Location>(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]) @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
fun start() { fun start() {
@ -119,33 +125,35 @@ class LocationTracker @Inject constructor(
} }
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION]) @RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
@VisibleForTesting
fun stop() { fun stop() {
Timber.d("stop()") Timber.d("stop()")
locationManager?.removeUpdates(this) locationManager?.removeUpdates(this)
synchronized(this) { callbacks.clear()
callbacks.clear()
}
debouncer.cancelAll()
hasLocationFromGPSProvider = false hasLocationFromGPSProvider = false
hasLocationFromFusedProvider = false hasLocationFromFusedProvider = false
} }
/** /**
* Request the last known location. It will be given async through Callback. * Request the last known location. It will be given async through corresponding flow.
* Please ensure adding a callback to receive the value. * Please ensure collecting the flow before calling this method.
*/ */
fun requestLastKnownLocation() { 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) { fun addCallback(callback: Callback) {
if (!callbacks.contains(callback)) { if (!callbacks.contains(callback)) {
callbacks.add(callback) callbacks.add(callback)
} }
} }
@Synchronized
fun removeCallback(callback: Callback) { fun removeCallback(callback: Callback) {
callbacks.remove(callback) callbacks.remove(callback)
if (callbacks.size == 0) { 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) { private fun notifyLocation(location: Location) {
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) { activeSessionHolder.getSafeActiveSession()?.coroutineScope?.launch {
Timber.d("notify location: $location") if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
} else { Timber.d("notify location: $location")
Timber.d("notify location: ${location.provider}") } else {
} Timber.d("notify location: ${location.provider}")
}
val locationData = location.toLocationData() _locations.emit(location)
lastLocation = locationData }
onLocationUpdate(locationData)
} }
override fun onProviderDisabled(provider: String) { override fun onProviderDisabled(provider: String) {
@ -215,9 +221,8 @@ class LocationTracker @Inject constructor(
} }
} }
@Synchronized
private fun onNoLocationProviderAvailable() { private fun onNoLocationProviderAvailable() {
callbacks.forEach { callbacks.toList().forEach {
try { try {
it.onNoLocationProviderAvailable() it.onNoLocationProviderAvailable()
} catch (error: Exception) { } 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 { private fun Location.toLocationData(): LocationData {
return LocationData(latitude, longitude, accuracy.toDouble()) return LocationData(latitude, longitude, accuracy.toDouble())
} }

View file

@ -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<LiveLocationShareAggregatedSummary> = 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()
}
}

View file

@ -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()
}
}

View file

@ -24,13 +24,17 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.location.LocationSharingServiceConnection import im.vector.app.features.location.LocationSharingServiceConnection
import im.vector.app.features.location.live.StopLiveLocationShareUseCase
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
class LocationLiveMapViewModel @AssistedInject constructor( class LocationLiveMapViewModel @AssistedInject constructor(
@Assisted private val initialState: LocationLiveMapViewState, @Assisted private val initialState: LocationLiveMapViewState,
getListOfUserLiveLocationUseCase: GetListOfUserLiveLocationUseCase, getListOfUserLiveLocationUseCase: GetListOfUserLiveLocationUseCase,
private val locationSharingServiceConnection: LocationSharingServiceConnection, private val locationSharingServiceConnection: LocationSharingServiceConnection,
private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase,
) : VectorViewModel<LocationLiveMapViewState, LocationLiveMapAction, LocationLiveMapViewEvents>(initialState), LocationSharingServiceConnection.Callback { ) : VectorViewModel<LocationLiveMapViewState, LocationLiveMapAction, LocationLiveMapViewEvents>(initialState), LocationSharingServiceConnection.Callback {
@AssistedFactory @AssistedFactory
@ -47,6 +51,11 @@ class LocationLiveMapViewModel @AssistedInject constructor(
locationSharingServiceConnection.bind(this) locationSharingServiceConnection.bind(this)
} }
override fun onCleared() {
locationSharingServiceConnection.unbind(this)
super.onCleared()
}
override fun handle(action: LocationLiveMapAction) { override fun handle(action: LocationLiveMapAction) {
when (action) { when (action) {
is LocationLiveMapAction.AddMapSymbol -> handleAddMapSymbol(action) is LocationLiveMapAction.AddMapSymbol -> handleAddMapSymbol(action)
@ -70,7 +79,12 @@ class LocationLiveMapViewModel @AssistedInject constructor(
} }
private fun handleStopSharing() { 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() { override fun onLocationServiceRunning() {

View file

@ -16,12 +16,11 @@
package im.vector.app.features.poll package im.vector.app.features.poll
sealed interface PollState { import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
object Sending : PollState
object Ready : PollState
data class Voted(val votes: Int) : PollState
object Undisclosed : PollState
object Ended : PollState
fun isVotable() = this !is Sending && this !is Ended data class PollViewState(
} val question: String,
val totalVotes: String,
val canVote: Boolean,
val optionViewStates: List<PollOptionViewState>?,
)

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="horizontal">
<Button
android:id="@+id/destructive_button"
style="@style/Widget.Vector.Button.Destructive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
tools:text="@string/action_decline" />
<Button
android:id="@+id/positive_button"
style="@style/Widget.Vector.Button.Positive"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="@string/action_accept" />
</LinearLayout>

View file

@ -0,0 +1,223 @@
/*
* 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.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
import im.vector.app.features.home.room.detail.timeline.item.PollVoteSummaryData
import im.vector.app.features.home.room.detail.timeline.item.ReactionsSummaryData
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.poll.PollViewState
import im.vector.app.test.fakes.FakeStringProvider
import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test
import org.matrix.android.sdk.api.session.room.model.message.MessagePollContent
import org.matrix.android.sdk.api.session.room.model.message.PollAnswer
import org.matrix.android.sdk.api.session.room.model.message.PollCreationInfo
import org.matrix.android.sdk.api.session.room.model.message.PollQuestion
import org.matrix.android.sdk.api.session.room.model.message.PollType
import org.matrix.android.sdk.api.session.room.send.SendState
private val A_MESSAGE_INFORMATION_DATA = MessageInformationData(
eventId = "eventId",
senderId = "senderId",
ageLocalTS = 0,
avatarUrl = "",
sendState = SendState.SENT,
messageLayout = TimelineMessageLayout.Default(showAvatar = true, showDisplayName = true, showTimestamp = true),
reactionsSummary = ReactionsSummaryData(),
sentByMe = true,
)
private val A_POLL_RESPONSE_DATA = PollResponseData(
myVote = null,
votes = emptyMap(),
)
private val A_POLL_OPTION_IDS = listOf("5ef5f7b0-c9a1-49cf-a0b3-374729a43e76", "ec1a4db0-46d8-4d7a-9bb6-d80724715938", "3677ca8e-061b-40ab-bffe-b22e4e88fcad")
private val A_POLL_CONTENT = MessagePollContent(
unstablePollCreationInfo = PollCreationInfo(
question = PollQuestion(
unstableQuestion = "What is your favourite coffee?"
),
kind = PollType.UNDISCLOSED_UNSTABLE,
maxSelections = 1,
answers = listOf(
PollAnswer(
id = A_POLL_OPTION_IDS[0],
unstableAnswer = "Double Espresso"
),
PollAnswer(
id = A_POLL_OPTION_IDS[1],
unstableAnswer = "Macchiato"
),
PollAnswer(
id = A_POLL_OPTION_IDS[2],
unstableAnswer = "Iced Coffee"
),
)
)
)
class PollItemViewStateFactoryTest {
@Test
fun `given a sending poll state then poll is not votable and option states are PollSending`() {
val stringProvider = FakeStringProvider()
val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance)
val sendingPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(sendState = SendState.SENDING)
val pollViewState = pollItemViewStateFactory.create(
pollContent = A_POLL_CONTENT,
informationData = sendingPollInformationData,
)
pollViewState shouldBeEqualTo PollViewState(
question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "",
totalVotes = stringProvider.instance.getString(R.string.poll_no_votes_cast),
canVote = false,
optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.map { answer ->
PollOptionViewState.PollSending(
optionId = answer.id ?: "",
optionAnswer = answer.getBestAnswer() ?: ""
)
},
)
}
@Test
fun `given a sent poll state when poll is closed then poll is not votable and option states are Ended`() {
val stringProvider = FakeStringProvider()
val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance)
val closedPollSummary = A_POLL_RESPONSE_DATA.copy(isClosed = true)
val closedPollInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = closedPollSummary)
val pollViewState = pollItemViewStateFactory.create(
pollContent = A_POLL_CONTENT,
informationData = closedPollInformationData,
)
pollViewState shouldBeEqualTo PollViewState(
question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "",
totalVotes = stringProvider.instance.getQuantityString(R.plurals.poll_total_vote_count_after_ended, 0, 0),
canVote = false,
optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.map { answer ->
PollOptionViewState.PollEnded(
optionId = answer.id ?: "",
optionAnswer = answer.getBestAnswer() ?: "",
voteCount = 0,
votePercentage = 0.0,
isWinner = false
)
},
)
}
@Test
fun `given a sent poll when undisclosed poll type is selected then poll is votable and option states are PollUndisclosed`() {
val stringProvider = FakeStringProvider()
val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance)
val pollViewState = pollItemViewStateFactory.create(
pollContent = A_POLL_CONTENT,
informationData = A_MESSAGE_INFORMATION_DATA,
)
pollViewState shouldBeEqualTo PollViewState(
question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "",
totalVotes = "",
canVote = true,
optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.map { answer ->
PollOptionViewState.PollUndisclosed(
optionId = answer.id ?: "",
optionAnswer = answer.getBestAnswer() ?: "",
isSelected = false
)
},
)
}
@Test
fun `given a sent poll when my vote exists then poll is still votable and options states are PollVoted`() {
val stringProvider = FakeStringProvider()
val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance)
val votedPollData = A_POLL_RESPONSE_DATA.copy(
totalVotes = 1,
myVote = A_POLL_OPTION_IDS[0],
votes = mapOf(A_POLL_OPTION_IDS[0] to PollVoteSummaryData(total = 1, percentage = 1.0))
)
val disclosedPollContent = A_POLL_CONTENT.copy(
unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy(
kind = PollType.DISCLOSED_UNSTABLE
),
)
val votedInformationData = A_MESSAGE_INFORMATION_DATA.copy(pollResponseAggregatedSummary = votedPollData)
val pollViewState = pollItemViewStateFactory.create(
pollContent = disclosedPollContent,
informationData = votedInformationData,
)
pollViewState shouldBeEqualTo PollViewState(
question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "",
totalVotes = stringProvider.instance.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, 1, 1),
canVote = true,
optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.mapIndexed { index, answer ->
PollOptionViewState.PollVoted(
optionId = answer.id ?: "",
optionAnswer = answer.getBestAnswer() ?: "",
voteCount = if (index == 0) 1 else 0,
votePercentage = if (index == 0) 1.0 else 0.0,
isSelected = index == 0
)
},
)
}
@Test
fun `given a sent poll when poll type is disclosed then poll is votable and option view states are PollReady`() {
val stringProvider = FakeStringProvider()
val pollItemViewStateFactory = PollItemViewStateFactory(stringProvider.instance)
val disclosedPollContent = A_POLL_CONTENT.copy(
unstablePollCreationInfo = A_POLL_CONTENT.getBestPollCreationInfo()?.copy(
kind = PollType.DISCLOSED_UNSTABLE
)
)
val pollViewState = pollItemViewStateFactory.create(
pollContent = disclosedPollContent,
informationData = A_MESSAGE_INFORMATION_DATA,
)
pollViewState shouldBeEqualTo PollViewState(
question = A_POLL_CONTENT.getBestPollCreationInfo()?.question?.getBestQuestion() ?: "",
totalVotes = stringProvider.instance.getString(R.string.poll_no_votes_cast),
canVote = true,
optionViewStates = A_POLL_CONTENT.getBestPollCreationInfo()?.answers?.map { answer ->
PollOptionViewState.PollReady(
optionId = answer.id ?: "",
optionAnswer = answer.getBestAnswer() ?: ""
)
},
)
}
}

View file

@ -28,19 +28,26 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageLocationCont
class LocationDataTest { class LocationDataTest {
@Test @Test
fun validCases() { fun validCases() {
parseGeo("geo:12.34,56.78;13.56") shouldBeEqualTo parseGeo("geo:12.34,56.78;u=13.56") shouldBeEqualTo
LocationData(latitude = 12.34, longitude = 56.78, uncertainty = 13.56) LocationData(latitude = 12.34, longitude = 56.78, uncertainty = 13.56)
parseGeo("geo:12.34,56.78") shouldBeEqualTo parseGeo("geo:12.34,56.78") shouldBeEqualTo
LocationData(latitude = 12.34, longitude = 56.78, uncertainty = null) LocationData(latitude = 12.34, longitude = 56.78, uncertainty = null)
}
@Test
fun lenientCases() {
// Error is ignored in case of invalid uncertainty // Error is ignored in case of invalid uncertainty
parseGeo("geo:12.34,56.78;13.5z6") shouldBeEqualTo parseGeo("geo:12.34,56.78;u=13.5z6") shouldBeEqualTo
LocationData(latitude = 12.34, longitude = 56.78, uncertainty = null) LocationData(latitude = 12.34, longitude = 56.78, uncertainty = null)
parseGeo("geo:12.34,56.78;13. 56") shouldBeEqualTo parseGeo("geo:12.34,56.78;u=13. 56") shouldBeEqualTo
LocationData(latitude = 12.34, longitude = 56.78, uncertainty = null) LocationData(latitude = 12.34, longitude = 56.78, uncertainty = null)
// Space are ignored (trim) // Space are ignored (trim)
parseGeo("geo: 12.34,56.78;13.56") shouldBeEqualTo parseGeo("geo: 12.34,56.78;u=13.56") shouldBeEqualTo
LocationData(latitude = 12.34, longitude = 56.78, uncertainty = 13.56) LocationData(latitude = 12.34, longitude = 56.78, uncertainty = 13.56)
parseGeo("geo:12.34,56.78; 13.56") shouldBeEqualTo parseGeo("geo:12.34,56.78; u=13.56") shouldBeEqualTo
LocationData(latitude = 12.34, longitude = 56.78, uncertainty = 13.56)
// missing "u=" for uncertainty is ignored
parseGeo("geo:12.34,56.78;13.56") shouldBeEqualTo
LocationData(latitude = 12.34, longitude = 56.78, uncertainty = 13.56) LocationData(latitude = 12.34, longitude = 56.78, uncertainty = 13.56)
} }
@ -50,17 +57,17 @@ class LocationDataTest {
parseGeo("geo").shouldBeNull() parseGeo("geo").shouldBeNull()
parseGeo("geo:").shouldBeNull() parseGeo("geo:").shouldBeNull()
parseGeo("geo:12.34").shouldBeNull() parseGeo("geo:12.34").shouldBeNull()
parseGeo("geo:12.34;13.56").shouldBeNull() parseGeo("geo:12.34;u=13.56").shouldBeNull()
parseGeo("gea:12.34,56.78;13.56").shouldBeNull() parseGeo("gea:12.34,56.78;u=13.56").shouldBeNull()
parseGeo("geo:12.x34,56.78;13.56").shouldBeNull() parseGeo("geo:12.x34,56.78;u=13.56").shouldBeNull()
parseGeo("geo:12.34,56.7y8;13.56").shouldBeNull() parseGeo("geo:12.34,56.7y8;u=13.56").shouldBeNull()
// Spaces are not ignored if inside the numbers // Spaces are not ignored if inside the numbers
parseGeo("geo:12.3 4,56.78;13.56").shouldBeNull() parseGeo("geo:12.3 4,56.78;u=13.56").shouldBeNull()
parseGeo("geo:12.34,56.7 8;13.56").shouldBeNull() parseGeo("geo:12.34,56.7 8;u=13.56").shouldBeNull()
// Or in the protocol part // Or in the protocol part
parseGeo(" geo:12.34,56.78;13.56").shouldBeNull() parseGeo(" geo:12.34,56.78;u=13.56").shouldBeNull()
parseGeo("ge o:12.34,56.78;13.56").shouldBeNull() parseGeo("ge o:12.34,56.78;u=13.56").shouldBeNull()
parseGeo("geo :12.34,56.78;13.56").shouldBeNull() parseGeo("geo :12.34,56.78;u=13.56").shouldBeNull()
} }
@Test @Test
@ -77,7 +84,7 @@ class LocationDataTest {
@Test @Test
fun unstablePrefixTest() { fun unstablePrefixTest() {
val geoUri = "geo :12.34,56.78;13.56" val geoUri = "aGeoUri"
val contentWithUnstablePrefixes = MessageLocationContent(body = "", geoUri = "", unstableLocationInfo = LocationInfo(geoUri = geoUri)) val contentWithUnstablePrefixes = MessageLocationContent(body = "", geoUri = "", unstableLocationInfo = LocationInfo(geoUri = geoUri))
contentWithUnstablePrefixes.getBestLocationInfo()?.geoUri.shouldBeEqualTo(geoUri) contentWithUnstablePrefixes.getBestLocationInfo()?.geoUri.shouldBeEqualTo(geoUri)

View file

@ -19,21 +19,21 @@ package im.vector.app.features.location
import android.content.Context import android.content.Context
import android.location.Location import android.location.Location
import android.location.LocationManager import android.location.LocationManager
import im.vector.app.core.utils.Debouncer import im.vector.app.features.session.coroutineScope
import im.vector.app.core.utils.createBackgroundHandler import im.vector.app.test.fakes.FakeActiveSessionHolder
import im.vector.app.test.fakes.FakeContext import im.vector.app.test.fakes.FakeContext
import im.vector.app.test.fakes.FakeHandler
import im.vector.app.test.fakes.FakeLocationManager import im.vector.app.test.fakes.FakeLocationManager
import im.vector.app.test.test
import io.mockk.every import io.mockk.every
import io.mockk.just import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkConstructor
import io.mockk.mockkStatic import io.mockk.mockkStatic
import io.mockk.runs import io.mockk.runs
import io.mockk.slot
import io.mockk.unmockkAll import io.mockk.unmockkAll
import io.mockk.verify import io.mockk.verify
import io.mockk.verifyOrder import io.mockk.verifyOrder
import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
@ -45,26 +45,18 @@ private const val AN_ACCURACY = 5.0f
class LocationTrackerTest { class LocationTrackerTest {
private val fakeHandler = FakeHandler()
private val fakeLocationManager = FakeLocationManager() private val fakeLocationManager = FakeLocationManager()
private val fakeContext = FakeContext().also { private val fakeContext = FakeContext().also {
it.givenService(Context.LOCATION_SERVICE, android.location.LocationManager::class.java, fakeLocationManager.instance) it.givenService(Context.LOCATION_SERVICE, android.location.LocationManager::class.java, fakeLocationManager.instance)
} }
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private lateinit var locationTracker: LocationTracker private lateinit var locationTracker: LocationTracker
@Before @Before
fun setUp() { fun setUp() {
mockkConstructor(Debouncer::class) mockkStatic("im.vector.app.features.session.SessionCoroutineScopesKt")
every { anyConstructed<Debouncer>().cancelAll() } just runs locationTracker = LocationTracker(fakeContext.instance, fakeActiveSessionHolder.instance)
val runnable = slot<Runnable>()
every { anyConstructed<Debouncer>().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, capture(runnable)) } answers {
runnable.captured.run()
true
}
mockkStatic("im.vector.app.core.utils.HandlerKt")
every { createBackgroundHandler(any()) } returns fakeHandler.instance
locationTracker = LocationTracker(fakeContext.instance)
fakeLocationManager.givenRemoveUpdates(locationTracker) fakeLocationManager.givenRemoveUpdates(locationTracker)
} }
@ -139,13 +131,11 @@ class LocationTrackerTest {
} }
@Test @Test
fun `when location updates are received from fused provider then fused locations are taken in priority`() { fun `when location updates are received from fused provider then fused locations are taken in priority`() = runTest {
every { fakeActiveSessionHolder.fakeSession.coroutineScope } returns this
val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER) val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER)
mockAvailableProviders(providers) mockAvailableProviders(providers)
val callback = mockCallback()
locationTracker.addCallback(callback)
locationTracker.start() locationTracker.start()
val fusedLocation = mockLocation( val fusedLocation = mockLocation(
provider = LocationManager.FUSED_PROVIDER, provider = LocationManager.FUSED_PROVIDER,
latitude = 1.0, latitude = 1.0,
@ -159,29 +149,31 @@ class LocationTrackerTest {
val networkLocation = mockLocation( val networkLocation = mockLocation(
provider = LocationManager.NETWORK_PROVIDER provider = LocationManager.NETWORK_PROVIDER
) )
val resultUpdates = locationTracker.locations.test(this)
locationTracker.onLocationChanged(fusedLocation) locationTracker.onLocationChanged(fusedLocation)
locationTracker.onLocationChanged(gpsLocation) locationTracker.onLocationChanged(gpsLocation)
locationTracker.onLocationChanged(networkLocation) locationTracker.onLocationChanged(networkLocation)
advanceTimeBy(MIN_TIME_TO_UPDATE_LOCATION_MILLIS + 1)
val expectedLocationData = LocationData( val expectedLocationData = LocationData(
latitude = 1.0, latitude = 1.0,
longitude = 3.0, longitude = 3.0,
uncertainty = 4.0 uncertainty = 4.0
) )
verify { callback.onLocationUpdate(expectedLocationData) } resultUpdates
verify { anyConstructed<Debouncer>().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, any()) } .assertValues(listOf(expectedLocationData))
.finish()
locationTracker.hasLocationFromFusedProvider shouldBeEqualTo true locationTracker.hasLocationFromFusedProvider shouldBeEqualTo true
locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false
} }
@Test @Test
fun `when location updates are received from gps provider then gps locations are taken if none are received from fused provider`() { fun `when location updates are received from gps provider then gps locations are taken if none are received from fused provider`() = runTest {
every { fakeActiveSessionHolder.fakeSession.coroutineScope } returns this
val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER) val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER)
mockAvailableProviders(providers) mockAvailableProviders(providers)
val callback = mockCallback()
locationTracker.addCallback(callback)
locationTracker.start() locationTracker.start()
val gpsLocation = mockLocation( val gpsLocation = mockLocation(
provider = LocationManager.GPS_PROVIDER, provider = LocationManager.GPS_PROVIDER,
latitude = 1.0, latitude = 1.0,
@ -192,66 +184,75 @@ class LocationTrackerTest {
val networkLocation = mockLocation( val networkLocation = mockLocation(
provider = LocationManager.NETWORK_PROVIDER provider = LocationManager.NETWORK_PROVIDER
) )
val resultUpdates = locationTracker.locations.test(this)
locationTracker.onLocationChanged(gpsLocation) locationTracker.onLocationChanged(gpsLocation)
locationTracker.onLocationChanged(networkLocation) locationTracker.onLocationChanged(networkLocation)
advanceTimeBy(MIN_TIME_TO_UPDATE_LOCATION_MILLIS + 1)
val expectedLocationData = LocationData( val expectedLocationData = LocationData(
latitude = 1.0, latitude = 1.0,
longitude = 3.0, longitude = 3.0,
uncertainty = 4.0 uncertainty = 4.0
) )
verify { callback.onLocationUpdate(expectedLocationData) } resultUpdates
verify { anyConstructed<Debouncer>().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, any()) } .assertValues(listOf(expectedLocationData))
.finish()
locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false
locationTracker.hasLocationFromGPSProvider shouldBeEqualTo true locationTracker.hasLocationFromGPSProvider shouldBeEqualTo true
} }
@Test @Test
fun `when location updates are received from network provider then network locations are taken if none are received from fused or gps provider`() { fun `when location updates are received from network provider then network locations are taken if none are received from fused, gps provider`() = runTest {
every { fakeActiveSessionHolder.fakeSession.coroutineScope } returns this
val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER) val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.FUSED_PROVIDER, LocationManager.NETWORK_PROVIDER)
mockAvailableProviders(providers) mockAvailableProviders(providers)
val callback = mockCallback()
locationTracker.addCallback(callback)
locationTracker.start() locationTracker.start()
val networkLocation = mockLocation( val networkLocation = mockLocation(
provider = LocationManager.NETWORK_PROVIDER, provider = LocationManager.NETWORK_PROVIDER,
latitude = 1.0, latitude = 1.0,
longitude = 3.0, longitude = 3.0,
accuracy = 4f accuracy = 4f
) )
val resultUpdates = locationTracker.locations.test(this)
locationTracker.onLocationChanged(networkLocation) locationTracker.onLocationChanged(networkLocation)
advanceTimeBy(MIN_TIME_TO_UPDATE_LOCATION_MILLIS + 1)
val expectedLocationData = LocationData( val expectedLocationData = LocationData(
latitude = 1.0, latitude = 1.0,
longitude = 3.0, longitude = 3.0,
uncertainty = 4.0 uncertainty = 4.0
) )
verify { callback.onLocationUpdate(expectedLocationData) } resultUpdates
verify { anyConstructed<Debouncer>().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, any()) } .assertValues(listOf(expectedLocationData))
.finish()
locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false
locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false
} }
@Test @Test
fun `when requesting the last location then last location is notified via callback`() { fun `when requesting the last location then last location is notified via location updates flow`() = runTest {
every { fakeActiveSessionHolder.fakeSession.coroutineScope } returns this
val providers = listOf(LocationManager.GPS_PROVIDER) val providers = listOf(LocationManager.GPS_PROVIDER)
fakeLocationManager.givenActiveProviders(providers) fakeLocationManager.givenActiveProviders(providers)
val lastLocation = mockLocation(provider = LocationManager.GPS_PROVIDER) val lastLocation = mockLocation(provider = LocationManager.GPS_PROVIDER)
fakeLocationManager.givenLastLocationForProvider(provider = LocationManager.GPS_PROVIDER, location = lastLocation) fakeLocationManager.givenLastLocationForProvider(provider = LocationManager.GPS_PROVIDER, location = lastLocation)
fakeLocationManager.givenRequestUpdatesForProvider(provider = LocationManager.GPS_PROVIDER, listener = locationTracker) fakeLocationManager.givenRequestUpdatesForProvider(provider = LocationManager.GPS_PROVIDER, listener = locationTracker)
val callback = mockCallback()
locationTracker.addCallback(callback)
locationTracker.start() locationTracker.start()
val resultUpdates = locationTracker.locations.test(this)
locationTracker.requestLastKnownLocation() locationTracker.requestLastKnownLocation()
advanceTimeBy(MIN_TIME_TO_UPDATE_LOCATION_MILLIS + 1)
val expectedLocationData = LocationData( val expectedLocationData = LocationData(
latitude = A_LATITUDE, latitude = A_LATITUDE,
longitude = A_LONGITUDE, longitude = A_LONGITUDE,
uncertainty = AN_ACCURACY.toDouble() uncertainty = AN_ACCURACY.toDouble()
) )
verify { callback.onLocationUpdate(expectedLocationData) } resultUpdates
.assertValues(listOf(expectedLocationData))
.finish()
} }
@Test @Test
@ -259,7 +260,6 @@ class LocationTrackerTest {
locationTracker.stop() locationTracker.stop()
verify { fakeLocationManager.instance.removeUpdates(locationTracker) } verify { fakeLocationManager.instance.removeUpdates(locationTracker) }
verify { anyConstructed<Debouncer>().cancelAll() }
locationTracker.callbacks.isEmpty() shouldBeEqualTo true locationTracker.callbacks.isEmpty() shouldBeEqualTo true
locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false
locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false
@ -276,7 +276,6 @@ class LocationTrackerTest {
private fun mockCallback(): LocationTracker.Callback { private fun mockCallback(): LocationTracker.Callback {
return mockk<LocationTracker.Callback>().also { return mockk<LocationTracker.Callback>().also {
every { it.onNoLocationProviderAvailable() } just runs every { it.onNoLocationProviderAvailable() } just runs
every { it.onLocationUpdate(any()) } just runs
} }
} }

View file

@ -16,21 +16,16 @@
package im.vector.app.features.location.domain.usecase package im.vector.app.features.location.domain.usecase
import com.airbnb.mvrx.test.MvRxTestRule
import im.vector.app.features.location.LocationData import im.vector.app.features.location.LocationData
import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeSession
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.OverrideMockKs import io.mockk.impl.annotations.OverrideMockKs
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
class CompareLocationsUseCaseTest { class CompareLocationsUseCaseTest {
@get:Rule
val mvRxTestRule = MvRxTestRule()
private val session = FakeSession() private val session = FakeSession()
@OverrideMockKs @OverrideMockKs

View file

@ -0,0 +1,73 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.location.live
import im.vector.app.test.fakes.FakeFlowLiveDataConversions
import im.vector.app.test.fakes.FakeSession
import im.vector.app.test.fakes.givenAsFlowReturns
import io.mockk.unmockkAll
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
import org.matrix.android.sdk.api.util.Optional
private const val A_ROOM_ID = "room_id"
private const val AN_EVENT_ID = "event_id"
class GetLiveLocationShareSummaryUseCaseTest {
private val fakeSession = FakeSession()
private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions()
private val getLiveLocationShareSummaryUseCase = GetLiveLocationShareSummaryUseCase(
session = fakeSession
)
@Before
fun setUp() {
fakeFlowLiveDataConversions.setup()
}
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given a room id and event id when calling use case then live data on summary is returned`() = runTest {
val summary = LiveLocationShareAggregatedSummary(
userId = "userId",
isActive = true,
endOfLiveTimestampMillis = 123,
lastLocationDataContent = MessageBeaconLocationDataContent()
)
fakeSession.roomService()
.getRoom(A_ROOM_ID)
.locationSharingService()
.givenLiveLocationShareSummaryReturns(AN_EVENT_ID, summary)
.givenAsFlowReturns(Optional(summary))
val result = getLiveLocationShareSummaryUseCase.execute(A_ROOM_ID, AN_EVENT_ID).first()
result shouldBeEqualTo summary
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.features.location.live
import im.vector.app.test.fakes.FakeActiveSessionHolder
import io.mockk.unmockkAll
import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo
import org.junit.After
import org.junit.Test
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
private const val A_ROOM_ID = "room_id"
private const val AN_EVENT_ID = "event_id"
class StopLiveLocationShareUseCaseTest {
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
private val stopLiveLocationShareUseCase = StopLiveLocationShareUseCase(
activeSessionHolder = fakeActiveSessionHolder.instance
)
@After
fun tearDown() {
unmockkAll()
}
@Test
fun `given a room id when calling use case then the current live is stopped with success`() = runTest {
val updateLiveResult = UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
fakeActiveSessionHolder
.fakeSession
.roomService()
.getRoom(A_ROOM_ID)
.locationSharingService()
.givenStopLiveLocationShareReturns(updateLiveResult)
val result = stopLiveLocationShareUseCase.execute(A_ROOM_ID)
result shouldBeEqualTo updateLiveResult
}
@Test
fun `given a room id and error during the process when calling use case then result is failure`() = runTest {
val error = Throwable()
val updateLiveResult = UpdateLiveLocationShareResult.Failure(error)
fakeActiveSessionHolder
.fakeSession
.roomService()
.getRoom(A_ROOM_ID)
.locationSharingService()
.givenStopLiveLocationShareReturns(updateLiveResult)
val result = stopLiveLocationShareUseCase.execute(A_ROOM_ID)
result shouldBeEqualTo updateLiveResult
}
}

View file

@ -16,52 +16,48 @@
package im.vector.app.features.location.live.map package im.vector.app.features.location.live.map
import androidx.lifecycle.asFlow
import com.airbnb.mvrx.test.MvRxTestRule
import im.vector.app.features.location.LocationData import im.vector.app.features.location.LocationData
import im.vector.app.test.fakes.FakeFlowLiveDataConversions
import im.vector.app.test.fakes.FakeSession import im.vector.app.test.fakes.FakeSession
import im.vector.app.test.fakes.givenAsFlowReturns
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import io.mockk.mockkStatic import io.mockk.unmockkAll
import io.mockk.unmockkStatic
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.shouldBeEqualTo
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
private const val A_ROOM_ID = "room_id"
class GetListOfUserLiveLocationUseCaseTest { class GetListOfUserLiveLocationUseCaseTest {
@get:Rule
val mvRxTestRule = MvRxTestRule()
private val fakeSession = FakeSession() private val fakeSession = FakeSession()
private val viewStateMapper = mockk<UserLiveLocationViewStateMapper>() private val viewStateMapper = mockk<UserLiveLocationViewStateMapper>()
private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions()
private val getListOfUserLiveLocationUseCase = GetListOfUserLiveLocationUseCase(fakeSession, viewStateMapper) private val getListOfUserLiveLocationUseCase = GetListOfUserLiveLocationUseCase(
session = fakeSession,
userLiveLocationViewStateMapper = viewStateMapper
)
@Before @Before
fun setUp() { fun setUp() {
mockkStatic("androidx.lifecycle.FlowLiveDataConversions") fakeFlowLiveDataConversions.setup()
} }
@After @After
fun tearDown() { fun tearDown() {
unmockkStatic("androidx.lifecycle.FlowLiveDataConversions") unmockkAll()
} }
@Test @Test
fun `given a room id then the correct flow of view states list is collected`() = runTest { fun `given a room id then the correct flow of view states list is collected`() = runTest {
val roomId = "roomId"
val summary1 = LiveLocationShareAggregatedSummary( val summary1 = LiveLocationShareAggregatedSummary(
userId = "userId1", userId = "userId1",
isActive = true, isActive = true,
@ -81,12 +77,11 @@ class GetListOfUserLiveLocationUseCaseTest {
lastLocationDataContent = MessageBeaconLocationDataContent() lastLocationDataContent = MessageBeaconLocationDataContent()
) )
val summaries = listOf(summary1, summary2, summary3) val summaries = listOf(summary1, summary2, summary3)
val liveData = fakeSession.roomService() fakeSession.roomService()
.getRoom(roomId) .getRoom(A_ROOM_ID)
.locationSharingService() .locationSharingService()
.givenRunningLiveLocationShareSummaries(summaries) .givenRunningLiveLocationShareSummariesReturns(summaries)
.givenAsFlowReturns(summaries)
every { liveData.asFlow() } returns flowOf(summaries)
val viewState1 = UserLiveLocationViewState( val viewState1 = UserLiveLocationViewState(
matrixItem = MatrixItem.UserItem(id = "@userId1:matrix.org", displayName = "User 1", avatarUrl = ""), matrixItem = MatrixItem.UserItem(id = "@userId1:matrix.org", displayName = "User 1", avatarUrl = ""),
@ -108,8 +103,8 @@ class GetListOfUserLiveLocationUseCaseTest {
coEvery { viewStateMapper.map(summary2) } returns viewState2 coEvery { viewStateMapper.map(summary2) } returns viewState2
coEvery { viewStateMapper.map(summary3) } returns null coEvery { viewStateMapper.map(summary3) } returns null
val viewStates = getListOfUserLiveLocationUseCase.execute(roomId).first() val viewStates = getListOfUserLiveLocationUseCase.execute(A_ROOM_ID).first()
assertEquals(listOf(viewState1, viewState2), viewStates) viewStates shouldBeEqualTo listOf(viewState1, viewState2)
} }
} }

View file

@ -18,39 +18,47 @@ package im.vector.app.features.location.live.map
import com.airbnb.mvrx.test.MvRxTestRule import com.airbnb.mvrx.test.MvRxTestRule
import im.vector.app.features.location.LocationData import im.vector.app.features.location.LocationData
import im.vector.app.features.location.LocationSharingServiceConnection import im.vector.app.features.location.live.StopLiveLocationShareUseCase
import im.vector.app.test.fakes.FakeLocationSharingServiceConnection
import im.vector.app.test.test import im.vector.app.test.test
import io.mockk.every import io.mockk.every
import io.mockk.just
import io.mockk.mockk import io.mockk.mockk
import io.mockk.runs import io.mockk.unmockkAll
import io.mockk.verify
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
private const val A_ROOM_ID = "room_id"
class LocationLiveMapViewModelTest { class LocationLiveMapViewModelTest {
@get:Rule @get:Rule
val mvrxTestRule = MvRxTestRule() val mvRxTestRule = MvRxTestRule(testDispatcher = UnconfinedTestDispatcher())
private val fakeRoomId = "" private val args = LocationLiveMapViewArgs(roomId = A_ROOM_ID)
private val args = LocationLiveMapViewArgs(roomId = fakeRoomId)
private val getListOfUserLiveLocationUseCase = mockk<GetListOfUserLiveLocationUseCase>() private val getListOfUserLiveLocationUseCase = mockk<GetListOfUserLiveLocationUseCase>()
private val locationServiceConnection = mockk<LocationSharingServiceConnection>() private val locationServiceConnection = FakeLocationSharingServiceConnection()
private val stopLiveLocationShareUseCase = mockk<StopLiveLocationShareUseCase>()
private fun createViewModel(): LocationLiveMapViewModel { private fun createViewModel(): LocationLiveMapViewModel {
return LocationLiveMapViewModel( return LocationLiveMapViewModel(
LocationLiveMapViewState(args), LocationLiveMapViewState(args),
getListOfUserLiveLocationUseCase, getListOfUserLiveLocationUseCase,
locationServiceConnection locationServiceConnection.instance,
stopLiveLocationShareUseCase
) )
} }
@After
fun tearDown() {
unmockkAll()
}
@Test @Test
fun `given the viewModel has been initialized then viewState contains user locations list`() = runTest { fun `given the viewModel has been initialized then viewState contains user locations list`() = runTest {
val userLocations = listOf( val userLocations = listOf(
@ -63,8 +71,8 @@ class LocationLiveMapViewModelTest {
showStopSharingButton = false showStopSharingButton = false
) )
) )
every { locationServiceConnection.bind(any()) } just runs locationServiceConnection.givenBind()
every { getListOfUserLiveLocationUseCase.execute(fakeRoomId) } returns flowOf(userLocations) every { getListOfUserLiveLocationUseCase.execute(A_ROOM_ID) } returns flowOf(userLocations)
val viewModel = createViewModel() val viewModel = createViewModel()
viewModel viewModel
@ -76,6 +84,6 @@ class LocationLiveMapViewModelTest {
) )
.finish() .finish()
verify { locationServiceConnection.bind(viewModel) } locationServiceConnection.verifyBind(viewModel)
} }
} }

View file

@ -46,7 +46,7 @@ private const val A_LOCATION_TIMESTAMP = 122L
private const val A_LATITUDE = 40.05 private const val A_LATITUDE = 40.05
private const val A_LONGITUDE = 29.24 private const val A_LONGITUDE = 29.24
private const val A_UNCERTAINTY = 30.0 private const val A_UNCERTAINTY = 30.0
private const val A_GEO_URI = "geo:$A_LATITUDE,$A_LONGITUDE;$A_UNCERTAINTY" private const val A_GEO_URI = "geo:$A_LATITUDE,$A_LONGITUDE;u=$A_UNCERTAINTY"
class UserLiveLocationViewStateMapperTest { class UserLiveLocationViewStateMapperTest {

View file

@ -19,7 +19,6 @@ package im.vector.app.features.media.domain.usecase
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import com.airbnb.mvrx.test.MvRxTestRule
import im.vector.app.core.intent.getMimeTypeFromUri import im.vector.app.core.intent.getMimeTypeFromUri
import im.vector.app.core.utils.saveMedia import im.vector.app.core.utils.saveMedia
import im.vector.app.features.notifications.NotificationUtils import im.vector.app.features.notifications.NotificationUtils
@ -42,14 +41,10 @@ import io.mockk.verifyAll
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule
import org.junit.Test import org.junit.Test
class DownloadMediaUseCaseTest { class DownloadMediaUseCaseTest {
@get:Rule
val mvRxTestRule = MvRxTestRule()
@MockK @MockK
lateinit var appContext: Context lateinit var appContext: Context

View file

@ -16,13 +16,15 @@
package im.vector.app.test package im.vector.app.test
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
private val testDispatcher = UnconfinedTestDispatcher()
internal val testCoroutineDispatchers = MatrixCoroutineDispatchers( internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(
io = Dispatchers.Main, io = testDispatcher,
computation = Dispatchers.Main, computation = testDispatcher,
main = Dispatchers.Main, main = testDispatcher,
crypto = Dispatchers.Main, crypto = testDispatcher,
dmVerif = Dispatchers.Main dmVerif = testDispatcher
) )

View file

@ -23,10 +23,11 @@ import io.mockk.mockk
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
class FakeActiveSessionHolder( class FakeActiveSessionHolder(
private val fakeSession: FakeSession = FakeSession() val fakeSession: FakeSession = FakeSession()
) { ) {
val instance = mockk<ActiveSessionHolder> { val instance = mockk<ActiveSessionHolder> {
every { getActiveSession() } returns fakeSession every { getActiveSession() } returns fakeSession
every { getSafeActiveSession() } returns fakeSession
} }
fun expectSetsActiveSession(session: Session) { fun expectSetsActiveSession(session: Session) {

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fakes
import androidx.lifecycle.LiveData
import androidx.lifecycle.asFlow
import io.mockk.every
import io.mockk.mockkStatic
import kotlinx.coroutines.flow.flowOf
class FakeFlowLiveDataConversions {
fun setup() {
mockkStatic("androidx.lifecycle.FlowLiveDataConversions")
}
}
fun <T> LiveData<T>.givenAsFlowReturns(value: T) {
every { asFlow() } returns flowOf(value)
}

View file

@ -18,17 +18,34 @@ package im.vector.app.test.fakes
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.mockk.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import org.matrix.android.sdk.api.session.room.location.LocationSharingService 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.session.room.model.livelocation.LiveLocationShareAggregatedSummary
import org.matrix.android.sdk.api.util.Optional
class FakeLocationSharingService : LocationSharingService by mockk() { class FakeLocationSharingService : LocationSharingService by mockk() {
fun givenRunningLiveLocationShareSummaries(summaries: List<LiveLocationShareAggregatedSummary>): fun givenRunningLiveLocationShareSummariesReturns(
LiveData<List<LiveLocationShareAggregatedSummary>> { summaries: List<LiveLocationShareAggregatedSummary>
): LiveData<List<LiveLocationShareAggregatedSummary>> {
return MutableLiveData(summaries).also { return MutableLiveData(summaries).also {
every { getRunningLiveLocationShareSummaries() } returns it every { getRunningLiveLocationShareSummaries() } returns it
} }
} }
fun givenLiveLocationShareSummaryReturns(
eventId: String,
summary: LiveLocationShareAggregatedSummary
): LiveData<Optional<LiveLocationShareAggregatedSummary>> {
return MutableLiveData(Optional(summary)).also {
every { getLiveLocationShareSummary(eventId) } returns it
}
}
fun givenStopLiveLocationShareReturns(result: UpdateLiveLocationShareResult) {
coEvery { stopLiveLocationShare() } returns result
}
} }

View file

@ -0,0 +1,37 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.test.fakes
import im.vector.app.features.location.LocationSharingServiceConnection
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
class FakeLocationSharingServiceConnection {
val instance = mockk<LocationSharingServiceConnection>()
fun givenBind() {
every { instance.bind(any()) } just runs
}
fun verifyBind(callback: LocationSharingServiceConnection.Callback) {
verify { instance.bind(callback) }
}
}

View file

@ -27,6 +27,10 @@ class FakeStringProvider {
every { instance.getString(any()) } answers { every { instance.getString(any()) } answers {
"test-${args[0]}" "test-${args[0]}"
} }
every { instance.getQuantityString(any(), any(), any()) } answers {
"test-${args[0]}-${args[1]}"
}
} }
fun given(id: Int, result: String) { fun given(id: Int, result: String) {