Feature : share live location (#6741)
* First live location sharing sending implementation * Simplify logic around canStop sharing * Add some debug logs around LiveLocationSharingService * Add LiveLocationException * Expose beaconId to identify the current share * Throttle live location instead of debouncing * Keep sync alive when sharing live location * Improve LiveLocation sharing * Show LiveLocationDisclaimer * Read minDistanceUpdate in LiveLocationSharingService * Set minDistanceUpdate in AdvancedSettings * Display banner in room when sharing live location * Fix tests around LiveLocationSharing * Ensure shares are properly restarted/stopped when app is re-launched * Ensure LLS data is cleared when session is removed * Update and fix LLS tests * Handle Start LLS in ui * Add check LLS permissions * Remove hardcoded strings * Fix quality and format * Create DeviceLocationProvider so we can share location data between sources (presenter/live location service) * Update screenshots * Fix warning * Do not try to stop if it was not sharing * Revert "Create DeviceLocationProvider so we can share location data between sources (presenter/live location service)" This reverts commit ba12bd968e82941cc231bdbb449310b24c97c5b8. * Tweak location provider config values * Address PR review remarks * Fix ktlint * Update screenshots * Fix some tests after merging develop * Adjust TimelineItemLocationView ui to match figma * Update screenshots * Documentation and cleanup * Remove temporary resource --------- Co-authored-by: ElementBot <android@element.io> Co-authored-by: Benoit Marty <benoit@matrix.org> Co-authored-by: Benoit Marty <benoitm@matrix.org>
This commit is contained in:
parent
0c657c258a
commit
e49e183178
145 changed files with 2913 additions and 278 deletions
|
|
@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.api.room.NotJoinedRoom
|
|||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||
import io.element.android.libraries.matrix.api.room.location.BeaconInfoUpdate
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
||||
|
|
@ -67,6 +68,7 @@ interface MatrixClient {
|
|||
val sessionCoroutineScope: CoroutineScope
|
||||
val ignoredUsersFlow: StateFlow<ImmutableList<UserId>>
|
||||
val roomMembershipObserver: RoomMembershipObserver
|
||||
val ownBeaconInfoUpdates: Flow<BeaconInfoUpdate>
|
||||
suspend fun getJoinedRoom(roomId: RoomId): JoinedRoom?
|
||||
suspend fun getRoom(roomId: RoomId): BaseRoom?
|
||||
suspend fun findDM(userId: UserId): Result<RoomId?>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.room.location
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
||||
typealias BeaconId = EventId
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.room.location
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
data class BeaconInfoUpdate(
|
||||
val roomId: RoomId,
|
||||
val beaconId: BeaconId,
|
||||
val isLive: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.api.room.location
|
||||
|
||||
sealed class LiveLocationException(message: String?) : Exception(message) {
|
||||
class NotLive : LiveLocationException("The beacon event has expired.")
|
||||
class Network : LiveLocationException("Network error")
|
||||
class Other(val exception: Exception) : LiveLocationException(exception.message)
|
||||
}
|
||||
|
|
@ -21,6 +21,8 @@ data class LiveLocationShare(
|
|||
val startTimestamp: Long,
|
||||
/** The timestamp when location sharing ends, in milliseconds. */
|
||||
val endTimestamp: Long,
|
||||
/** The event id from the beacon info. */
|
||||
val beaconId: BeaconId
|
||||
)
|
||||
|
||||
data class LastLocation(
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ import io.element.android.libraries.matrix.impl.room.RustRoomFactory
|
|||
import io.element.android.libraries.matrix.impl.room.TimelineEventFilterFactory
|
||||
import io.element.android.libraries.matrix.impl.room.history.map
|
||||
import io.element.android.libraries.matrix.impl.room.join.map
|
||||
import io.element.android.libraries.matrix.impl.room.location.map
|
||||
import io.element.android.libraries.matrix.impl.room.preview.RoomPreviewInfoMapper
|
||||
import io.element.android.libraries.matrix.impl.roomdirectory.RustRoomDirectoryService
|
||||
import io.element.android.libraries.matrix.impl.roomdirectory.map
|
||||
|
|
@ -113,6 +114,8 @@ import kotlinx.coroutines.withContext
|
|||
import kotlinx.coroutines.withTimeout
|
||||
import org.matrix.rustcomponents.sdk.AuthData
|
||||
import org.matrix.rustcomponents.sdk.AuthDataPasswordDetails
|
||||
import org.matrix.rustcomponents.sdk.BeaconInfoListener
|
||||
import org.matrix.rustcomponents.sdk.BeaconInfoUpdate
|
||||
import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.ClientException
|
||||
import org.matrix.rustcomponents.sdk.IgnoredUsersListener
|
||||
|
|
@ -207,6 +210,15 @@ class RustMatrixClient(
|
|||
analyticsService = analyticsService,
|
||||
)
|
||||
|
||||
override val ownBeaconInfoUpdates = mxCallbackFlow {
|
||||
val listener = object : BeaconInfoListener {
|
||||
override fun onUpdate(update: BeaconInfoUpdate) {
|
||||
trySend(update.map())
|
||||
}
|
||||
}
|
||||
innerClient.subscribeToOwnBeaconInfoUpdates(listener)
|
||||
}
|
||||
|
||||
override val sessionVerificationService = RustSessionVerificationService(
|
||||
client = innerClient,
|
||||
isSyncServiceReady = syncService.syncState.map { it == SyncState.Running },
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import io.element.android.libraries.matrix.impl.room.history.map
|
|||
import io.element.android.libraries.matrix.impl.room.join.map
|
||||
import io.element.android.libraries.matrix.impl.room.knock.RustKnockRequest
|
||||
import io.element.android.libraries.matrix.impl.room.location.liveLocationSharesFlow
|
||||
import io.element.android.libraries.matrix.impl.room.location.map
|
||||
import io.element.android.libraries.matrix.impl.room.location.timedByExpiry
|
||||
import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher
|
||||
import io.element.android.libraries.matrix.impl.room.threads.RustThreadsListService
|
||||
|
|
@ -72,6 +73,7 @@ import kotlinx.coroutines.withContext
|
|||
import org.matrix.rustcomponents.sdk.DateDividerMode
|
||||
import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener
|
||||
import org.matrix.rustcomponents.sdk.KnockRequestsListener
|
||||
import org.matrix.rustcomponents.sdk.LiveLocationException
|
||||
import org.matrix.rustcomponents.sdk.RoomMessageEventMessageType
|
||||
import org.matrix.rustcomponents.sdk.RoomSendQueueUpdate
|
||||
import org.matrix.rustcomponents.sdk.SendQueueListener
|
||||
|
|
@ -525,12 +527,22 @@ class JoinedRustRoom(
|
|||
override suspend fun stopLiveLocationShare(): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerRoom.stopLiveLocationShare()
|
||||
}.mapFailure { throwable ->
|
||||
when (throwable) {
|
||||
is LiveLocationException -> throwable.map()
|
||||
else -> throwable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendLiveLocation(geoUri: String): Result<Unit> = withContext(roomDispatcher) {
|
||||
runCatchingExceptions {
|
||||
innerRoom.sendLiveLocation(geoUri)
|
||||
}.mapFailure { throwable ->
|
||||
when (throwable) {
|
||||
is LiveLocationException -> throwable.map()
|
||||
else -> throwable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.room.location
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.location.BeaconInfoUpdate
|
||||
import org.matrix.rustcomponents.sdk.BeaconInfoUpdate as RustBeaconInfoUpdate
|
||||
|
||||
fun RustBeaconInfoUpdate.map(): BeaconInfoUpdate {
|
||||
return BeaconInfoUpdate(
|
||||
roomId = RoomId(roomId),
|
||||
beaconId = EventId(eventId),
|
||||
isLive = live
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.impl.room.location
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.location.LiveLocationException
|
||||
import org.matrix.rustcomponents.sdk.LiveLocationException as RustLiveLocationException
|
||||
|
||||
fun RustLiveLocationException.map(): LiveLocationException {
|
||||
return when (this) {
|
||||
is RustLiveLocationException.Network -> LiveLocationException.Network()
|
||||
is RustLiveLocationException.NotLive -> LiveLocationException.NotLive()
|
||||
else -> LiveLocationException.Other(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
package io.element.android.libraries.matrix.impl.room.location
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.location.LastLocation
|
||||
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
|
||||
|
|
@ -41,9 +42,9 @@ fun RoomInterface.liveLocationSharesFlow(): Flow<List<LiveLocationShare>> {
|
|||
}
|
||||
}
|
||||
return callbackFlow {
|
||||
val liveLocationShares = liveLocationsObserver()
|
||||
val observer = liveLocationsObserver()
|
||||
val shares: MutableList<LiveLocationShare> = ArrayList()
|
||||
val taskHandle = liveLocationShares.subscribe(object : LiveLocationsListener {
|
||||
val taskHandle = observer.subscribe(object : LiveLocationsListener {
|
||||
override fun onUpdate(updates: List<LiveLocationShareUpdate>) {
|
||||
for (update in updates) {
|
||||
shares.applyUpdate(update)
|
||||
|
|
@ -53,13 +54,14 @@ fun RoomInterface.liveLocationSharesFlow(): Flow<List<LiveLocationShare>> {
|
|||
})
|
||||
awaitClose {
|
||||
taskHandle.cancelAndDestroy()
|
||||
liveLocationShares.destroy()
|
||||
observer.destroy()
|
||||
}
|
||||
}.buffer(Channel.UNLIMITED)
|
||||
}
|
||||
|
||||
private fun RustLiveLocationShare.into(): LiveLocationShare {
|
||||
return LiveLocationShare(
|
||||
beaconId = EventId(beaconId),
|
||||
userId = UserId(userId),
|
||||
lastLocation = lastLocation?.let {
|
||||
LastLocation(
|
||||
|
|
@ -69,6 +71,6 @@ private fun RustLiveLocationShare.into(): LiveLocationShare {
|
|||
)
|
||||
},
|
||||
startTimestamp = startTs.toLong(),
|
||||
endTimestamp = (startTs + timeout).toLong()
|
||||
endTimestamp = (startTs + timeout).toLong(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import app.cash.turbine.test
|
|||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
|
||||
import io.element.android.libraries.matrix.test.room.location.aLiveLocationShare
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
|
|
@ -24,9 +25,9 @@ class TimedLiveLocationSharesFlowTest {
|
|||
@Test
|
||||
fun `it keeps emitting shares for subsequent expiries without upstream changes`() = runTest {
|
||||
val shares = listOf(
|
||||
aLiveLocationShare(userId = "@alice:server", endTimestamp = 1_000),
|
||||
aLiveLocationShare(userId = "@bob:server", endTimestamp = 2_000),
|
||||
aLiveLocationShare(userId = "@carol:server", endTimestamp = 3_000),
|
||||
aLiveLocationShare(userId = UserId("@alice:server"), endTimestamp = 1_000),
|
||||
aLiveLocationShare(userId = UserId("@bob:server"), endTimestamp = 2_000),
|
||||
aLiveLocationShare(userId = UserId("@carol:server"), endTimestamp = 3_000),
|
||||
)
|
||||
|
||||
flowOf(shares)
|
||||
|
|
@ -56,8 +57,8 @@ class TimedLiveLocationSharesFlowTest {
|
|||
@Test
|
||||
fun `it does not double-emit when a share is already expired on receipt`() = runTest {
|
||||
val shares = listOf(
|
||||
aLiveLocationShare(userId = "@alice:server", endTimestamp = 500),
|
||||
aLiveLocationShare(userId = "@bob:server", endTimestamp = 2_000),
|
||||
aLiveLocationShare(userId = UserId("@alice:server"), endTimestamp = 500),
|
||||
aLiveLocationShare(userId = UserId("@bob:server"), endTimestamp = 2_000),
|
||||
)
|
||||
|
||||
flowOf(shares)
|
||||
|
|
@ -81,8 +82,8 @@ class TimedLiveLocationSharesFlowTest {
|
|||
val upstream = MutableSharedFlow<List<LiveLocationShare>>(extraBufferCapacity = 1)
|
||||
val initialShares = listOf(aLiveLocationShare(endTimestamp = 10_000))
|
||||
val updatedShares = listOf(
|
||||
aLiveLocationShare(userId = "@alice:server", endTimestamp = 10_000),
|
||||
aLiveLocationShare(userId = "@bob:server", endTimestamp = 6_000),
|
||||
aLiveLocationShare(userId = UserId("@alice:server"), endTimestamp = 10_000),
|
||||
aLiveLocationShare(userId = UserId("@bob:server"), endTimestamp = 6_000),
|
||||
)
|
||||
|
||||
upstream
|
||||
|
|
@ -133,15 +134,3 @@ class TimedLiveLocationSharesFlowTest {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun aLiveLocationShare(
|
||||
userId: String = "@user:server",
|
||||
endTimestamp: Long,
|
||||
): LiveLocationShare {
|
||||
return LiveLocationShare(
|
||||
userId = UserId(userId),
|
||||
lastLocation = null,
|
||||
startTimestamp = 0L,
|
||||
endTimestamp = endTimestamp,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.api.room.NotJoinedRoom
|
|||
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||
import io.element.android.libraries.matrix.api.room.location.BeaconInfoUpdate
|
||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomDirectoryService
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService
|
||||
import io.element.android.libraries.matrix.api.spaces.SpaceService
|
||||
|
|
@ -107,6 +108,7 @@ class FakeMatrixClient(
|
|||
private val canReportRoomLambda: () -> Boolean = { false },
|
||||
private val isLivekitRtcSupportedLambda: () -> Boolean = { false },
|
||||
override val ignoredUsersFlow: StateFlow<ImmutableList<UserId>> = MutableStateFlow(persistentListOf()),
|
||||
override val ownBeaconInfoUpdates: Flow<BeaconInfoUpdate> = emptyFlow(),
|
||||
private val getMaxUploadSizeResult: () -> Result<Long> = { lambdaError() },
|
||||
private val getJoinedRoomIdsResult: () -> Result<Set<RoomId>> = { Result.success(emptySet()) },
|
||||
private val getRecentEmojisLambda: () -> Result<List<String>> = { Result.success(emptyList()) },
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ class FakeJoinedRoom(
|
|||
private val updateJoinRuleResult: (JoinRule) -> Result<Unit> = { lambdaError() },
|
||||
private val setSendQueueEnabledResult: (Boolean) -> Unit = { _: Boolean -> },
|
||||
private val liveLocationSharesFlow: Flow<List<LiveLocationShare>> = MutableStateFlow(emptyList()),
|
||||
private val startLiveLocationShareResult: (Long) -> Result<Unit> = { lambdaError() },
|
||||
private val startLiveLocationShareResult: (Long) -> Result<EventId> = { lambdaError() },
|
||||
private val stopLiveLocationShareResult: () -> Result<Unit> = { lambdaError() },
|
||||
private val sendLiveLocationResult: (String) -> Result<Unit> = { lambdaError() },
|
||||
) : JoinedRoom, BaseRoom by baseRoom {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.matrix.test.room.location
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||
import io.element.android.libraries.matrix.api.room.location.LastLocation
|
||||
import io.element.android.libraries.matrix.api.room.location.LiveLocationShare
|
||||
import io.element.android.libraries.matrix.test.AN_EVENT_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
|
||||
fun aLiveLocationShare(
|
||||
beaconId: EventId = AN_EVENT_ID,
|
||||
userId: UserId = A_USER_ID,
|
||||
geoUri: String = "geo:48.8584,2.2945",
|
||||
timestamp: Long = 0L,
|
||||
startTimestamp: Long = 0L,
|
||||
endTimestamp: Long = Long.MAX_VALUE,
|
||||
assetType: AssetType = AssetType.SENDER,
|
||||
): LiveLocationShare {
|
||||
return LiveLocationShare(
|
||||
beaconId = beaconId,
|
||||
userId = userId,
|
||||
lastLocation = LastLocation(
|
||||
geoUri = geoUri,
|
||||
timestamp = timestamp,
|
||||
assetType = assetType,
|
||||
),
|
||||
startTimestamp = startTimestamp,
|
||||
endTimestamp = endTimestamp,
|
||||
)
|
||||
}
|
||||
|
|
@ -23,6 +23,9 @@ interface AppPreferencesStore {
|
|||
suspend fun setTheme(theme: String)
|
||||
fun getThemeFlow(): Flow<String?>
|
||||
|
||||
suspend fun setLiveLocationMinimumDistanceInMetersUpdate(value: Int)
|
||||
fun getLiveLocationMinimumDistanceInMetersUpdateFlow(): Flow<Int>
|
||||
|
||||
@Deprecated("Use MediaPreviewService instead. Kept only for migration.")
|
||||
suspend fun setHideInviteAvatars(hide: Boolean?)
|
||||
@Deprecated("Use MediaPreviewService instead. Kept only for migration.")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import extension.setupDependencyInjection
|
||||
import extension.testCommonDependencies
|
||||
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
|
|
@ -26,4 +27,6 @@ dependencies {
|
|||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.sessionStorage.api)
|
||||
testCommonDependencies(libs)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ package io.element.android.libraries.preferences.impl.store
|
|||
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.intPreferencesKey
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import dev.zacsweers.metro.AppScope
|
||||
import dev.zacsweers.metro.ContributesBinding
|
||||
|
|
@ -28,6 +29,7 @@ private val customElementCallBaseUrlKey = stringPreferencesKey("elementCallBaseU
|
|||
private val themeKey = stringPreferencesKey("theme")
|
||||
private val hideInviteAvatarsKey = booleanPreferencesKey("hideInviteAvatars")
|
||||
private val timelineMediaPreviewValueKey = stringPreferencesKey("timelineMediaPreviewValue")
|
||||
private val liveLocationMinimumDistanceUpdateKey = intPreferencesKey("liveLocationMinimumDistanceUpdate")
|
||||
private val logLevelKey = stringPreferencesKey("logLevel")
|
||||
private val traceLogPacksKey = stringPreferencesKey("traceLogPacks")
|
||||
|
||||
|
|
@ -79,6 +81,18 @@ class DefaultAppPreferencesStore(
|
|||
}
|
||||
}
|
||||
|
||||
override suspend fun setLiveLocationMinimumDistanceInMetersUpdate(value: Int) {
|
||||
store.edit { prefs ->
|
||||
prefs[liveLocationMinimumDistanceUpdateKey] = value
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLiveLocationMinimumDistanceInMetersUpdateFlow(): Flow<Int> {
|
||||
return store.data.map { prefs ->
|
||||
prefs[liveLocationMinimumDistanceUpdateKey] ?: 10
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Use MediaPreviewService instead. Kept only for migration.")
|
||||
override fun getHideInviteAvatarsFlow(): Flow<Boolean?> {
|
||||
return store.data.map { prefs ->
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.preferences.impl.store
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.core.meta.BuildMeta
|
||||
import io.element.android.libraries.core.meta.BuildType
|
||||
import io.element.android.libraries.preferences.test.FakePreferenceDataStoreFactory
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
||||
class DefaultAppPreferencesStoreTest {
|
||||
private val buildMeta = BuildMeta(
|
||||
buildType = BuildType.DEBUG,
|
||||
isDebuggable = true,
|
||||
applicationName = "Element X",
|
||||
productionApplicationName = "Element",
|
||||
desktopApplicationName = "Element Desktop",
|
||||
applicationId = "io.element.android",
|
||||
isEnterpriseBuild = false,
|
||||
lowPrivacyLoggingEnabled = false,
|
||||
versionName = "1.0.0",
|
||||
versionCode = 1,
|
||||
gitRevision = "test",
|
||||
gitBranchName = "test",
|
||||
flavorDescription = "test",
|
||||
flavorShortDescription = "test",
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `live location minimum distance defaults to 10`() = runTest {
|
||||
val store = DefaultAppPreferencesStore(
|
||||
buildMeta = buildMeta,
|
||||
preferenceDataStoreFactory = FakePreferenceDataStoreFactory(),
|
||||
)
|
||||
|
||||
assertThat(store.getLiveLocationMinimumDistanceInMetersUpdateFlow().first()).isEqualTo(10)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `live location minimum distance persists updates`() = runTest {
|
||||
val store = DefaultAppPreferencesStore(
|
||||
buildMeta = buildMeta,
|
||||
preferenceDataStoreFactory = FakePreferenceDataStoreFactory(),
|
||||
)
|
||||
|
||||
store.setLiveLocationMinimumDistanceInMetersUpdate(25)
|
||||
|
||||
assertThat(store.getLiveLocationMinimumDistanceInMetersUpdateFlow().first()).isEqualTo(25)
|
||||
}
|
||||
}
|
||||
|
|
@ -21,12 +21,14 @@ class InMemoryAppPreferencesStore(
|
|||
hideInviteAvatars: Boolean? = null,
|
||||
timelineMediaPreviewValue: MediaPreviewValue? = null,
|
||||
theme: String? = null,
|
||||
liveLocationMinimumDistanceUpdate: Int = 10,
|
||||
logLevel: LogLevel = LogLevel.INFO,
|
||||
traceLockPacks: Set<TraceLogPack> = emptySet(),
|
||||
) : AppPreferencesStore {
|
||||
private val isDeveloperModeEnabled = MutableStateFlow(isDeveloperModeEnabled)
|
||||
private val customElementCallBaseUrl = MutableStateFlow(customElementCallBaseUrl)
|
||||
private val theme = MutableStateFlow(theme)
|
||||
private val liveLocationMinimumDistanceUpdate = MutableStateFlow(liveLocationMinimumDistanceUpdate)
|
||||
private val logLevel = MutableStateFlow(logLevel)
|
||||
private val tracingLogPacks = MutableStateFlow(traceLockPacks)
|
||||
private val hideInviteAvatars = MutableStateFlow(hideInviteAvatars)
|
||||
|
|
@ -56,6 +58,14 @@ class InMemoryAppPreferencesStore(
|
|||
return theme
|
||||
}
|
||||
|
||||
override suspend fun setLiveLocationMinimumDistanceInMetersUpdate(value: Int) {
|
||||
liveLocationMinimumDistanceUpdate.value = value
|
||||
}
|
||||
|
||||
override fun getLiveLocationMinimumDistanceInMetersUpdateFlow(): Flow<Int> {
|
||||
return liveLocationMinimumDistanceUpdate
|
||||
}
|
||||
|
||||
@Deprecated("Use MediaPreviewService instead. Kept only for migration.")
|
||||
override fun getHideInviteAvatarsFlow(): Flow<Boolean?> {
|
||||
return hideInviteAvatars
|
||||
|
|
|
|||
|
|
@ -53,4 +53,5 @@ object NotificationIdProvider {
|
|||
enum class ForegroundServiceType {
|
||||
INCOMING_CALL,
|
||||
ONGOING_CALL,
|
||||
LIVE_LOCATION,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,12 +10,13 @@ package io.element.android.libraries.sessionstorage.test.observer
|
|||
|
||||
import io.element.android.libraries.sessionstorage.api.observer.SessionListener
|
||||
import io.element.android.libraries.sessionstorage.api.observer.SessionObserver
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
|
||||
class FakeSessionObserver : SessionObserver {
|
||||
private val _listeners = mutableListOf<SessionListener>()
|
||||
private val _listeners = CopyOnWriteArraySet<SessionListener>()
|
||||
|
||||
val listeners: List<SessionListener>
|
||||
get() = _listeners
|
||||
get() = _listeners.toList()
|
||||
|
||||
override fun addListener(listener: SessionListener) {
|
||||
_listeners.add(listener)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue