Merge branch 'develop' into feature/fga/load_room_members_by_chunk
This commit is contained in:
commit
1a33f6e094
|
@ -8,8 +8,9 @@ on:
|
|||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
CI_GRADLE_ARG_PROPERTIES: >
|
||||
-Porg.gradle.jvmargs=-Xmx2g
|
||||
-Porg.gradle.jvmargs=-Xmx4g
|
||||
-Porg.gradle.parallel=false
|
||||
--no-daemon
|
||||
|
||||
jobs:
|
||||
debug:
|
||||
|
|
|
@ -13,6 +13,7 @@ env:
|
|||
CI_GRADLE_ARG_PROPERTIES: >
|
||||
-Porg.gradle.jvmargs=-Xmx4g
|
||||
-Porg.gradle.parallel=false
|
||||
--no-daemon
|
||||
|
||||
jobs:
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ on:
|
|||
env:
|
||||
CI_GRADLE_ARG_PROPERTIES: >
|
||||
-Porg.gradle.jvmargs=-Xmx4g
|
||||
-Porg.gradle.parallel=false
|
||||
--no-daemon
|
||||
|
||||
jobs:
|
||||
check:
|
||||
|
|
|
@ -8,8 +8,9 @@ on:
|
|||
# Enrich gradle.properties for CI/CD
|
||||
env:
|
||||
CI_GRADLE_ARG_PROPERTIES: >
|
||||
-Porg.gradle.jvmargs=-Xmx2g
|
||||
-Porg.gradle.jvmargs=-Xmx4g
|
||||
-Porg.gradle.parallel=false
|
||||
--no-daemon
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
|
|
|
@ -29,7 +29,7 @@ buildscript {
|
|||
classpath 'com.google.android.gms:oss-licenses-plugin:0.10.5'
|
||||
classpath "com.likethesalad.android:stem-plugin:2.1.1"
|
||||
classpath 'org.owasp:dependency-check-gradle:7.1.1'
|
||||
classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.6.21"
|
||||
classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.0"
|
||||
classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0"
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
@ -43,7 +43,7 @@ plugins {
|
|||
id "io.gitlab.arturbosch.detekt" version "1.20.0"
|
||||
|
||||
// Dependency Analysis
|
||||
id 'com.autonomousapps.dependency-analysis' version "1.8.0"
|
||||
id 'com.autonomousapps.dependency-analysis' version "1.9.0"
|
||||
}
|
||||
|
||||
// https://github.com/jeremylong/DependencyCheck
|
||||
|
@ -267,6 +267,8 @@ dependencyAnalysis {
|
|||
onUnusedDependencies {
|
||||
// False positives
|
||||
exclude(
|
||||
"androidx.fragment:fragment-testing",
|
||||
"com.facebook.soloader:soloader",
|
||||
"com.vanniktech:emoji-google",
|
||||
"com.vanniktech:emoji-material",
|
||||
"org.maplibre.gl:android-plugin-annotation-v9",
|
||||
|
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
|||
Fix | Some user verification requests couldn't be accepted/declined
|
|
@ -0,0 +1 @@
|
|||
[Location sharing] Fix stop of a live not possible from another device
|
|
@ -0,0 +1 @@
|
|||
[Location sharing] - Stop any active live before starting a new one
|
|
@ -0,0 +1 @@
|
|||
Poll view state unit tests
|
|
@ -0,0 +1 @@
|
|||
[Location Share] - Adding missing prefix "u=" for uncertainty in geo URI
|
|
@ -0,0 +1 @@
|
|||
Update the PR process doc to come back to one reviewer with optional additional reviewers.
|
|
@ -21,6 +21,7 @@ def markwon = "4.6.2"
|
|||
def moshi = "1.13.0"
|
||||
def lifecycle = "2.4.1"
|
||||
def flowBinding = "1.2.0"
|
||||
def flipper = "0.151.1"
|
||||
def epoxy = "4.6.2"
|
||||
def mavericks = "2.7.0"
|
||||
def glide = "4.13.2"
|
||||
|
@ -91,6 +92,10 @@ ext.libs = [
|
|||
'hiltAndroidTesting' : "com.google.dagger:hilt-android-testing:$dagger",
|
||||
'hiltCompiler' : "com.google.dagger:hilt-compiler:$dagger"
|
||||
],
|
||||
flipper : [
|
||||
'flipper' : "com.facebook.flipper:flipper:$flipper",
|
||||
'flipperNetworkPlugin' : "com.facebook.flipper:flipper-network-plugin:$flipper",
|
||||
],
|
||||
squareup : [
|
||||
'moshi' : "com.squareup.moshi:moshi:$moshi",
|
||||
'moshiKt' : "com.squareup.moshi:moshi-kotlin:$moshi",
|
||||
|
|
|
@ -83,15 +83,16 @@ Exceptions can occur:
|
|||
|
||||
##### PR Review Assignment
|
||||
|
||||
We use automatic assignment for PR reviews. A PR is automatically routed by GitHub to 2 team members using the round robin algorithm. The process is the following:
|
||||
We use automatic assignment for PR reviews. **A PR is automatically routed by GitHub to one team member** using the round robin algorithm. Additional reviewers can be used for complex changes or when the first reviewer is not confident enough on the changes.
|
||||
The process is the following:
|
||||
|
||||
- The PR creator can assign specific people if they have another Android developer in their team or they think a specific reviewer should take a look at the PR.
|
||||
- If there are missing reviewers, the PR creator assigns the [element-android-reviewers](https://github.com/orgs/vector-im/teams/element-android-reviewers) team as a reviewer.
|
||||
- GitHub automatically assigns other reviewers. If one of the chosen reviewers is not available (holiday, etc.), remove them and set again the team, GitHub will select another reviewer.
|
||||
- The PR creator selects the [element-android-reviewers](https://github.com/orgs/vector-im/teams/element-android-reviewers) team as a reviewer.
|
||||
- GitHub automatically assign the reviewer. If the reviewer is not available (holiday, etc.), remove them and set again the team, GitHub will select another reviewer.
|
||||
- Alternatively, the PR creator can directly assign specific people if they have another Android developer in their team or they think a specific reviewer should take a look at their PR.
|
||||
- Reviewers get a notification to make the review: they review the code following the good practice (see the rest of this document).
|
||||
- After making their own review, if they feel not confident enough, they can ask another person for a full review, or they can tag someone within a PR comment to check specific lines.
|
||||
|
||||
For PRs coming from the community, the issue wrangler can assign either the team [element-android-reviewers](https://github.com/orgs/vector-im/teams/element-android-reviewers) or any members directly.
|
||||
For PRs coming from the community, the issue wrangler can assign either the team [element-android-reviewers](https://github.com/orgs/vector-im/teams/element-android-reviewers) or any member directly.
|
||||
|
||||
##### PR review time
|
||||
|
||||
|
@ -102,6 +103,7 @@ Some tips to achieve it:
|
|||
- Set up your GH notifications correctly
|
||||
- Check your pulls page: [https://github.com/pulls](https://github.com/pulls)
|
||||
- Check your pending assigned PRs before starting or resuming your day to day tasks
|
||||
- If you are busy with high priority tasks, inform the author. They will find another developer
|
||||
|
||||
It is hard to define a deadline for a review. It depends on the PR size and the complexity. Let's start with a goal of 24h (working day!) for a PR smaller than 500 lines. If bigger, the submitter and the reviewer should discuss.
|
||||
|
||||
|
|
|
@ -16,9 +16,11 @@
|
|||
|
||||
package org.matrix.android.sdk.api.session.room.location
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
|
||||
/**
|
||||
* Manage all location sharing related features.
|
||||
|
@ -59,5 +61,13 @@ interface LocationSharingService {
|
|||
/**
|
||||
* Returns a LiveData on the list of current running live location shares.
|
||||
*/
|
||||
@MainThread
|
||||
fun getRunningLiveLocationShareSummaries(): LiveData<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>>
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import com.squareup.moshi.JsonClass
|
|||
@JsonClass(generateAdapter = true)
|
||||
data class LocationInfo(
|
||||
/**
|
||||
* Required. RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30' representing this location.
|
||||
* Required. RFC5870 formatted geo uri 'geo:latitude,longitude;u=uncertainty' like 'geo:40.05,29.24;u=30' representing this location.
|
||||
*/
|
||||
@Json(name = "uri") val geoUri: String? = null,
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ data class MessageLocationContent(
|
|||
@Json(name = "body") override val body: String,
|
||||
|
||||
/**
|
||||
* Required. RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30' representing this location.
|
||||
* Required. RFC5870 formatted geo uri 'geo:latitude,longitude;u=uncertainty' like 'geo:40.05,29.24;u=30' representing this location.
|
||||
*/
|
||||
@Json(name = "geo_uri") val geoUri: String,
|
||||
|
||||
|
|
|
@ -25,4 +25,7 @@ data class PollCreationInfo(
|
|||
@Json(name = "kind") val kind: PollType? = PollType.DISCLOSED_UNSTABLE,
|
||||
@Json(name = "max_selections") val maxSelections: Int = 1,
|
||||
@Json(name = "answers") val answers: List<PollAnswer>? = null
|
||||
)
|
||||
) {
|
||||
|
||||
fun isUndisclosed() = kind in listOf(PollType.UNDISCLOSED_UNSTABLE, PollType.UNDISCLOSED)
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ internal class VerificationMessageProcessor @Inject constructor(
|
|||
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
|
||||
// the message should be ignored by the receiver.
|
||||
|
||||
if (event.ageLocalTs != null && !VerificationService.isValidRequest(event.ageLocalTs, clock.epochMillis())) return Unit.also {
|
||||
if (!VerificationService.isValidRequest(event.ageLocalTs, clock.epochMillis())) return Unit.also {
|
||||
Timber.d("## SAS Verification live observer: msgId: ${event.eventId} is outdated age:$event.ageLocalTs ms")
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -130,7 +130,7 @@ internal fun EventEntity.asDomain(castJsonNumbers: Boolean = false): Event {
|
|||
internal fun Event.toEntity(
|
||||
roomId: String,
|
||||
sendState: SendState,
|
||||
ageLocalTs: Long?,
|
||||
ageLocalTs: Long,
|
||||
contentToInject: String? = null
|
||||
): EventEntity {
|
||||
return EventMapper.map(this, roomId).apply {
|
||||
|
|
|
@ -52,6 +52,10 @@ internal class MigrateSessionTo030(realm: DynamicRealm) : RealmMigrator(realm, 3
|
|||
timelineEvents.deleteAllFromRealm()
|
||||
}
|
||||
chunks.deleteAllFromRealm()
|
||||
Timber.d("MigrateSessionTo030: $nbOfDeletedChunks deleted chunk(s), $nbOfDeletedTimelineEvents deleted TimelineEvent(s) and $nbOfDeletedEvents deleted Event(s).")
|
||||
Timber.d(
|
||||
"MigrateSessionTo030: $nbOfDeletedChunks deleted chunk(s)," +
|
||||
" $nbOfDeletedTimelineEvents deleted TimelineEvent(s)" +
|
||||
" and $nbOfDeletedEvents deleted Event(s)."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -76,7 +76,7 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveIn
|
|||
realm: Realm,
|
||||
roomId: String,
|
||||
userId: String,
|
||||
ignoredEventId: String
|
||||
ignoredEventId: String,
|
||||
): List<LiveLocationShareAggregatedSummaryEntity> {
|
||||
return LiveLocationShareAggregatedSummaryEntity
|
||||
.whereRoomId(realm, roomId = roomId)
|
||||
|
|
|
@ -51,10 +51,14 @@ import org.matrix.android.sdk.internal.session.room.directory.DefaultSetRoomDire
|
|||
import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask
|
||||
import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask
|
||||
import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask
|
||||
import org.matrix.android.sdk.internal.session.room.location.CheckIfExistingActiveLiveTask
|
||||
import org.matrix.android.sdk.internal.session.room.location.DefaultCheckIfExistingActiveLiveTask
|
||||
import org.matrix.android.sdk.internal.session.room.location.DefaultGetActiveBeaconInfoForUserTask
|
||||
import org.matrix.android.sdk.internal.session.room.location.DefaultSendLiveLocationTask
|
||||
import org.matrix.android.sdk.internal.session.room.location.DefaultSendStaticLocationTask
|
||||
import org.matrix.android.sdk.internal.session.room.location.DefaultStartLiveLocationShareTask
|
||||
import org.matrix.android.sdk.internal.session.room.location.DefaultStopLiveLocationShareTask
|
||||
import org.matrix.android.sdk.internal.session.room.location.GetActiveBeaconInfoForUserTask
|
||||
import org.matrix.android.sdk.internal.session.room.location.SendLiveLocationTask
|
||||
import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask
|
||||
import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask
|
||||
|
@ -319,4 +323,10 @@ internal abstract class RoomModule {
|
|||
|
||||
@Binds
|
||||
abstract fun bindSendLiveLocationTask(task: DefaultSendLiveLocationTask): SendLiveLocationTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindGetActiveBeaconInfoForUserTask(task: DefaultGetActiveBeaconInfoForUserTask): GetActiveBeaconInfoForUserTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindCheckIfExistingActiveLiveTask(task: DefaultCheckIfExistingActiveLiveTask): CheckIfExistingActiveLiveTask
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@
|
|||
package org.matrix.android.sdk.internal.session.room.location
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
@ -25,9 +26,12 @@ import org.matrix.android.sdk.api.session.room.location.LocationSharingService
|
|||
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
|
||||
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper
|
||||
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.query.findRunningLiveInRoom
|
||||
import org.matrix.android.sdk.internal.database.query.where
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
|
||||
internal class DefaultLocationSharingService @AssistedInject constructor(
|
||||
|
@ -37,6 +41,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
|
|||
private val sendLiveLocationTask: SendLiveLocationTask,
|
||||
private val startLiveLocationShareTask: StartLiveLocationShareTask,
|
||||
private val stopLiveLocationShareTask: StopLiveLocationShareTask,
|
||||
private val checkIfExistingActiveLiveTask: CheckIfExistingActiveLiveTask,
|
||||
private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper,
|
||||
) : LocationSharingService {
|
||||
|
||||
|
@ -68,6 +73,13 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
|
|||
}
|
||||
|
||||
override suspend fun startLiveLocationShare(timeoutMillis: Long): UpdateLiveLocationShareResult {
|
||||
// Ensure to stop any active live before starting a new one
|
||||
if (checkIfExistingActiveLive()) {
|
||||
val result = stopLiveLocationShare()
|
||||
if (result is UpdateLiveLocationShareResult.Failure) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
val params = StartLiveLocationShareTask.Params(
|
||||
roomId = roomId,
|
||||
timeoutMillis = timeoutMillis
|
||||
|
@ -75,6 +87,13 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
|
|||
return startLiveLocationShareTask.execute(params)
|
||||
}
|
||||
|
||||
private suspend fun checkIfExistingActiveLive(): Boolean {
|
||||
val params = CheckIfExistingActiveLiveTask.Params(
|
||||
roomId = roomId
|
||||
)
|
||||
return checkIfExistingActiveLiveTask.execute(params)
|
||||
}
|
||||
|
||||
override suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult {
|
||||
val params = StopLiveLocationShareTask.Params(
|
||||
roomId = roomId,
|
||||
|
@ -88,4 +107,15 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
|
|||
liveLocationShareAggregatedSummaryMapper
|
||||
)
|
||||
}
|
||||
|
||||
override fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData<Optional<LiveLocationShareAggregatedSummary>> {
|
||||
return Transformations.map(
|
||||
monarchy.findAllMappedWithChanges(
|
||||
{ LiveLocationShareAggregatedSummaryEntity.where(it, roomId = roomId, eventId = beaconInfoEventId) },
|
||||
liveLocationShareAggregatedSummaryMapper
|
||||
)
|
||||
) {
|
||||
it.firstOrNull().toOptional()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -16,17 +16,13 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.room.location
|
||||
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
|
||||
import org.matrix.android.sdk.internal.di.UserId
|
||||
import org.matrix.android.sdk.internal.session.room.state.SendStateTask
|
||||
import org.matrix.android.sdk.internal.session.room.state.StateEventDataSource
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -37,13 +33,12 @@ internal interface StopLiveLocationShareTask : Task<StopLiveLocationShareTask.Pa
|
|||
}
|
||||
|
||||
internal class DefaultStopLiveLocationShareTask @Inject constructor(
|
||||
@UserId private val userId: String,
|
||||
private val sendStateTask: SendStateTask,
|
||||
private val stateEventDataSource: StateEventDataSource,
|
||||
private val getActiveBeaconInfoForUserTask: GetActiveBeaconInfoForUserTask,
|
||||
) : StopLiveLocationShareTask {
|
||||
|
||||
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 content = beaconInfoStateEvent.getClearContent()?.toModel<MessageBeaconInfoContent>() ?: return getResultForIncorrectBeaconInfoEvent()
|
||||
val updatedContent = content.copy(isLive = false).toContent()
|
||||
|
@ -68,17 +63,10 @@ internal class DefaultStopLiveLocationShareTask @Inject constructor(
|
|||
private fun getResultForIncorrectBeaconInfoEvent() =
|
||||
UpdateLiveLocationShareResult.Failure(Exception("incorrect last beacon info event"))
|
||||
|
||||
private fun getLiveLocationBeaconInfoForUser(userId: String, roomId: String): Event? {
|
||||
return EventType.STATE_ROOM_BEACON_INFO
|
||||
.mapNotNull {
|
||||
stateEventDataSource.getStateEvent(
|
||||
roomId = roomId,
|
||||
eventType = it,
|
||||
stateKey = QueryStringValue.Equals(userId)
|
||||
)
|
||||
}
|
||||
.firstOrNull { beaconInfoEvent ->
|
||||
beaconInfoEvent.getClearContent()?.toModel<MessageBeaconInfoContent>()?.isLive.orFalse()
|
||||
}
|
||||
private suspend fun getActiveLiveLocationBeaconInfoForUser(roomId: String): Event? {
|
||||
val params = GetActiveBeaconInfoForUserTask.Params(
|
||||
roomId = roomId
|
||||
)
|
||||
return getActiveBeaconInfoForUserTask.execute(params)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,7 +116,7 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
|
|||
if (roomMemberEvent.eventId == null || roomMemberEvent.stateKey == null || roomMemberEvent.type == null) {
|
||||
continue
|
||||
}
|
||||
val ageLocalTs = roomMemberEvent.unsignedData?.age?.let { now - it }
|
||||
val ageLocalTs = now - (roomMemberEvent.unsignedData?.age ?: 0)
|
||||
val eventEntity = roomMemberEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
|
||||
CurrentStateEventEntity.getOrCreate(
|
||||
realm,
|
||||
|
|
|
@ -209,7 +209,8 @@ internal class DefaultFetchThreadTimelineTask @Inject constructor(
|
|||
* Create an EventEntity to be added in the TimelineEventEntity.
|
||||
*/
|
||||
private fun createEventEntity(roomId: String, event: Event, realm: Realm): EventEntity {
|
||||
val ageLocalTs = event.unsignedData?.age?.let { clock.epochMillis() - it }
|
||||
val now = clock.epochMillis()
|
||||
val ageLocalTs = now - (event.unsignedData?.age ?: 0)
|
||||
return event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
|
||||
}
|
||||
|
||||
|
|
|
@ -708,7 +708,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns RFC5870 formatted geo uri 'geo:latitude,longitude;uncertainty' like 'geo:40.05,29.24;30'
|
||||
* Returns RFC5870 formatted geo uri 'geo:latitude,longitude;u=uncertainty' like 'geo:40.05,29.24;u=30'
|
||||
* Uncertainty of the location is in meters and not required.
|
||||
*/
|
||||
private fun buildGeoUri(latitude: Double, longitude: Double, uncertainty: Double?): String {
|
||||
|
@ -718,7 +718,7 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||
append(",")
|
||||
append(longitude)
|
||||
uncertainty?.let {
|
||||
append(";")
|
||||
append(";u=")
|
||||
append(it)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -142,7 +142,7 @@ internal class TokenChunkEventPersistor @Inject constructor(
|
|||
val now = clock.epochMillis()
|
||||
|
||||
stateEvents?.forEach { stateEvent ->
|
||||
val ageLocalTs = stateEvent.unsignedData?.age?.let { now - it }
|
||||
val ageLocalTs = now - (stateEvent.unsignedData?.age ?: 0)
|
||||
val stateEventEntity = stateEvent.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
|
||||
currentChunk.addStateEvent(roomId, stateEventEntity, direction)
|
||||
if (stateEvent.type == EventType.STATE_ROOM_MEMBER && stateEvent.stateKey != null) {
|
||||
|
@ -155,7 +155,7 @@ internal class TokenChunkEventPersistor @Inject constructor(
|
|||
if (event.eventId == null || event.senderId == null) {
|
||||
return@forEach
|
||||
}
|
||||
val ageLocalTs = event.unsignedData?.age?.let { now - it }
|
||||
val ageLocalTs = now - (event.unsignedData?.age ?: 0)
|
||||
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, EventInsertType.PAGINATION)
|
||||
if (event.type == EventType.STATE_ROOM_MEMBER && event.stateKey != null) {
|
||||
val contentToUse = if (direction == PaginationDirection.BACKWARDS) {
|
||||
|
|
|
@ -244,7 +244,7 @@ internal class RoomSyncHandler @Inject constructor(
|
|||
if (event.eventId == null || event.stateKey == null || event.type == null) {
|
||||
continue
|
||||
}
|
||||
val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
|
||||
val ageLocalTs = syncLocalTimestampMillis - (event.unsignedData?.age ?: 0)
|
||||
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
|
||||
Timber.v("## received state event ${event.type} and key ${event.stateKey}")
|
||||
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
|
||||
|
@ -306,7 +306,7 @@ internal class RoomSyncHandler @Inject constructor(
|
|||
if (event.stateKey == null || event.type == null) {
|
||||
return@forEach
|
||||
}
|
||||
val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
|
||||
val ageLocalTs = syncLocalTimestampMillis - (event.unsignedData?.age ?: 0)
|
||||
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
|
||||
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
|
||||
eventId = eventEntity.eventId
|
||||
|
@ -336,7 +336,7 @@ internal class RoomSyncHandler @Inject constructor(
|
|||
if (event.eventId == null || event.stateKey == null || event.type == null) {
|
||||
continue
|
||||
}
|
||||
val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
|
||||
val ageLocalTs = syncLocalTimestampMillis - (event.unsignedData?.age ?: 0)
|
||||
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
|
||||
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
|
||||
eventId = event.eventId
|
||||
|
@ -348,7 +348,7 @@ internal class RoomSyncHandler @Inject constructor(
|
|||
if (event.eventId == null || event.senderId == null || event.type == null) {
|
||||
continue
|
||||
}
|
||||
val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
|
||||
val ageLocalTs = syncLocalTimestampMillis - (event.unsignedData?.age ?: 0)
|
||||
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
|
||||
if (event.stateKey != null) {
|
||||
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
|
||||
|
@ -401,7 +401,10 @@ internal class RoomSyncHandler @Inject constructor(
|
|||
for (rawEvent in eventList) {
|
||||
// It's annoying roomId is not there, but lot of code rely on it.
|
||||
// And had to do it now as copy would delete all decryption results..
|
||||
val event = rawEvent.copy(roomId = roomId)
|
||||
val ageLocalTs = syncLocalTimestampMillis - (rawEvent.unsignedData?.age ?: 0)
|
||||
val event = rawEvent.copy(roomId = roomId).also {
|
||||
it.ageLocalTs = ageLocalTs
|
||||
}
|
||||
if (event.eventId == null || event.senderId == null || event.type == null) {
|
||||
continue
|
||||
}
|
||||
|
@ -423,7 +426,6 @@ internal class RoomSyncHandler @Inject constructor(
|
|||
contentToInject = threadsAwarenessHandler.makeEventThreadAware(realm, roomId, event)
|
||||
}
|
||||
|
||||
val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
|
||||
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs, contentToInject).copyToRealmOrIgnore(realm, insertType)
|
||||
if (event.stateKey != null) {
|
||||
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
|
||||
|
|
|
@ -53,6 +53,7 @@ import org.matrix.android.sdk.internal.session.permalinks.PermalinkFactory
|
|||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||
import org.matrix.android.sdk.internal.session.room.timeline.GetEventTask
|
||||
import org.matrix.android.sdk.internal.util.awaitTransaction
|
||||
import org.matrix.android.sdk.internal.util.time.Clock
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -64,7 +65,8 @@ internal class ThreadsAwarenessHandler @Inject constructor(
|
|||
private val permalinkFactory: PermalinkFactory,
|
||||
@SessionDatabase private val monarchy: Monarchy,
|
||||
private val lightweightSettingsStorage: LightweightSettingsStorage,
|
||||
private val getEventTask: GetEventTask
|
||||
private val getEventTask: GetEventTask,
|
||||
private val clock: Clock,
|
||||
) {
|
||||
|
||||
// This caching is responsible to improve the performance when we receive a root event
|
||||
|
@ -120,7 +122,7 @@ internal class ThreadsAwarenessHandler @Inject constructor(
|
|||
private suspend fun fetchThreadsEvents(threadsToFetch: Map<String, String>) {
|
||||
val eventEntityList = threadsToFetch.mapNotNull { (eventId, roomId) ->
|
||||
fetchEvent(eventId, roomId)?.let {
|
||||
it.toEntity(roomId, SendState.SYNCED, it.ageLocalTs)
|
||||
it.toEntity(roomId, SendState.SYNCED, it.ageLocalTs ?: clock.epochMillis())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ private const val A_TIMEOUT_MILLIS = 15 * 60 * 1000L
|
|||
private const val A_LATITUDE = 40.05
|
||||
private const val A_LONGITUDE = 29.24
|
||||
private const val A_UNCERTAINTY = 30.0
|
||||
private const val A_GEO_URI = "geo:$A_LATITUDE,$A_LONGITUDE;$A_UNCERTAINTY"
|
||||
private const val A_GEO_URI = "geo:$A_LATITUDE,$A_LONGITUDE;u=$A_UNCERTAINTY"
|
||||
|
||||
internal class LiveLocationAggregationProcessorTest {
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -16,18 +16,27 @@
|
|||
|
||||
package org.matrix.android.sdk.internal.session.room.location
|
||||
|
||||
import androidx.arch.core.util.Function
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkAll
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
|
||||
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
import org.matrix.android.sdk.api.util.toOptional
|
||||
import org.matrix.android.sdk.internal.database.mapper.LiveLocationShareAggregatedSummaryMapper
|
||||
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
|
||||
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
|
||||
|
@ -46,24 +55,30 @@ private const val A_TIMEOUT = 15_000L
|
|||
@ExperimentalCoroutinesApi
|
||||
internal class DefaultLocationSharingServiceTest {
|
||||
|
||||
private val fakeRoomId = A_ROOM_ID
|
||||
private val fakeMonarchy = FakeMonarchy()
|
||||
private val sendStaticLocationTask = mockk<SendStaticLocationTask>()
|
||||
private val sendLiveLocationTask = mockk<SendLiveLocationTask>()
|
||||
private val startLiveLocationShareTask = mockk<StartLiveLocationShareTask>()
|
||||
private val stopLiveLocationShareTask = mockk<StopLiveLocationShareTask>()
|
||||
private val checkIfExistingActiveLiveTask = mockk<CheckIfExistingActiveLiveTask>()
|
||||
private val fakeLiveLocationShareAggregatedSummaryMapper = mockk<LiveLocationShareAggregatedSummaryMapper>()
|
||||
|
||||
private val defaultLocationSharingService = DefaultLocationSharingService(
|
||||
roomId = fakeRoomId,
|
||||
roomId = A_ROOM_ID,
|
||||
monarchy = fakeMonarchy.instance,
|
||||
sendStaticLocationTask = sendStaticLocationTask,
|
||||
sendLiveLocationTask = sendLiveLocationTask,
|
||||
startLiveLocationShareTask = startLiveLocationShareTask,
|
||||
stopLiveLocationShareTask = stopLiveLocationShareTask,
|
||||
checkIfExistingActiveLiveTask = checkIfExistingActiveLiveTask,
|
||||
liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockkStatic("androidx.lifecycle.Transformations")
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
|
@ -117,17 +132,65 @@ internal class DefaultLocationSharingServiceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `live location share can be started with a given timeout`() = runTest {
|
||||
fun `given existing active live can be stopped when starting a live then the current live is stopped and the new live is started`() = runTest {
|
||||
coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns true
|
||||
coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success("stopped-event-id")
|
||||
coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
|
||||
|
||||
val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
|
||||
|
||||
result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
|
||||
val expectedParams = StartLiveLocationShareTask.Params(
|
||||
val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
|
||||
roomId = A_ROOM_ID
|
||||
)
|
||||
coVerify { checkIfExistingActiveLiveTask.execute(expectedCheckExistingParams) }
|
||||
val expectedStopParams = StopLiveLocationShareTask.Params(
|
||||
roomId = A_ROOM_ID
|
||||
)
|
||||
coVerify { stopLiveLocationShareTask.execute(expectedStopParams) }
|
||||
val expectedStartParams = StartLiveLocationShareTask.Params(
|
||||
roomId = A_ROOM_ID,
|
||||
timeoutMillis = A_TIMEOUT
|
||||
)
|
||||
coVerify { startLiveLocationShareTask.execute(expectedParams) }
|
||||
coVerify { startLiveLocationShareTask.execute(expectedStartParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given existing active live cannot be stopped when starting a live then the result is failure`() = runTest {
|
||||
coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns true
|
||||
val error = Throwable()
|
||||
coEvery { stopLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Failure(error)
|
||||
|
||||
val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
|
||||
|
||||
result shouldBeEqualTo UpdateLiveLocationShareResult.Failure(error)
|
||||
val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
|
||||
roomId = A_ROOM_ID
|
||||
)
|
||||
coVerify { checkIfExistingActiveLiveTask.execute(expectedCheckExistingParams) }
|
||||
val expectedStopParams = StopLiveLocationShareTask.Params(
|
||||
roomId = A_ROOM_ID
|
||||
)
|
||||
coVerify { stopLiveLocationShareTask.execute(expectedStopParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given no existing active live when starting a live then the new live is started`() = runTest {
|
||||
coEvery { checkIfExistingActiveLiveTask.execute(any()) } returns false
|
||||
coEvery { startLiveLocationShareTask.execute(any()) } returns UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
|
||||
|
||||
val result = defaultLocationSharingService.startLiveLocationShare(A_TIMEOUT)
|
||||
|
||||
result shouldBeEqualTo UpdateLiveLocationShareResult.Success(AN_EVENT_ID)
|
||||
val expectedCheckExistingParams = CheckIfExistingActiveLiveTask.Params(
|
||||
roomId = A_ROOM_ID
|
||||
)
|
||||
coVerify { checkIfExistingActiveLiveTask.execute(expectedCheckExistingParams) }
|
||||
val expectedStartParams = StartLiveLocationShareTask.Params(
|
||||
roomId = A_ROOM_ID,
|
||||
timeoutMillis = A_TIMEOUT
|
||||
)
|
||||
coVerify { startLiveLocationShareTask.execute(expectedStartParams) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -154,7 +217,7 @@ internal class DefaultLocationSharingServiceTest {
|
|||
)
|
||||
|
||||
fakeMonarchy.givenWhere<LiveLocationShareAggregatedSummaryEntity>()
|
||||
.givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, fakeRoomId)
|
||||
.givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.ROOM_ID, A_ROOM_ID)
|
||||
.givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.IS_ACTIVE, true)
|
||||
.givenIsNotEmpty(LiveLocationShareAggregatedSummaryEntityFields.USER_ID)
|
||||
.givenIsNotNull(LiveLocationShareAggregatedSummaryEntityFields.LAST_LOCATION_CONTENT)
|
||||
|
@ -168,4 +231,38 @@ internal class DefaultLocationSharingServiceTest {
|
|||
|
||||
result shouldBeEqualTo listOf(summary)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given an event id when getting livedata on corresponding live summary then it is correctly computed`() {
|
||||
val entity = LiveLocationShareAggregatedSummaryEntity()
|
||||
val summary = LiveLocationShareAggregatedSummary(
|
||||
userId = "",
|
||||
isActive = true,
|
||||
endOfLiveTimestampMillis = 123,
|
||||
lastLocationDataContent = null
|
||||
)
|
||||
|
||||
fakeMonarchy.givenWhere<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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,11 +27,10 @@ import org.matrix.android.sdk.api.session.events.model.Event
|
|||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconInfoContent
|
||||
import org.matrix.android.sdk.internal.session.room.state.SendStateTask
|
||||
import org.matrix.android.sdk.test.fakes.FakeGetActiveBeaconInfoForUserTask
|
||||
import org.matrix.android.sdk.test.fakes.FakeSendStateTask
|
||||
import org.matrix.android.sdk.test.fakes.FakeStateEventDataSource
|
||||
|
||||
private const val A_USER_ID = "user-id"
|
||||
private const val A_ROOM_ID = "room-id"
|
||||
|
@ -43,12 +42,11 @@ private const val AN_EPOCH = 1655210176L
|
|||
class DefaultStopLiveLocationShareTaskTest {
|
||||
|
||||
private val fakeSendStateTask = FakeSendStateTask()
|
||||
private val fakeStateEventDataSource = FakeStateEventDataSource()
|
||||
private val fakeGetActiveBeaconInfoForUserTask = FakeGetActiveBeaconInfoForUserTask()
|
||||
|
||||
private val defaultStopLiveLocationShareTask = DefaultStopLiveLocationShareTask(
|
||||
userId = A_USER_ID,
|
||||
sendStateTask = fakeSendStateTask,
|
||||
stateEventDataSource = fakeStateEventDataSource.instance
|
||||
getActiveBeaconInfoForUserTask = fakeGetActiveBeaconInfoForUserTask
|
||||
)
|
||||
|
||||
@After
|
||||
|
@ -67,7 +65,7 @@ class DefaultStopLiveLocationShareTaskTest {
|
|||
unstableTimestampMillis = AN_EPOCH
|
||||
).toContent()
|
||||
)
|
||||
fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent)
|
||||
fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
|
||||
fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
|
||||
|
||||
val result = defaultStopLiveLocationShareTask.execute(params)
|
||||
|
@ -78,20 +76,21 @@ class DefaultStopLiveLocationShareTaskTest {
|
|||
isLive = false,
|
||||
unstableTimestampMillis = AN_EPOCH
|
||||
).toContent()
|
||||
val expectedParams = SendStateTask.Params(
|
||||
val expectedSendParams = SendStateTask.Params(
|
||||
roomId = params.roomId,
|
||||
stateKey = A_USER_ID,
|
||||
eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
|
||||
body = expectedBeaconContent
|
||||
)
|
||||
fakeSendStateTask.verifyExecuteRetry(
|
||||
params = expectedParams,
|
||||
params = expectedSendParams,
|
||||
remainingRetry = 3
|
||||
)
|
||||
fakeStateEventDataSource.verifyGetStateEvent(
|
||||
roomId = params.roomId,
|
||||
eventType = EventType.STATE_ROOM_BEACON_INFO.first(),
|
||||
stateKey = A_USER_ID
|
||||
val expectedGetBeaconParams = GetActiveBeaconInfoForUserTask.Params(
|
||||
roomId = params.roomId
|
||||
)
|
||||
fakeGetActiveBeaconInfoForUserTask.verifyExecute(
|
||||
expectedGetBeaconParams
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -109,18 +108,15 @@ class DefaultStopLiveLocationShareTaskTest {
|
|||
unstableTimestampMillis = AN_EPOCH
|
||||
).toContent()
|
||||
),
|
||||
// incorrect content
|
||||
// null content
|
||||
Event(
|
||||
stateKey = A_USER_ID,
|
||||
content = MessageAudioContent(
|
||||
msgType = "",
|
||||
body = ""
|
||||
).toContent()
|
||||
content = null
|
||||
)
|
||||
)
|
||||
|
||||
incorrectCurrentStateEvents.forEach { currentStateEvent ->
|
||||
fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent)
|
||||
fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
|
||||
fakeSendStateTask.givenExecuteRetryReturns(AN_EVENT_ID)
|
||||
val params = StopLiveLocationShareTask.Params(roomId = A_ROOM_ID)
|
||||
|
||||
|
@ -141,7 +137,7 @@ class DefaultStopLiveLocationShareTaskTest {
|
|||
unstableTimestampMillis = AN_EPOCH
|
||||
).toContent()
|
||||
)
|
||||
fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent)
|
||||
fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
|
||||
fakeSendStateTask.givenExecuteRetryReturns("")
|
||||
|
||||
val result = defaultStopLiveLocationShareTask.execute(params)
|
||||
|
@ -160,7 +156,7 @@ class DefaultStopLiveLocationShareTaskTest {
|
|||
unstableTimestampMillis = AN_EPOCH
|
||||
).toContent()
|
||||
)
|
||||
fakeStateEventDataSource.givenGetStateEventReturns(currentStateEvent)
|
||||
fakeGetActiveBeaconInfoForUserTask.givenExecuteReturns(currentStateEvent)
|
||||
val error = Throwable()
|
||||
fakeSendStateTask.givenExecuteRetryThrows(error)
|
||||
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.matrix.android.sdk.test.fakes
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import io.mockk.MockKVerificationScope
|
||||
|
@ -60,10 +61,11 @@ internal class FakeMonarchy {
|
|||
realmEntities: List<T>,
|
||||
mappedResult: List<R>,
|
||||
mapper: Monarchy.Mapper<R, T>
|
||||
) {
|
||||
): LiveData<List<R>> {
|
||||
every { mapper.map(any()) } returns mockk()
|
||||
val monarchyQuery = slot<Monarchy.Query<T>>()
|
||||
val monarchyMapper = slot<Monarchy.Mapper<R, T>>()
|
||||
val result = MutableLiveData(mappedResult)
|
||||
every {
|
||||
instance.findAllMappedWithChanges(capture(monarchyQuery), capture(monarchyMapper))
|
||||
} answers {
|
||||
|
@ -71,7 +73,8 @@ internal class FakeMonarchy {
|
|||
realmEntities.forEach {
|
||||
monarchyMapper.captured.map(it)
|
||||
}
|
||||
MutableLiveData(mappedResult)
|
||||
result
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -372,7 +372,6 @@ dependencies {
|
|||
implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.10.0"
|
||||
|
||||
implementation libs.squareup.moshi
|
||||
implementation libs.squareup.moshiKt
|
||||
kapt libs.squareup.moshiKotlin
|
||||
|
||||
// Lifecycle
|
||||
|
@ -534,10 +533,10 @@ dependencies {
|
|||
}
|
||||
|
||||
// Flipper, debug builds only
|
||||
debugImplementation('com.facebook.flipper:flipper:0.150.0') {
|
||||
debugImplementation(libs.flipper.flipper) {
|
||||
exclude group: 'com.facebook.fbjni', module: 'fbjni'
|
||||
}
|
||||
debugImplementation('com.facebook.flipper:flipper-network-plugin:0.150.0') {
|
||||
debugImplementation(libs.flipper.flipperNetworkPlugin) {
|
||||
exclude group: 'com.facebook.fbjni', module: 'fbjni'
|
||||
}
|
||||
debugImplementation 'com.facebook.soloader:soloader:0.10.3'
|
||||
|
|
|
@ -44,11 +44,11 @@ class ActiveSessionHolder @Inject constructor(
|
|||
private val guardServiceStarter: GuardServiceStarter
|
||||
) {
|
||||
|
||||
private var activeSession: AtomicReference<Session?> = AtomicReference()
|
||||
private var activeSessionReference: AtomicReference<Session?> = AtomicReference()
|
||||
|
||||
fun setActiveSession(session: Session) {
|
||||
Timber.w("setActiveSession of ${session.myUserId}")
|
||||
activeSession.set(session)
|
||||
activeSessionReference.set(session)
|
||||
activeSessionDataSource.post(Option.just(session))
|
||||
|
||||
keyRequestHandler.start(session)
|
||||
|
@ -68,7 +68,7 @@ class ActiveSessionHolder @Inject constructor(
|
|||
it.removeListener(sessionListener)
|
||||
}
|
||||
|
||||
activeSession.set(null)
|
||||
activeSessionReference.set(null)
|
||||
activeSessionDataSource.post(Option.empty())
|
||||
|
||||
keyRequestHandler.stop()
|
||||
|
@ -80,15 +80,15 @@ class ActiveSessionHolder @Inject constructor(
|
|||
}
|
||||
|
||||
fun hasActiveSession(): Boolean {
|
||||
return activeSession.get() != null
|
||||
return activeSessionReference.get() != null
|
||||
}
|
||||
|
||||
fun getSafeActiveSession(): Session? {
|
||||
return activeSession.get()
|
||||
return activeSessionReference.get()
|
||||
}
|
||||
|
||||
fun getActiveSession(): Session {
|
||||
return activeSession.get()
|
||||
return activeSessionReference.get()
|
||||
?: throw IllegalStateException("You should authenticate before using this")
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ import im.vector.app.features.call.vectorCallService
|
|||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.createdirect.DirectRoomHelper
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class DialPadLookup @Inject constructor(
|
||||
|
@ -42,18 +43,23 @@ class DialPadLookup @Inject constructor(
|
|||
val sipUserId = thirdPartyUser.userId
|
||||
val nativeLookupResults = session.sipNativeLookup(thirdPartyUser.userId)
|
||||
// If I have a native user I check for an existing native room with him...
|
||||
val roomId = if (nativeLookupResults.isNotEmpty()) {
|
||||
if (nativeLookupResults.isNotEmpty()) {
|
||||
val nativeUserId = nativeLookupResults.first().userId
|
||||
if (nativeUserId == session.myUserId) {
|
||||
throw Failure.NumberIsYours
|
||||
}
|
||||
session.roomService().getExistingDirectRoomWithUser(nativeUserId)
|
||||
// if there is not, just create a DM with the sip user
|
||||
?: directRoomHelper.ensureDMExists(sipUserId)
|
||||
} else {
|
||||
// do the same if there is no corresponding native user.
|
||||
directRoomHelper.ensureDMExists(sipUserId)
|
||||
var nativeRoomId = session.roomService().getExistingDirectRoomWithUser(nativeUserId)
|
||||
if (nativeRoomId == null) {
|
||||
// if there is no existing native room with the existing native user,
|
||||
// just create a DM with the native user
|
||||
nativeRoomId = directRoomHelper.ensureDMExists(nativeUserId)
|
||||
}
|
||||
Timber.d("lookupPhoneNumber with nativeUserId: $nativeUserId and nativeRoomId: $nativeRoomId")
|
||||
return Result(userId = nativeUserId, roomId = nativeRoomId)
|
||||
}
|
||||
return Result(userId = sipUserId, roomId = roomId)
|
||||
// If there is no native user then we return sipUserId and sipRoomId - this is usually a PSTN call.
|
||||
val sipRoomId = directRoomHelper.ensureDMExists(sipUserId)
|
||||
Timber.d("lookupPhoneNumber with sipRoomId: $sipRoomId and sipUserId: $sipUserId")
|
||||
return Result(userId = sipUserId, roomId = sipRoomId)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,8 @@ sealed class VerificationAction : VectorViewModelAction {
|
|||
data class GotItConclusion(val verified: Boolean) : VerificationAction()
|
||||
object SkipVerification : VerificationAction()
|
||||
object VerifyFromPassphrase : VerificationAction()
|
||||
object ReadyPendingVerification : VerificationAction()
|
||||
object CancelPendingVerification : VerificationAction()
|
||||
data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction()
|
||||
object CancelledFromSsss : VerificationAction()
|
||||
object SecuredStorageHasBeenReset : VerificationAction()
|
||||
|
|
|
@ -360,6 +360,27 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
|||
as? SasVerificationTransaction)
|
||||
?.shortCodeDoesNotMatch()
|
||||
}
|
||||
is VerificationAction.ReadyPendingVerification -> {
|
||||
state.pendingRequest.invoke()?.let { request ->
|
||||
// will only be there for dm verif
|
||||
if (state.roomId != null) {
|
||||
session.cryptoService().verificationService()
|
||||
.readyPendingVerificationInDMs(
|
||||
supportedVerificationMethodsProvider.provide(),
|
||||
state.otherUserId,
|
||||
state.roomId,
|
||||
request.transactionId ?: ""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is VerificationAction.CancelPendingVerification -> {
|
||||
state.pendingRequest.invoke()?.let {
|
||||
session.cryptoService().verificationService()
|
||||
.cancelVerificationRequest(it)
|
||||
}
|
||||
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
|
||||
}
|
||||
is VerificationAction.GotItConclusion -> {
|
||||
if (state.isVerificationRequired && !action.verified) {
|
||||
// we should go back to first screen
|
||||
|
|
|
@ -21,6 +21,7 @@ import im.vector.app.R
|
|||
import im.vector.app.core.epoxy.bottomSheetDividerItem
|
||||
import im.vector.app.core.resources.ColorProvider
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.ui.list.buttonPositiveDestructiveButtonBarItem
|
||||
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
|
||||
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
|
||||
import im.vector.app.features.crypto.verification.epoxy.bottomSheetVerificationQrCodeItem
|
||||
|
@ -108,6 +109,15 @@ class VerificationChooseMethodController @Inject constructor(
|
|||
iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
|
||||
listener { host.listener?.doVerifyBySas() }
|
||||
}
|
||||
} else if (!state.isReadySent) {
|
||||
// a bit of a special case, if you tapped on the timeline cell but not on a button
|
||||
buttonPositiveDestructiveButtonBarItem {
|
||||
id("accept_decline")
|
||||
positiveText(host.stringProvider.getString(R.string.action_accept).toEpoxyCharSequence())
|
||||
destructiveText(host.stringProvider.getString(R.string.action_decline).toEpoxyCharSequence())
|
||||
positiveButtonClickAction { host.listener?.acceptRequest() }
|
||||
destructiveButtonClickAction { host.listener?.declineRequest() }
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isMe && state.canCrossSign) {
|
||||
|
@ -131,5 +141,7 @@ class VerificationChooseMethodController @Inject constructor(
|
|||
fun openCamera()
|
||||
fun doVerifyBySas()
|
||||
fun onClickOnWasNotMe()
|
||||
fun acceptRequest()
|
||||
fun declineRequest()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,6 +100,14 @@ class VerificationChooseMethodFragment @Inject constructor(
|
|||
sharedViewModel.itWasNotMe()
|
||||
}
|
||||
|
||||
override fun acceptRequest() {
|
||||
sharedViewModel.handle(VerificationAction.ReadyPendingVerification)
|
||||
}
|
||||
|
||||
override fun declineRequest() {
|
||||
sharedViewModel.handle(VerificationAction.CancelPendingVerification)
|
||||
}
|
||||
|
||||
private fun doOpenQRCodeScanner() {
|
||||
QrCodeScannerActivity.startForResult(requireActivity(), scanActivityResultLauncher)
|
||||
}
|
||||
|
|
|
@ -44,7 +44,8 @@ data class VerificationChooseMethodViewState(
|
|||
val qrCodeText: String? = null,
|
||||
val sasModeAvailable: Boolean = false,
|
||||
val isMe: Boolean = false,
|
||||
val canCrossSign: Boolean = false
|
||||
val canCrossSign: Boolean = false,
|
||||
val isReadySent: Boolean = false
|
||||
) : MavericksState
|
||||
|
||||
class VerificationChooseMethodViewModel @AssistedInject constructor(
|
||||
|
@ -81,7 +82,8 @@ class VerificationChooseMethodViewModel @AssistedInject constructor(
|
|||
copy(
|
||||
otherCanShowQrCode = pvr?.otherCanShowQrCode().orFalse(),
|
||||
otherCanScanQrCode = pvr?.otherCanScanQrCode().orFalse(),
|
||||
sasModeAvailable = pvr?.isSasSupported().orFalse()
|
||||
sasModeAvailable = pvr?.isSasSupported().orFalse(),
|
||||
isReadySent = pvr?.isReady.orFalse(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,7 @@ import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
|
|||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||
import im.vector.app.features.home.room.typing.TypingHelper
|
||||
import im.vector.app.features.location.LocationSharingServiceConnection
|
||||
import im.vector.app.features.location.live.StopLiveLocationShareUseCase
|
||||
import im.vector.app.features.notifications.NotificationDrawerManager
|
||||
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
|
||||
import im.vector.app.features.raw.wellknown.getOutboundSessionKeySharingStrategyOrDefault
|
||||
|
@ -92,6 +93,7 @@ import org.matrix.android.sdk.api.session.file.FileService
|
|||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.getStateEvent
|
||||
import org.matrix.android.sdk.api.session.room.getTimelineEvent
|
||||
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
|
||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
|
@ -133,8 +135,9 @@ class TimelineViewModel @AssistedInject constructor(
|
|||
private val decryptionFailureTracker: DecryptionFailureTracker,
|
||||
private val notificationDrawerManager: NotificationDrawerManager,
|
||||
private val locationSharingServiceConnection: LocationSharingServiceConnection,
|
||||
private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase,
|
||||
timelineFactory: TimelineFactory,
|
||||
appStateHandler: AppStateHandler
|
||||
appStateHandler: AppStateHandler,
|
||||
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
|
||||
Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener, LocationSharingServiceConnection.Callback {
|
||||
|
||||
|
@ -1139,7 +1142,12 @@ class TimelineViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun handleStopLiveLocationSharing() {
|
||||
locationSharingServiceConnection.stopLiveLocationSharing(room.roomId)
|
||||
viewModelScope.launch {
|
||||
val result = stopLiveLocationShareUseCase.execute(room.roomId)
|
||||
if (result is UpdateLiveLocationShareResult.Failure) {
|
||||
_viewEvents.post(RoomDetailViewEvents.Failure(throwable = result.error, showInDialog = true))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeRoomSummary() {
|
||||
|
@ -1310,7 +1318,7 @@ class TimelineViewModel @AssistedInject constructor(
|
|||
// we should also mark it as read here, for the scenario that the user
|
||||
// is already in the thread timeline
|
||||
markThreadTimelineAsReadLocal()
|
||||
locationSharingServiceConnection.unbind()
|
||||
locationSharingServiceConnection.unbind(this)
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,12 +59,6 @@ import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem
|
|||
import im.vector.app.features.home.room.detail.timeline.item.MessageVoiceItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollEnded
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollReady
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollSending
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollUndisclosed
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState.PollVoted
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollResponseData
|
||||
import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem
|
||||
import im.vector.app.features.home.room.detail.timeline.item.RedactedMessageItem_
|
||||
import im.vector.app.features.home.room.detail.timeline.item.VerificationRequestItem
|
||||
|
@ -81,18 +75,11 @@ import im.vector.app.features.location.UrlMapProvider
|
|||
import im.vector.app.features.location.toLocationData
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import im.vector.app.features.media.VideoContentRenderer
|
||||
import im.vector.app.features.poll.PollState
|
||||
import im.vector.app.features.poll.PollState.Ended
|
||||
import im.vector.app.features.poll.PollState.Ready
|
||||
import im.vector.app.features.poll.PollState.Sending
|
||||
import im.vector.app.features.poll.PollState.Undisclosed
|
||||
import im.vector.app.features.poll.PollState.Voted
|
||||
import im.vector.app.features.settings.VectorPreferences
|
||||
import im.vector.app.features.voice.AudioWaveformView
|
||||
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
|
||||
import me.gujun.android.span.span
|
||||
import org.matrix.android.sdk.api.MatrixUrls.isMxcUrl
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.crypto.attachments.toElementToDecrypt
|
||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||
|
@ -113,8 +100,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
|
|||
import org.matrix.android.sdk.api.session.room.model.message.MessageType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollAnswer
|
||||
import org.matrix.android.sdk.api.session.room.model.message.PollType
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getFileUrl
|
||||
import org.matrix.android.sdk.api.session.room.model.message.getThumbnailUrl
|
||||
import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
|
||||
|
@ -149,6 +134,7 @@ class MessageItemFactory @Inject constructor(
|
|||
private val vectorPreferences: VectorPreferences,
|
||||
private val urlMapProvider: UrlMapProvider,
|
||||
private val liveLocationShareMessageItemFactory: LiveLocationShareMessageItemFactory,
|
||||
private val pollItemViewStateFactory: PollItemViewStateFactory,
|
||||
) {
|
||||
|
||||
// TODO inject this properly?
|
||||
|
@ -251,62 +237,21 @@ class MessageItemFactory @Inject constructor(
|
|||
callback: TimelineEventController.Callback?,
|
||||
attributes: AbsMessageItem.Attributes,
|
||||
): PollItem {
|
||||
val pollResponseSummary = informationData.pollResponseAggregatedSummary
|
||||
val pollState = createPollState(informationData, pollResponseSummary, pollContent)
|
||||
val pollCreationInfo = pollContent.getBestPollCreationInfo()
|
||||
val questionText = pollCreationInfo?.question?.getBestQuestion().orEmpty()
|
||||
val question = createPollQuestion(informationData, questionText, callback)
|
||||
val optionViewStates = pollCreationInfo?.answers?.mapToOptions(pollState, informationData)
|
||||
val totalVotesText = createTotalVotesText(pollState, pollResponseSummary)
|
||||
val pollViewState = pollItemViewStateFactory.create(pollContent, informationData)
|
||||
|
||||
return PollItem_()
|
||||
.attributes(attributes)
|
||||
.eventId(informationData.eventId)
|
||||
.pollQuestion(question)
|
||||
.canVote(pollState.isVotable())
|
||||
.totalVotesText(totalVotesText)
|
||||
.optionViewStates(optionViewStates)
|
||||
.pollQuestion(createPollQuestion(informationData, pollViewState.question, callback))
|
||||
.canVote(pollViewState.canVote)
|
||||
.totalVotesText(pollViewState.totalVotes)
|
||||
.optionViewStates(pollViewState.optionViewStates)
|
||||
.edited(informationData.hasBeenEdited)
|
||||
.highlighted(highlight)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
.callback(callback)
|
||||
}
|
||||
|
||||
private fun createPollState(
|
||||
informationData: MessageInformationData,
|
||||
pollResponseSummary: PollResponseData?,
|
||||
pollContent: MessagePollContent,
|
||||
): PollState = when {
|
||||
!informationData.sendState.isSent() -> Sending
|
||||
pollResponseSummary?.isClosed.orFalse() -> Ended
|
||||
pollContent.getBestPollCreationInfo()?.kind == PollType.UNDISCLOSED -> Undisclosed
|
||||
pollResponseSummary?.myVote?.isNotEmpty().orFalse() -> Voted(pollResponseSummary?.totalVotes ?: 0)
|
||||
else -> Ready
|
||||
}
|
||||
|
||||
private fun List<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(
|
||||
informationData: MessageInformationData,
|
||||
question: String,
|
||||
|
@ -317,20 +262,6 @@ class MessageItemFactory @Inject constructor(
|
|||
question
|
||||
}.toEpoxyCharSequence()
|
||||
|
||||
private fun createTotalVotesText(
|
||||
pollState: PollState,
|
||||
pollResponseSummary: PollResponseData?,
|
||||
): String {
|
||||
val votes = pollResponseSummary?.totalVotes ?: 0
|
||||
return when {
|
||||
pollState is Ended -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_after_ended, votes, votes)
|
||||
pollState is Undisclosed -> ""
|
||||
pollState is Voted -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_voted, votes, votes)
|
||||
votes == 0 -> stringProvider.getString(R.string.poll_no_votes_cast)
|
||||
else -> stringProvider.getQuantityString(R.plurals.poll_total_vote_count_before_ended_and_not_voted, votes, votes)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildAudioMessageItem(
|
||||
params: TimelineItemFactoryParams,
|
||||
messageContent: MessageAudioContent,
|
||||
|
|
|
@ -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() ?: ""
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -91,7 +91,10 @@ data class PollResponseData(
|
|||
val totalVotes: Int = 0,
|
||||
val winnerVoteCount: Int = 0,
|
||||
val isClosed: Boolean = false
|
||||
) : Parcelable
|
||||
) : Parcelable {
|
||||
|
||||
fun getVoteSummaryOfAnOption(optionId: String) = votes?.get(optionId)
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class PollVoteSummaryData(
|
||||
|
|
|
@ -30,7 +30,7 @@ data class LocationData(
|
|||
|
||||
/**
|
||||
* Creates location data from a MessageLocationContent.
|
||||
* "geo:40.05,29.24;30" -> LocationData(40.05, 29.24, 30)
|
||||
* "geo:40.05,29.24;u=30" -> LocationData(40.05, 29.24, 30)
|
||||
* @return location data or null if geo uri is not valid
|
||||
*/
|
||||
fun MessageLocationContent.toLocationData(): LocationData? {
|
||||
|
@ -39,7 +39,7 @@ fun MessageLocationContent.toLocationData(): LocationData? {
|
|||
|
||||
/**
|
||||
* Creates location data from a geoUri String.
|
||||
* "geo:40.05,29.24;30" -> LocationData(40.05, 29.24, 30)
|
||||
* "geo:40.05,29.24;u=30" -> LocationData(40.05, 29.24, 30)
|
||||
* @return location data or null if geo uri is null or not valid
|
||||
*/
|
||||
fun String?.toLocationData(): LocationData? {
|
||||
|
|
|
@ -23,17 +23,21 @@ import android.os.Parcelable
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.services.VectorService
|
||||
import im.vector.app.features.location.live.GetLiveLocationShareSummaryUseCase
|
||||
import im.vector.app.features.notifications.NotificationUtils
|
||||
import im.vector.app.features.session.coroutineScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
|
||||
import timber.log.Timber
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
@ -49,6 +53,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||
@Inject lateinit var notificationUtils: NotificationUtils
|
||||
@Inject lateinit var locationTracker: LocationTracker
|
||||
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
||||
@Inject lateinit var getLiveLocationShareSummaryUseCase: GetLiveLocationShareSummaryUseCase
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
|
@ -56,37 +61,50 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||
* Keep track of a map between beacon event Id starting the live and RoomArgs.
|
||||
*/
|
||||
private val roomArgsMap = mutableMapOf<String, RoomArgs>()
|
||||
private val timers = mutableListOf<Timer>()
|
||||
var callback: Callback? = null
|
||||
private val jobs = mutableListOf<Job>()
|
||||
private var startInProgress = false
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Timber.i("### LocationSharingService.onCreate")
|
||||
Timber.i("onCreate")
|
||||
|
||||
initLocationTracking()
|
||||
}
|
||||
|
||||
private fun initLocationTracking() {
|
||||
// Start tracking location
|
||||
locationTracker.addCallback(this)
|
||||
locationTracker.start()
|
||||
|
||||
launchWithActiveSession { session ->
|
||||
val job = locationTracker.locations
|
||||
.onEach(this@LocationSharingService::onLocationUpdate)
|
||||
.launchIn(session.coroutineScope)
|
||||
jobs.add(job)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
startInProgress = true
|
||||
|
||||
val roomArgs = intent?.getParcelableExtra(EXTRA_ROOM_ARGS) as? RoomArgs
|
||||
|
||||
Timber.i("### LocationSharingService.onStartCommand. sessionId - roomId ${roomArgs?.sessionId} - ${roomArgs?.roomId}")
|
||||
Timber.i("onStartCommand. sessionId - roomId ${roomArgs?.sessionId} - ${roomArgs?.roomId}")
|
||||
|
||||
if (roomArgs != null) {
|
||||
// Show a sticky notification
|
||||
val notification = notificationUtils.buildLiveLocationSharingNotification()
|
||||
startForeground(roomArgs.roomId.hashCode(), notification)
|
||||
|
||||
// Schedule a timer to stop sharing
|
||||
scheduleTimer(roomArgs.roomId, roomArgs.durationMillis)
|
||||
|
||||
// Send beacon info state event
|
||||
launchInIO { session ->
|
||||
launchWithActiveSession { session ->
|
||||
sendStartingLiveBeaconInfo(session, roomArgs)
|
||||
}
|
||||
}
|
||||
|
||||
startInProgress = false
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
|
@ -100,7 +118,8 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||
?.let { result ->
|
||||
when (result) {
|
||||
is UpdateLiveLocationShareResult.Success -> {
|
||||
roomArgsMap[result.beaconEventId] = roomArgs
|
||||
addRoomArgs(result.beaconEventId, roomArgs)
|
||||
listenForLiveSummaryChanges(roomArgs.roomId, result.beaconEventId)
|
||||
locationTracker.requestLastKnownLocation()
|
||||
}
|
||||
is UpdateLiveLocationShareResult.Failure -> {
|
||||
|
@ -110,55 +129,19 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||
}
|
||||
}
|
||||
?: run {
|
||||
Timber.w("### LocationSharingService.sendStartingLiveBeaconInfo error, no received beacon info id")
|
||||
Timber.w("sendStartingLiveBeaconInfo error, no received beacon info id")
|
||||
tryToDestroyMe()
|
||||
}
|
||||
}
|
||||
|
||||
private fun scheduleTimer(roomId: String, durationMillis: Long) {
|
||||
Timer()
|
||||
.apply {
|
||||
schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
stopSharingLocation(roomId)
|
||||
timers.remove(this@apply)
|
||||
}
|
||||
}, durationMillis)
|
||||
}
|
||||
.also {
|
||||
timers.add(it)
|
||||
}
|
||||
private fun stopSharingLocation(beaconEventId: String) {
|
||||
Timber.i("stopSharingLocation for beacon $beaconEventId")
|
||||
removeRoomArgs(beaconEventId)
|
||||
tryToDestroyMe()
|
||||
}
|
||||
|
||||
fun stopSharingLocation(roomId: String) {
|
||||
Timber.i("### LocationSharingService.stopSharingLocation for $roomId")
|
||||
|
||||
launchInIO { session ->
|
||||
when (val result = sendStoppedBeaconInfo(session, roomId)) {
|
||||
is UpdateLiveLocationShareResult.Success -> {
|
||||
synchronized(roomArgsMap) {
|
||||
val beaconIds = roomArgsMap
|
||||
.filter { it.value.roomId == roomId }
|
||||
.map { it.key }
|
||||
beaconIds.forEach { roomArgsMap.remove(it) }
|
||||
|
||||
tryToDestroyMe()
|
||||
}
|
||||
}
|
||||
is UpdateLiveLocationShareResult.Failure -> callback?.onServiceError(result.error)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendStoppedBeaconInfo(session: Session, roomId: String): UpdateLiveLocationShareResult? {
|
||||
return session.getRoom(roomId)
|
||||
?.locationSharingService()
|
||||
?.stopLiveLocationShare()
|
||||
}
|
||||
|
||||
override fun onLocationUpdate(locationData: LocationData) {
|
||||
Timber.i("### LocationSharingService.onLocationUpdate. Uncertainty: ${locationData.uncertainty}")
|
||||
private fun onLocationUpdate(locationData: LocationData) {
|
||||
Timber.i("onLocationUpdate. Uncertainty: ${locationData.uncertainty}")
|
||||
|
||||
// Emit location update to all rooms in which live location sharing is active
|
||||
roomArgsMap.toMap().forEach { item ->
|
||||
|
@ -171,7 +154,7 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||
beaconInfoEventId: String,
|
||||
locationData: LocationData
|
||||
) {
|
||||
launchInIO { session ->
|
||||
launchWithActiveSession { session ->
|
||||
session.getRoom(roomId)
|
||||
?.locationSharingService()
|
||||
?.sendLiveLocation(
|
||||
|
@ -189,31 +172,46 @@ class LocationSharingService : VectorService(), LocationTracker.Callback {
|
|||
}
|
||||
|
||||
private fun tryToDestroyMe() {
|
||||
if (roomArgsMap.isEmpty()) {
|
||||
Timber.i("### LocationSharingService. Destroying self, time is up for all rooms")
|
||||
destroyMe()
|
||||
if (startInProgress.not() && roomArgsMap.isEmpty()) {
|
||||
Timber.i("Destroying self, time is up for all rooms")
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
private fun destroyMe() {
|
||||
locationTracker.removeCallback(this)
|
||||
timers.forEach { it.cancel() }
|
||||
timers.clear()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
Timber.i("### LocationSharingService.onDestroy")
|
||||
destroyMe()
|
||||
Timber.i("onDestroy")
|
||||
jobs.forEach { it.cancel() }
|
||||
jobs.clear()
|
||||
locationTracker.removeCallback(this)
|
||||
}
|
||||
|
||||
private fun launchInIO(block: suspend CoroutineScope.(Session) -> Unit) =
|
||||
private fun addRoomArgs(beaconEventId: String, roomArgs: RoomArgs) {
|
||||
Timber.i("adding roomArgs for beaconEventId: $beaconEventId")
|
||||
roomArgsMap[beaconEventId] = roomArgs
|
||||
}
|
||||
|
||||
private fun removeRoomArgs(beaconEventId: String) {
|
||||
Timber.i("removing roomArgs for beaconEventId: $beaconEventId")
|
||||
roomArgsMap.remove(beaconEventId)
|
||||
}
|
||||
|
||||
private fun listenForLiveSummaryChanges(roomId: String, beaconEventId: String) {
|
||||
launchWithActiveSession { session ->
|
||||
val job = getLiveLocationShareSummaryUseCase.execute(roomId, beaconEventId)
|
||||
.distinctUntilChangedBy { it.isActive }
|
||||
.filter { it.isActive == false }
|
||||
.onEach { stopSharingLocation(beaconEventId) }
|
||||
.launchIn(session.coroutineScope)
|
||||
jobs.add(job)
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchWithActiveSession(block: suspend CoroutineScope.(Session) -> Unit) =
|
||||
activeSessionHolder
|
||||
.getSafeActiveSession()
|
||||
?.let { session ->
|
||||
session.coroutineScope.launch(
|
||||
context = session.coroutineDispatchers.io,
|
||||
block = { block(session) }
|
||||
)
|
||||
}
|
||||
|
|
|
@ -22,7 +22,9 @@ import android.content.Intent
|
|||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class LocationSharingServiceConnection @Inject constructor(
|
||||
private val context: Context
|
||||
) : ServiceConnection, LocationSharingService.Callback {
|
||||
|
@ -33,12 +35,12 @@ class LocationSharingServiceConnection @Inject constructor(
|
|||
fun onLocationServiceError(error: Throwable)
|
||||
}
|
||||
|
||||
private var callback: Callback? = null
|
||||
private val callbacks = mutableSetOf<Callback>()
|
||||
private var isBound = false
|
||||
private var locationSharingService: LocationSharingService? = null
|
||||
|
||||
fun bind(callback: Callback) {
|
||||
this.callback = callback
|
||||
addCallback(callback)
|
||||
|
||||
if (isBound) {
|
||||
callback.onLocationServiceRunning()
|
||||
|
@ -49,12 +51,8 @@ class LocationSharingServiceConnection @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
fun unbind() {
|
||||
callback = null
|
||||
}
|
||||
|
||||
fun stopLiveLocationSharing(roomId: String) {
|
||||
locationSharingService?.stopSharingLocation(roomId)
|
||||
fun unbind(callback: Callback) {
|
||||
removeCallback(callback)
|
||||
}
|
||||
|
||||
override fun onServiceConnected(className: ComponentName, binder: IBinder) {
|
||||
|
@ -62,17 +60,33 @@ class LocationSharingServiceConnection @Inject constructor(
|
|||
it.callback = this
|
||||
}
|
||||
isBound = true
|
||||
callback?.onLocationServiceRunning()
|
||||
onCallbackActionNoArg(Callback::onLocationServiceRunning)
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(className: ComponentName) {
|
||||
isBound = false
|
||||
locationSharingService?.callback = null
|
||||
locationSharingService = null
|
||||
callback?.onLocationServiceStopped()
|
||||
onCallbackActionNoArg(Callback::onLocationServiceStopped)
|
||||
}
|
||||
|
||||
override fun onServiceError(error: Throwable) {
|
||||
callback?.onLocationServiceError(error)
|
||||
forwardErrorToCallbacks(error)
|
||||
}
|
||||
|
||||
private fun addCallback(callback: Callback) {
|
||||
callbacks.add(callback)
|
||||
}
|
||||
|
||||
private fun removeCallback(callback: Callback) {
|
||||
callbacks.remove(callback)
|
||||
}
|
||||
|
||||
private fun onCallbackActionNoArg(action: Callback.() -> Unit) {
|
||||
callbacks.toList().forEach(action)
|
||||
}
|
||||
|
||||
private fun forwardErrorToCallbacks(error: Throwable) {
|
||||
callbacks.toList().forEach { it.onLocationServiceError(error) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.session.Session
|
|||
import org.matrix.android.sdk.api.session.getRoom
|
||||
import org.matrix.android.sdk.api.session.getUser
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Sampling period to compare target location and user location.
|
||||
|
@ -65,13 +66,20 @@ class LocationSharingViewModel @AssistedInject constructor(
|
|||
companion object : MavericksViewModelFactory<LocationSharingViewModel, LocationSharingViewState> by hiltMavericksViewModelFactory()
|
||||
|
||||
init {
|
||||
locationTracker.addCallback(this)
|
||||
locationTracker.start()
|
||||
initLocationTracking()
|
||||
setUserItem()
|
||||
updatePin()
|
||||
compareTargetAndUserLocation()
|
||||
}
|
||||
|
||||
private fun initLocationTracking() {
|
||||
locationTracker.addCallback(this)
|
||||
locationTracker.locations
|
||||
.onEach(::onLocationUpdate)
|
||||
.launchIn(viewModelScope)
|
||||
locationTracker.start()
|
||||
}
|
||||
|
||||
private fun setUserItem() {
|
||||
setState { copy(userItem = session.getUser(session.myUserId)?.toMatrixItem()) }
|
||||
}
|
||||
|
@ -172,7 +180,8 @@ class LocationSharingViewModel @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override fun onLocationUpdate(locationData: LocationData) {
|
||||
private fun onLocationUpdate(locationData: LocationData) {
|
||||
Timber.d("onLocationUpdate()")
|
||||
setState {
|
||||
copy(lastKnownUserLocation = locationData)
|
||||
}
|
||||
|
|
|
@ -25,28 +25,27 @@ import androidx.annotation.VisibleForTesting
|
|||
import androidx.core.content.getSystemService
|
||||
import androidx.core.location.LocationListenerCompat
|
||||
import im.vector.app.BuildConfig
|
||||
import im.vector.app.core.utils.Debouncer
|
||||
import im.vector.app.core.utils.createBackgroundHandler
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.features.session.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
private const val BKG_HANDLER_NAME = "LocationTracker.BKG_HANDLER_NAME"
|
||||
private const val LOCATION_DEBOUNCE_ID = "LocationTracker.LOCATION_DEBOUNCE_ID"
|
||||
|
||||
@Singleton
|
||||
class LocationTracker @Inject constructor(
|
||||
context: Context
|
||||
context: Context,
|
||||
private val activeSessionHolder: ActiveSessionHolder
|
||||
) : LocationListenerCompat {
|
||||
|
||||
private val locationManager = context.getSystemService<LocationManager>()
|
||||
|
||||
interface Callback {
|
||||
/**
|
||||
* Called on every location update.
|
||||
*/
|
||||
fun onLocationUpdate(locationData: LocationData)
|
||||
|
||||
/**
|
||||
* Called when no location provider is available to request location updates.
|
||||
*/
|
||||
|
@ -62,9 +61,16 @@ class LocationTracker @Inject constructor(
|
|||
@VisibleForTesting
|
||||
var hasLocationFromGPSProvider = false
|
||||
|
||||
private var lastLocation: LocationData? = null
|
||||
private val _locations = MutableSharedFlow<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])
|
||||
fun start() {
|
||||
|
@ -119,33 +125,35 @@ class LocationTracker @Inject constructor(
|
|||
}
|
||||
|
||||
@RequiresPermission(anyOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION])
|
||||
@VisibleForTesting
|
||||
fun stop() {
|
||||
Timber.d("stop()")
|
||||
locationManager?.removeUpdates(this)
|
||||
synchronized(this) {
|
||||
callbacks.clear()
|
||||
}
|
||||
debouncer.cancelAll()
|
||||
callbacks.clear()
|
||||
hasLocationFromGPSProvider = false
|
||||
hasLocationFromFusedProvider = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the last known location. It will be given async through Callback.
|
||||
* Please ensure adding a callback to receive the value.
|
||||
* Request the last known location. It will be given async through corresponding flow.
|
||||
* Please ensure collecting the flow before calling this method.
|
||||
*/
|
||||
fun requestLastKnownLocation() {
|
||||
lastLocation?.let { locationData -> onLocationUpdate(locationData) }
|
||||
Timber.d("requestLastKnownLocation")
|
||||
activeSessionHolder.getSafeActiveSession()?.coroutineScope?.launch {
|
||||
_locations.replayCache.firstOrNull()?.let {
|
||||
Timber.d("emitting last location from cache")
|
||||
_locations.emit(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addCallback(callback: Callback) {
|
||||
if (!callbacks.contains(callback)) {
|
||||
callbacks.add(callback)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeCallback(callback: Callback) {
|
||||
callbacks.remove(callback)
|
||||
if (callbacks.size == 0) {
|
||||
|
@ -183,21 +191,19 @@ class LocationTracker @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
debouncer.debounce(LOCATION_DEBOUNCE_ID, MIN_TIME_TO_UPDATE_LOCATION_MILLIS) {
|
||||
notifyLocation(location)
|
||||
}
|
||||
notifyLocation(location)
|
||||
}
|
||||
|
||||
private fun notifyLocation(location: Location) {
|
||||
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
|
||||
Timber.d("notify location: $location")
|
||||
} else {
|
||||
Timber.d("notify location: ${location.provider}")
|
||||
}
|
||||
activeSessionHolder.getSafeActiveSession()?.coroutineScope?.launch {
|
||||
if (BuildConfig.LOW_PRIVACY_LOG_ENABLE) {
|
||||
Timber.d("notify location: $location")
|
||||
} else {
|
||||
Timber.d("notify location: ${location.provider}")
|
||||
}
|
||||
|
||||
val locationData = location.toLocationData()
|
||||
lastLocation = locationData
|
||||
onLocationUpdate(locationData)
|
||||
_locations.emit(location)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProviderDisabled(provider: String) {
|
||||
|
@ -215,9 +221,8 @@ class LocationTracker @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun onNoLocationProviderAvailable() {
|
||||
callbacks.forEach {
|
||||
callbacks.toList().forEach {
|
||||
try {
|
||||
it.onNoLocationProviderAvailable()
|
||||
} catch (error: Exception) {
|
||||
|
@ -226,17 +231,6 @@ class LocationTracker @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun onLocationUpdate(locationData: LocationData) {
|
||||
callbacks.forEach {
|
||||
try {
|
||||
it.onLocationUpdate(locationData)
|
||||
} catch (error: Exception) {
|
||||
Timber.e(error, "error in onLocationUpdate callback $it")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Location.toLocationData(): LocationData {
|
||||
return LocationData(latitude, longitude, accuracy.toDouble())
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -24,13 +24,17 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
|||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.features.location.LocationSharingServiceConnection
|
||||
import im.vector.app.features.location.live.StopLiveLocationShareUseCase
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
|
||||
|
||||
class LocationLiveMapViewModel @AssistedInject constructor(
|
||||
@Assisted private val initialState: LocationLiveMapViewState,
|
||||
getListOfUserLiveLocationUseCase: GetListOfUserLiveLocationUseCase,
|
||||
private val locationSharingServiceConnection: LocationSharingServiceConnection,
|
||||
private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase,
|
||||
) : VectorViewModel<LocationLiveMapViewState, LocationLiveMapAction, LocationLiveMapViewEvents>(initialState), LocationSharingServiceConnection.Callback {
|
||||
|
||||
@AssistedFactory
|
||||
|
@ -47,6 +51,11 @@ class LocationLiveMapViewModel @AssistedInject constructor(
|
|||
locationSharingServiceConnection.bind(this)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
locationSharingServiceConnection.unbind(this)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
override fun handle(action: LocationLiveMapAction) {
|
||||
when (action) {
|
||||
is LocationLiveMapAction.AddMapSymbol -> handleAddMapSymbol(action)
|
||||
|
@ -70,7 +79,12 @@ class LocationLiveMapViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun handleStopSharing() {
|
||||
locationSharingServiceConnection.stopLiveLocationSharing(initialState.roomId)
|
||||
viewModelScope.launch {
|
||||
val result = stopLiveLocationShareUseCase.execute(initialState.roomId)
|
||||
if (result is UpdateLiveLocationShareResult.Failure) {
|
||||
_viewEvents.post(LocationLiveMapViewEvents.Error(result.error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLocationServiceRunning() {
|
||||
|
|
|
@ -16,12 +16,11 @@
|
|||
|
||||
package im.vector.app.features.poll
|
||||
|
||||
sealed interface PollState {
|
||||
object Sending : PollState
|
||||
object Ready : PollState
|
||||
data class Voted(val votes: Int) : PollState
|
||||
object Undisclosed : PollState
|
||||
object Ended : PollState
|
||||
import im.vector.app.features.home.room.detail.timeline.item.PollOptionViewState
|
||||
|
||||
fun isVotable() = this !is Sending && this !is Ended
|
||||
}
|
||||
data class PollViewState(
|
||||
val question: String,
|
||||
val totalVotes: String,
|
||||
val canVote: Boolean,
|
||||
val optionViewStates: List<PollOptionViewState>?,
|
||||
)
|
|
@ -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>
|
|
@ -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() ?: ""
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
|
@ -28,19 +28,26 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageLocationCont
|
|||
class LocationDataTest {
|
||||
@Test
|
||||
fun validCases() {
|
||||
parseGeo("geo:12.34,56.78;13.56") shouldBeEqualTo
|
||||
parseGeo("geo:12.34,56.78;u=13.56") shouldBeEqualTo
|
||||
LocationData(latitude = 12.34, longitude = 56.78, uncertainty = 13.56)
|
||||
parseGeo("geo:12.34,56.78") shouldBeEqualTo
|
||||
LocationData(latitude = 12.34, longitude = 56.78, uncertainty = null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun lenientCases() {
|
||||
// Error is ignored in case of invalid uncertainty
|
||||
parseGeo("geo:12.34,56.78;13.5z6") shouldBeEqualTo
|
||||
parseGeo("geo:12.34,56.78;u=13.5z6") shouldBeEqualTo
|
||||
LocationData(latitude = 12.34, longitude = 56.78, uncertainty = null)
|
||||
parseGeo("geo:12.34,56.78;13. 56") shouldBeEqualTo
|
||||
parseGeo("geo:12.34,56.78;u=13. 56") shouldBeEqualTo
|
||||
LocationData(latitude = 12.34, longitude = 56.78, uncertainty = null)
|
||||
// Space are ignored (trim)
|
||||
parseGeo("geo: 12.34,56.78;13.56") shouldBeEqualTo
|
||||
parseGeo("geo: 12.34,56.78;u=13.56") shouldBeEqualTo
|
||||
LocationData(latitude = 12.34, longitude = 56.78, uncertainty = 13.56)
|
||||
parseGeo("geo:12.34,56.78; 13.56") shouldBeEqualTo
|
||||
parseGeo("geo:12.34,56.78; u=13.56") shouldBeEqualTo
|
||||
LocationData(latitude = 12.34, longitude = 56.78, uncertainty = 13.56)
|
||||
// missing "u=" for uncertainty is ignored
|
||||
parseGeo("geo:12.34,56.78;13.56") shouldBeEqualTo
|
||||
LocationData(latitude = 12.34, longitude = 56.78, uncertainty = 13.56)
|
||||
}
|
||||
|
||||
|
@ -50,17 +57,17 @@ class LocationDataTest {
|
|||
parseGeo("geo").shouldBeNull()
|
||||
parseGeo("geo:").shouldBeNull()
|
||||
parseGeo("geo:12.34").shouldBeNull()
|
||||
parseGeo("geo:12.34;13.56").shouldBeNull()
|
||||
parseGeo("gea:12.34,56.78;13.56").shouldBeNull()
|
||||
parseGeo("geo:12.x34,56.78;13.56").shouldBeNull()
|
||||
parseGeo("geo:12.34,56.7y8;13.56").shouldBeNull()
|
||||
parseGeo("geo:12.34;u=13.56").shouldBeNull()
|
||||
parseGeo("gea:12.34,56.78;u=13.56").shouldBeNull()
|
||||
parseGeo("geo:12.x34,56.78;u=13.56").shouldBeNull()
|
||||
parseGeo("geo:12.34,56.7y8;u=13.56").shouldBeNull()
|
||||
// Spaces are not ignored if inside the numbers
|
||||
parseGeo("geo:12.3 4,56.78;13.56").shouldBeNull()
|
||||
parseGeo("geo:12.34,56.7 8;13.56").shouldBeNull()
|
||||
parseGeo("geo:12.3 4,56.78;u=13.56").shouldBeNull()
|
||||
parseGeo("geo:12.34,56.7 8;u=13.56").shouldBeNull()
|
||||
// Or in the protocol part
|
||||
parseGeo(" geo:12.34,56.78;13.56").shouldBeNull()
|
||||
parseGeo("ge o:12.34,56.78;13.56").shouldBeNull()
|
||||
parseGeo("geo :12.34,56.78;13.56").shouldBeNull()
|
||||
parseGeo(" geo:12.34,56.78;u=13.56").shouldBeNull()
|
||||
parseGeo("ge o:12.34,56.78;u=13.56").shouldBeNull()
|
||||
parseGeo("geo :12.34,56.78;u=13.56").shouldBeNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -77,7 +84,7 @@ class LocationDataTest {
|
|||
|
||||
@Test
|
||||
fun unstablePrefixTest() {
|
||||
val geoUri = "geo :12.34,56.78;13.56"
|
||||
val geoUri = "aGeoUri"
|
||||
|
||||
val contentWithUnstablePrefixes = MessageLocationContent(body = "", geoUri = "", unstableLocationInfo = LocationInfo(geoUri = geoUri))
|
||||
contentWithUnstablePrefixes.getBestLocationInfo()?.geoUri.shouldBeEqualTo(geoUri)
|
||||
|
|
|
@ -19,21 +19,21 @@ package im.vector.app.features.location
|
|||
import android.content.Context
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import im.vector.app.core.utils.Debouncer
|
||||
import im.vector.app.core.utils.createBackgroundHandler
|
||||
import im.vector.app.features.session.coroutineScope
|
||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||
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.test
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkConstructor
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkAll
|
||||
import io.mockk.verify
|
||||
import io.mockk.verifyOrder
|
||||
import kotlinx.coroutines.test.advanceTimeBy
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
|
@ -45,26 +45,18 @@ private const val AN_ACCURACY = 5.0f
|
|||
|
||||
class LocationTrackerTest {
|
||||
|
||||
private val fakeHandler = FakeHandler()
|
||||
private val fakeLocationManager = FakeLocationManager()
|
||||
private val fakeContext = FakeContext().also {
|
||||
it.givenService(Context.LOCATION_SERVICE, android.location.LocationManager::class.java, fakeLocationManager.instance)
|
||||
}
|
||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||
|
||||
private lateinit var locationTracker: LocationTracker
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockkConstructor(Debouncer::class)
|
||||
every { anyConstructed<Debouncer>().cancelAll() } just runs
|
||||
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)
|
||||
mockkStatic("im.vector.app.features.session.SessionCoroutineScopesKt")
|
||||
locationTracker = LocationTracker(fakeContext.instance, fakeActiveSessionHolder.instance)
|
||||
fakeLocationManager.givenRemoveUpdates(locationTracker)
|
||||
}
|
||||
|
||||
|
@ -139,13 +131,11 @@ class LocationTrackerTest {
|
|||
}
|
||||
|
||||
@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)
|
||||
mockAvailableProviders(providers)
|
||||
val callback = mockCallback()
|
||||
locationTracker.addCallback(callback)
|
||||
locationTracker.start()
|
||||
|
||||
val fusedLocation = mockLocation(
|
||||
provider = LocationManager.FUSED_PROVIDER,
|
||||
latitude = 1.0,
|
||||
|
@ -159,29 +149,31 @@ class LocationTrackerTest {
|
|||
val networkLocation = mockLocation(
|
||||
provider = LocationManager.NETWORK_PROVIDER
|
||||
)
|
||||
val resultUpdates = locationTracker.locations.test(this)
|
||||
|
||||
locationTracker.onLocationChanged(fusedLocation)
|
||||
locationTracker.onLocationChanged(gpsLocation)
|
||||
locationTracker.onLocationChanged(networkLocation)
|
||||
advanceTimeBy(MIN_TIME_TO_UPDATE_LOCATION_MILLIS + 1)
|
||||
|
||||
val expectedLocationData = LocationData(
|
||||
latitude = 1.0,
|
||||
longitude = 3.0,
|
||||
uncertainty = 4.0
|
||||
)
|
||||
verify { callback.onLocationUpdate(expectedLocationData) }
|
||||
verify { anyConstructed<Debouncer>().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, any()) }
|
||||
resultUpdates
|
||||
.assertValues(listOf(expectedLocationData))
|
||||
.finish()
|
||||
locationTracker.hasLocationFromFusedProvider shouldBeEqualTo true
|
||||
locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false
|
||||
}
|
||||
|
||||
@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)
|
||||
mockAvailableProviders(providers)
|
||||
val callback = mockCallback()
|
||||
locationTracker.addCallback(callback)
|
||||
locationTracker.start()
|
||||
|
||||
val gpsLocation = mockLocation(
|
||||
provider = LocationManager.GPS_PROVIDER,
|
||||
latitude = 1.0,
|
||||
|
@ -192,66 +184,75 @@ class LocationTrackerTest {
|
|||
val networkLocation = mockLocation(
|
||||
provider = LocationManager.NETWORK_PROVIDER
|
||||
)
|
||||
val resultUpdates = locationTracker.locations.test(this)
|
||||
|
||||
locationTracker.onLocationChanged(gpsLocation)
|
||||
locationTracker.onLocationChanged(networkLocation)
|
||||
advanceTimeBy(MIN_TIME_TO_UPDATE_LOCATION_MILLIS + 1)
|
||||
|
||||
val expectedLocationData = LocationData(
|
||||
latitude = 1.0,
|
||||
longitude = 3.0,
|
||||
uncertainty = 4.0
|
||||
)
|
||||
verify { callback.onLocationUpdate(expectedLocationData) }
|
||||
verify { anyConstructed<Debouncer>().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, any()) }
|
||||
resultUpdates
|
||||
.assertValues(listOf(expectedLocationData))
|
||||
.finish()
|
||||
locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false
|
||||
locationTracker.hasLocationFromGPSProvider shouldBeEqualTo true
|
||||
}
|
||||
|
||||
@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)
|
||||
mockAvailableProviders(providers)
|
||||
val callback = mockCallback()
|
||||
locationTracker.addCallback(callback)
|
||||
locationTracker.start()
|
||||
|
||||
val networkLocation = mockLocation(
|
||||
provider = LocationManager.NETWORK_PROVIDER,
|
||||
latitude = 1.0,
|
||||
longitude = 3.0,
|
||||
accuracy = 4f
|
||||
)
|
||||
val resultUpdates = locationTracker.locations.test(this)
|
||||
|
||||
locationTracker.onLocationChanged(networkLocation)
|
||||
advanceTimeBy(MIN_TIME_TO_UPDATE_LOCATION_MILLIS + 1)
|
||||
|
||||
val expectedLocationData = LocationData(
|
||||
latitude = 1.0,
|
||||
longitude = 3.0,
|
||||
uncertainty = 4.0
|
||||
)
|
||||
verify { callback.onLocationUpdate(expectedLocationData) }
|
||||
verify { anyConstructed<Debouncer>().debounce(any(), MIN_TIME_TO_UPDATE_LOCATION_MILLIS, any()) }
|
||||
resultUpdates
|
||||
.assertValues(listOf(expectedLocationData))
|
||||
.finish()
|
||||
locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false
|
||||
locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false
|
||||
}
|
||||
|
||||
@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)
|
||||
fakeLocationManager.givenActiveProviders(providers)
|
||||
val lastLocation = mockLocation(provider = LocationManager.GPS_PROVIDER)
|
||||
fakeLocationManager.givenLastLocationForProvider(provider = LocationManager.GPS_PROVIDER, location = lastLocation)
|
||||
fakeLocationManager.givenRequestUpdatesForProvider(provider = LocationManager.GPS_PROVIDER, listener = locationTracker)
|
||||
val callback = mockCallback()
|
||||
locationTracker.addCallback(callback)
|
||||
locationTracker.start()
|
||||
val resultUpdates = locationTracker.locations.test(this)
|
||||
|
||||
locationTracker.requestLastKnownLocation()
|
||||
advanceTimeBy(MIN_TIME_TO_UPDATE_LOCATION_MILLIS + 1)
|
||||
|
||||
val expectedLocationData = LocationData(
|
||||
latitude = A_LATITUDE,
|
||||
longitude = A_LONGITUDE,
|
||||
uncertainty = AN_ACCURACY.toDouble()
|
||||
)
|
||||
verify { callback.onLocationUpdate(expectedLocationData) }
|
||||
resultUpdates
|
||||
.assertValues(listOf(expectedLocationData))
|
||||
.finish()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -259,7 +260,6 @@ class LocationTrackerTest {
|
|||
locationTracker.stop()
|
||||
|
||||
verify { fakeLocationManager.instance.removeUpdates(locationTracker) }
|
||||
verify { anyConstructed<Debouncer>().cancelAll() }
|
||||
locationTracker.callbacks.isEmpty() shouldBeEqualTo true
|
||||
locationTracker.hasLocationFromGPSProvider shouldBeEqualTo false
|
||||
locationTracker.hasLocationFromFusedProvider shouldBeEqualTo false
|
||||
|
@ -276,7 +276,6 @@ class LocationTrackerTest {
|
|||
private fun mockCallback(): LocationTracker.Callback {
|
||||
return mockk<LocationTracker.Callback>().also {
|
||||
every { it.onNoLocationProviderAvailable() } just runs
|
||||
every { it.onLocationUpdate(any()) } just runs
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,21 +16,16 @@
|
|||
|
||||
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.test.fakes.FakeSession
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.impl.annotations.OverrideMockKs
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class CompareLocationsUseCaseTest {
|
||||
|
||||
@get:Rule
|
||||
val mvRxTestRule = MvRxTestRule()
|
||||
|
||||
private val session = FakeSession()
|
||||
|
||||
@OverrideMockKs
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -16,52 +16,48 @@
|
|||
|
||||
package im.vector.app.features.location.live.map
|
||||
|
||||
import androidx.lifecycle.asFlow
|
||||
import com.airbnb.mvrx.test.MvRxTestRule
|
||||
import im.vector.app.features.location.LocationData
|
||||
import im.vector.app.test.fakes.FakeFlowLiveDataConversions
|
||||
import im.vector.app.test.fakes.FakeSession
|
||||
import im.vector.app.test.fakes.givenAsFlowReturns
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.unmockkAll
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.internal.assertEquals
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
private const val A_ROOM_ID = "room_id"
|
||||
|
||||
class GetListOfUserLiveLocationUseCaseTest {
|
||||
|
||||
@get:Rule
|
||||
val mvRxTestRule = MvRxTestRule()
|
||||
|
||||
private val fakeSession = FakeSession()
|
||||
|
||||
private val viewStateMapper = mockk<UserLiveLocationViewStateMapper>()
|
||||
private val fakeFlowLiveDataConversions = FakeFlowLiveDataConversions()
|
||||
|
||||
private val getListOfUserLiveLocationUseCase = GetListOfUserLiveLocationUseCase(fakeSession, viewStateMapper)
|
||||
private val getListOfUserLiveLocationUseCase = GetListOfUserLiveLocationUseCase(
|
||||
session = fakeSession,
|
||||
userLiveLocationViewStateMapper = viewStateMapper
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
mockkStatic("androidx.lifecycle.FlowLiveDataConversions")
|
||||
fakeFlowLiveDataConversions.setup()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkStatic("androidx.lifecycle.FlowLiveDataConversions")
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a room id then the correct flow of view states list is collected`() = runTest {
|
||||
val roomId = "roomId"
|
||||
|
||||
val summary1 = LiveLocationShareAggregatedSummary(
|
||||
userId = "userId1",
|
||||
isActive = true,
|
||||
|
@ -81,12 +77,11 @@ class GetListOfUserLiveLocationUseCaseTest {
|
|||
lastLocationDataContent = MessageBeaconLocationDataContent()
|
||||
)
|
||||
val summaries = listOf(summary1, summary2, summary3)
|
||||
val liveData = fakeSession.roomService()
|
||||
.getRoom(roomId)
|
||||
fakeSession.roomService()
|
||||
.getRoom(A_ROOM_ID)
|
||||
.locationSharingService()
|
||||
.givenRunningLiveLocationShareSummaries(summaries)
|
||||
|
||||
every { liveData.asFlow() } returns flowOf(summaries)
|
||||
.givenRunningLiveLocationShareSummariesReturns(summaries)
|
||||
.givenAsFlowReturns(summaries)
|
||||
|
||||
val viewState1 = UserLiveLocationViewState(
|
||||
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(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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,39 +18,47 @@ package im.vector.app.features.location.live.map
|
|||
|
||||
import com.airbnb.mvrx.test.MvRxTestRule
|
||||
import im.vector.app.features.location.LocationData
|
||||
import im.vector.app.features.location.LocationSharingServiceConnection
|
||||
import im.vector.app.features.location.live.StopLiveLocationShareUseCase
|
||||
import im.vector.app.test.fakes.FakeLocationSharingServiceConnection
|
||||
import im.vector.app.test.test
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import io.mockk.unmockkAll
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
private const val A_ROOM_ID = "room_id"
|
||||
|
||||
class LocationLiveMapViewModelTest {
|
||||
|
||||
@get:Rule
|
||||
val mvrxTestRule = MvRxTestRule()
|
||||
val mvRxTestRule = MvRxTestRule(testDispatcher = UnconfinedTestDispatcher())
|
||||
|
||||
private val fakeRoomId = ""
|
||||
|
||||
private val args = LocationLiveMapViewArgs(roomId = fakeRoomId)
|
||||
private val args = LocationLiveMapViewArgs(roomId = A_ROOM_ID)
|
||||
|
||||
private val getListOfUserLiveLocationUseCase = mockk<GetListOfUserLiveLocationUseCase>()
|
||||
private val locationServiceConnection = mockk<LocationSharingServiceConnection>()
|
||||
private val locationServiceConnection = FakeLocationSharingServiceConnection()
|
||||
private val stopLiveLocationShareUseCase = mockk<StopLiveLocationShareUseCase>()
|
||||
|
||||
private fun createViewModel(): LocationLiveMapViewModel {
|
||||
return LocationLiveMapViewModel(
|
||||
LocationLiveMapViewState(args),
|
||||
getListOfUserLiveLocationUseCase,
|
||||
locationServiceConnection
|
||||
locationServiceConnection.instance,
|
||||
stopLiveLocationShareUseCase
|
||||
)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkAll()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given the viewModel has been initialized then viewState contains user locations list`() = runTest {
|
||||
val userLocations = listOf(
|
||||
|
@ -63,8 +71,8 @@ class LocationLiveMapViewModelTest {
|
|||
showStopSharingButton = false
|
||||
)
|
||||
)
|
||||
every { locationServiceConnection.bind(any()) } just runs
|
||||
every { getListOfUserLiveLocationUseCase.execute(fakeRoomId) } returns flowOf(userLocations)
|
||||
locationServiceConnection.givenBind()
|
||||
every { getListOfUserLiveLocationUseCase.execute(A_ROOM_ID) } returns flowOf(userLocations)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
viewModel
|
||||
|
@ -76,6 +84,6 @@ class LocationLiveMapViewModelTest {
|
|||
)
|
||||
.finish()
|
||||
|
||||
verify { locationServiceConnection.bind(viewModel) }
|
||||
locationServiceConnection.verifyBind(viewModel)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ private const val A_LOCATION_TIMESTAMP = 122L
|
|||
private const val A_LATITUDE = 40.05
|
||||
private const val A_LONGITUDE = 29.24
|
||||
private const val A_UNCERTAINTY = 30.0
|
||||
private const val A_GEO_URI = "geo:$A_LATITUDE,$A_LONGITUDE;$A_UNCERTAINTY"
|
||||
private const val A_GEO_URI = "geo:$A_LATITUDE,$A_LONGITUDE;u=$A_UNCERTAINTY"
|
||||
|
||||
class UserLiveLocationViewStateMapperTest {
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ package im.vector.app.features.media.domain.usecase
|
|||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.airbnb.mvrx.test.MvRxTestRule
|
||||
import im.vector.app.core.intent.getMimeTypeFromUri
|
||||
import im.vector.app.core.utils.saveMedia
|
||||
import im.vector.app.features.notifications.NotificationUtils
|
||||
|
@ -42,14 +41,10 @@ import io.mockk.verifyAll
|
|||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class DownloadMediaUseCaseTest {
|
||||
|
||||
@get:Rule
|
||||
val mvRxTestRule = MvRxTestRule()
|
||||
|
||||
@MockK
|
||||
lateinit var appContext: Context
|
||||
|
||||
|
|
|
@ -16,13 +16,15 @@
|
|||
|
||||
package im.vector.app.test
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
|
||||
|
||||
private val testDispatcher = UnconfinedTestDispatcher()
|
||||
|
||||
internal val testCoroutineDispatchers = MatrixCoroutineDispatchers(
|
||||
io = Dispatchers.Main,
|
||||
computation = Dispatchers.Main,
|
||||
main = Dispatchers.Main,
|
||||
crypto = Dispatchers.Main,
|
||||
dmVerif = Dispatchers.Main
|
||||
io = testDispatcher,
|
||||
computation = testDispatcher,
|
||||
main = testDispatcher,
|
||||
crypto = testDispatcher,
|
||||
dmVerif = testDispatcher
|
||||
)
|
||||
|
|
|
@ -23,10 +23,11 @@ import io.mockk.mockk
|
|||
import org.matrix.android.sdk.api.session.Session
|
||||
|
||||
class FakeActiveSessionHolder(
|
||||
private val fakeSession: FakeSession = FakeSession()
|
||||
val fakeSession: FakeSession = FakeSession()
|
||||
) {
|
||||
val instance = mockk<ActiveSessionHolder> {
|
||||
every { getActiveSession() } returns fakeSession
|
||||
every { getSafeActiveSession() } returns fakeSession
|
||||
}
|
||||
|
||||
fun expectSetsActiveSession(session: Session) {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -18,17 +18,34 @@ package im.vector.app.test.fakes
|
|||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.matrix.android.sdk.api.session.room.location.LocationSharingService
|
||||
import org.matrix.android.sdk.api.session.room.location.UpdateLiveLocationShareResult
|
||||
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||
import org.matrix.android.sdk.api.util.Optional
|
||||
|
||||
class FakeLocationSharingService : LocationSharingService by mockk() {
|
||||
|
||||
fun givenRunningLiveLocationShareSummaries(summaries: List<LiveLocationShareAggregatedSummary>):
|
||||
LiveData<List<LiveLocationShareAggregatedSummary>> {
|
||||
fun givenRunningLiveLocationShareSummariesReturns(
|
||||
summaries: List<LiveLocationShareAggregatedSummary>
|
||||
): LiveData<List<LiveLocationShareAggregatedSummary>> {
|
||||
return MutableLiveData(summaries).also {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
}
|
|
@ -27,6 +27,10 @@ class FakeStringProvider {
|
|||
every { instance.getString(any()) } answers {
|
||||
"test-${args[0]}"
|
||||
}
|
||||
|
||||
every { instance.getQuantityString(any(), any(), any()) } answers {
|
||||
"test-${args[0]}-${args[1]}"
|
||||
}
|
||||
}
|
||||
|
||||
fun given(id: Int, result: String) {
|
||||
|
|
Loading…
Reference in New Issue