Add Constraints check for permissions and GPS check

This commit is contained in:
ganfra
2026-03-12 11:53:36 +01:00
parent 7b305e34ef
commit 392fa9de9b
23 changed files with 382 additions and 326 deletions

View File

@@ -0,0 +1,45 @@
/*
* Copyright (c) 2025 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.features.location.impl.common
import io.element.android.features.location.impl.common.actions.LocationActions
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
sealed interface LocationConstraintsCheckResult {
data object Success : LocationConstraintsCheckResult
data object PermissionRationale : LocationConstraintsCheckResult
data object PermissionDenied : LocationConstraintsCheckResult
data object LocationServiceDisabled : LocationConstraintsCheckResult
}
fun checkLocationConstraints(
permissionsState: PermissionsState,
locationActions: LocationActions,
): LocationConstraintsCheckResult {
return when {
permissionsState.isAnyGranted -> {
if (locationActions.isLocationEnabled()) {
LocationConstraintsCheckResult.Success
} else {
LocationConstraintsCheckResult.LocationServiceDisabled
}
}
permissionsState.shouldShowRationale -> LocationConstraintsCheckResult.PermissionRationale
else -> LocationConstraintsCheckResult.PermissionDenied
}
}
fun LocationConstraintsCheckResult.toDialogState(): LocationConstraintsDialogState {
return when (this) {
LocationConstraintsCheckResult.Success -> LocationConstraintsDialogState.None
LocationConstraintsCheckResult.PermissionRationale -> LocationConstraintsDialogState.PermissionRationale
LocationConstraintsCheckResult.PermissionDenied -> LocationConstraintsDialogState.PermissionDenied
LocationConstraintsCheckResult.LocationServiceDisabled -> LocationConstraintsDialogState.LocationServiceDisabled
}
}

View File

@@ -34,43 +34,9 @@ object MapDefaults {
)
)
/*
val uiSettings: MapUiSettings
@Composable
@ReadOnlyComposable
get() = MapUiSettings(
compassEnabled = false,
rotationGesturesEnabled = false,
scrollGesturesEnabled = true,
tiltGesturesEnabled = false,
zoomGesturesEnabled = true,
logoGravity = Gravity.TOP,
attributionGravity = Gravity.TOP,
attributionTintColor = ElementTheme.colors.iconPrimary
)
val symbolManagerSettings: MapSymbolManagerSettings
get() = MapSymbolManagerSettings(
iconAllowOverlap = true
)
val locationSettings: MapLocationSettings
get() = MapLocationSettings(
locationEnabled = false,
backgroundTintColor = Color.White,
foregroundTintColor = Color.Black,
backgroundStaleTintColor = Color.White,
foregroundStaleTintColor = Color.Black,
accuracyColor = Color.Black,
pulseEnabled = true,
pulseColor = Color.Black,
)
*/
val centerCameraPosition = CameraPosition(
target = Position(49.843, 9.902056),
zoom = 2.7,
val defaultCameraPosition = CameraPosition(
target = Position(0.0, 0.0),
zoom = 0.0,
)
const val DEFAULT_ZOOM = 15.0

View File

@@ -1,29 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.features.location.impl.common
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun PermissionDeniedDialog(
onContinue: () -> Unit,
onDismiss: () -> Unit,
appName: String,
) {
ConfirmationDialog(
content = stringResource(CommonStrings.error_missing_location_auth_android, appName),
onSubmitClick = onContinue,
onDismiss = onDismiss,
submitText = stringResource(CommonStrings.action_continue),
cancelText = stringResource(CommonStrings.action_cancel),
)
}

View File

@@ -1,29 +0,0 @@
/*
* Copyright (c) 2025 Element Creations Ltd.
* Copyright 2023-2025 New Vector 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.features.location.impl.common
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun PermissionRationaleDialog(
onContinue: () -> Unit,
onDismiss: () -> Unit,
appName: String,
) {
ConfirmationDialog(
content = stringResource(CommonStrings.error_missing_location_rationale_android, appName),
onSubmitClick = onContinue,
onDismiss = onDismiss,
submitText = stringResource(CommonStrings.action_continue),
cancelText = stringResource(CommonStrings.action_cancel),
)
}

View File

@@ -43,7 +43,7 @@ class AndroidLocationActions(
}
}
override fun openSettings() {
override fun openAppSettings() {
context.openAppSettingsPage()
}

View File

@@ -12,7 +12,7 @@ import io.element.android.features.location.api.Location
interface LocationActions {
fun share(location: Location, label: String?)
fun openSettings()
fun openAppSettings()
fun isLocationEnabled(): Boolean
fun openLocationSettings()
}

View File

@@ -0,0 +1,55 @@
/*
* 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.features.location.impl.common.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.ui.strings.CommonStrings
sealed interface LocationConstraintsDialogState {
data object None : LocationConstraintsDialogState
data object PermissionRationale : LocationConstraintsDialogState
data object PermissionDenied : LocationConstraintsDialogState
data object LocationServiceDisabled : LocationConstraintsDialogState
}
@Composable
fun LocationConstraintsDialog(
state: LocationConstraintsDialogState,
appName: String,
onRequestPermissions: () -> Unit,
onOpenAppSettings: () -> Unit,
onOpenLocationSettings: () -> Unit,
onDismiss: () -> Unit,
) {
when (state) {
LocationConstraintsDialogState.None -> Unit
LocationConstraintsDialogState.PermissionRationale -> ConfirmationDialog(
content = stringResource(CommonStrings.error_missing_location_rationale_android, appName),
onSubmitClick = onRequestPermissions,
onDismiss = onDismiss,
submitText = stringResource(CommonStrings.action_continue),
cancelText = stringResource(CommonStrings.action_cancel),
)
LocationConstraintsDialogState.PermissionDenied -> ConfirmationDialog(
content = stringResource(CommonStrings.error_missing_location_auth_android, appName),
onSubmitClick = onOpenAppSettings,
onDismiss = onDismiss,
submitText = stringResource(CommonStrings.action_continue),
cancelText = stringResource(CommonStrings.action_cancel),
)
LocationConstraintsDialogState.LocationServiceDisabled -> ConfirmationDialog(
content = "Please enable your GPS to access location-based features.",
onSubmitClick = onOpenLocationSettings,
onDismiss = onDismiss,
submitText = stringResource(CommonStrings.action_continue),
cancelText = stringResource(CommonStrings.action_cancel),
)
}
}

View File

@@ -1,27 +0,0 @@
/*
* 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.features.location.impl.common.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
internal fun LocationServiceDisabledDialog(
onContinue: () -> Unit,
onDismiss: () -> Unit,
) {
ConfirmationDialog(
content = "Location services are disabled. Please enable them in your device settings to use this feature.",
onSubmitClick = onContinue,
onDismiss = onDismiss,
submitText = stringResource(CommonStrings.action_continue),
cancelText = stringResource(CommonStrings.action_cancel),
)
}

View File

@@ -20,9 +20,10 @@ sealed interface ShareLocationEvent {
data object ShowLiveLocationDurationPicker : ShareLocationEvent
data class StartLiveLocationShare(val duration: Duration) : ShareLocationEvent
data object StartTrackingUserPosition : ShareLocationEvent
data object StopTrackingUserPosition : ShareLocationEvent
data object StartTrackingUserLocation : ShareLocationEvent
data object StopTrackingUserLocation : ShareLocationEvent
data object DismissDialog : ShareLocationEvent
data object RequestPermissions: ShareLocationEvent
data object OpenAppSettings : ShareLocationEvent
data object OpenLocationSettings : ShareLocationEvent

View File

@@ -21,11 +21,15 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.Composer
import io.element.android.features.location.impl.common.LocationConstraintsCheckResult
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.actions.LocationActions
import io.element.android.features.location.impl.common.checkLocationConstraints
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.features.location.impl.common.toDialogState
import io.element.android.features.location.impl.share.ShareLocationState.Dialog.Constraints
import io.element.android.features.messages.api.MessageComposerContext
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.extensions.flatMap
@@ -63,7 +67,7 @@ class ShareLocationPresenter(
@Composable
override fun present(): ShareLocationState {
val permissionsState: PermissionsState = permissionsPresenter.present()
var trackUserPosition: Boolean by remember { mutableStateOf(permissionsState.isAnyGranted) }
var trackUserPosition: Boolean by remember { mutableStateOf(permissionsState.isAnyGranted && locationActions.isLocationEnabled()) }
val isLiveLocationSharingEnabled by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.LiveLocationSharing)
}.collectAsState(false)
@@ -74,55 +78,46 @@ class ShareLocationPresenter(
val currentUser by client.userProfile.collectAsState()
val scope = rememberCoroutineScope()
LaunchedEffect(permissionsState.permissions) {
if (permissionsState.isAnyGranted) {
trackUserPosition = true
dialogState = ShareLocationState.Dialog.None
}
fun checkLocationConstraints() {
val locationConstraints = checkLocationConstraints(permissionsState, locationActions)
dialogState = Constraints(locationConstraints.toDialogState())
trackUserPosition = locationConstraints is LocationConstraintsCheckResult.Success
}
LaunchedEffect(permissionsState.permissions) { checkLocationConstraints() }
fun handleEvent(event: ShareLocationEvent) {
when (event) {
is ShareLocationEvent.ShareStaticLocation -> scope.launch {
shareStaticLocation(event)
}
ShareLocationEvent.StartTrackingUserPosition -> when {
permissionsState.isAnyGranted -> {
if (!locationActions.isLocationEnabled()) {
dialogState = ShareLocationState.Dialog.LocationServiceDisabled
} else {
trackUserPosition = true
}
}
permissionsState.shouldShowRationale -> dialogState = ShareLocationState.Dialog.PermissionRationale
else -> dialogState = ShareLocationState.Dialog.PermissionDenied
}
ShareLocationEvent.StopTrackingUserPosition -> trackUserPosition = false
ShareLocationEvent.StartTrackingUserLocation -> checkLocationConstraints()
ShareLocationEvent.StopTrackingUserLocation -> trackUserPosition = false
ShareLocationEvent.DismissDialog -> dialogState = ShareLocationState.Dialog.None
ShareLocationEvent.OpenAppSettings -> {
locationActions.openSettings()
locationActions.openAppSettings()
dialogState = ShareLocationState.Dialog.None
}
ShareLocationEvent.OpenLocationSettings -> {
locationActions.openLocationSettings()
dialogState = ShareLocationState.Dialog.None
}
ShareLocationEvent.RequestPermissions -> permissionsState.eventSink(PermissionsEvents.RequestPermissions)
ShareLocationEvent.ShowLiveLocationDurationPicker -> dialogState = when {
permissionsState.isAnyGranted -> {
if (!locationActions.isLocationEnabled()) {
ShareLocationState.Dialog.LocationServiceDisabled
} else {
ShareLocationEvent.ShowLiveLocationDurationPicker -> {
val constraintsResult = checkLocationConstraints(permissionsState, locationActions)
dialogState = if (constraintsResult is LocationConstraintsCheckResult.Success) {
ShareLocationState.Dialog.LiveLocationDuration
} else {
Constraints(constraintsResult.toDialogState())
}
}
permissionsState.shouldShowRationale -> ShareLocationState.Dialog.PermissionRationale
else -> ShareLocationState.Dialog.PermissionDenied
}
is ShareLocationEvent.StartLiveLocationShare -> scope.launch {
dialogState = ShareLocationState.Dialog.None
//room.startLiveLocationShare(event.duration.inWholeMilliseconds)
}
ShareLocationEvent.RequestPermissions -> {
dialogState = ShareLocationState.Dialog.None
permissionsState.eventSink(PermissionsEvents.RequestPermissions)
}
}
}

View File

@@ -8,6 +8,7 @@
package io.element.android.features.location.impl.share
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.libraries.matrix.api.user.MatrixUser
data class ShareLocationState(
@@ -21,9 +22,7 @@ data class ShareLocationState(
) {
sealed interface Dialog {
data object None : Dialog
data object PermissionRationale : Dialog
data object PermissionDenied : Dialog
data object LocationServiceDisabled : Dialog
data class Constraints(val state: LocationConstraintsDialogState) : Dialog
data object LiveLocationDuration : Dialog
}
}

View File

@@ -9,6 +9,7 @@
package io.element.android.features.location.impl.share
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.user.MatrixUser
@@ -18,37 +19,37 @@ class ShareLocationStateProvider : PreviewParameterProvider<ShareLocationState>
override val values: Sequence<ShareLocationState>
get() = sequenceOf(
aShareLocationState(
permissionDialog = ShareLocationState.Dialog.None,
dialogState = ShareLocationState.Dialog.None,
trackUserPosition = false,
hasLocationPermission = false,
),
aShareLocationState(
permissionDialog = ShareLocationState.Dialog.PermissionDenied,
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied),
trackUserPosition = false,
hasLocationPermission = false,
),
aShareLocationState(
permissionDialog = ShareLocationState.Dialog.PermissionRationale,
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale),
trackUserPosition = false,
hasLocationPermission = false,
),
aShareLocationState(
permissionDialog = ShareLocationState.Dialog.LocationServiceDisabled,
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled),
trackUserPosition = false,
hasLocationPermission = true,
),
aShareLocationState(
permissionDialog = ShareLocationState.Dialog.None,
dialogState = ShareLocationState.Dialog.None,
trackUserPosition = false,
hasLocationPermission = true,
),
aShareLocationState(
permissionDialog = ShareLocationState.Dialog.None,
dialogState = ShareLocationState.Dialog.None,
trackUserPosition = true,
hasLocationPermission = true,
),
aShareLocationState(
permissionDialog = ShareLocationState.Dialog.LiveLocationDuration,
dialogState = ShareLocationState.Dialog.LiveLocationDuration,
trackUserPosition = true,
hasLocationPermission = true,
canShareLiveLocation = true,
@@ -58,14 +59,14 @@ class ShareLocationStateProvider : PreviewParameterProvider<ShareLocationState>
private fun aShareLocationState(
currentUser: MatrixUser = MatrixUser(UserId("@user:matrix.org")),
permissionDialog: ShareLocationState.Dialog,
dialogState: ShareLocationState.Dialog,
trackUserPosition: Boolean,
hasLocationPermission: Boolean,
canShareLiveLocation: Boolean = false,
): ShareLocationState {
return ShareLocationState(
currentUser = currentUser,
dialogState = permissionDialog,
dialogState = dialogState,
trackUserLocation = trackUserPosition,
hasLocationPermission = hasLocationPermission,
canShareLiveLocation = canShareLiveLocation,

View File

@@ -35,10 +35,8 @@ import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.location.api.Location
import io.element.android.features.location.api.internal.centerBottomEdge
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.PermissionDeniedDialog
import io.element.android.features.location.impl.common.PermissionRationaleDialog
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialog
import io.element.android.features.location.impl.common.ui.LocationFloatingActionButton
import io.element.android.features.location.impl.common.ui.LocationServiceDisabledDialog
import io.element.android.features.location.impl.common.ui.MapBottomSheetScaffold
import io.element.android.features.location.impl.common.ui.UserLocationPuck
import io.element.android.features.location.impl.common.ui.rememberUserLocationState
@@ -58,7 +56,6 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.ui.model.getAvatarData
import io.element.android.libraries.ui.strings.CommonStrings
import org.maplibre.compose.camera.CameraMoveReason
import org.maplibre.compose.camera.CameraPosition
import org.maplibre.compose.camera.CameraState
import org.maplibre.compose.camera.rememberCameraState
import org.maplibre.compose.location.UserLocationState
@@ -71,24 +68,14 @@ fun ShareLocationView(
navigateUp: () -> Unit,
modifier: Modifier = Modifier,
) {
LaunchedEffect(Unit) {
state.eventSink(ShareLocationEvent.RequestPermissions)
}
when (state.dialogState) {
when (val dialogState = state.dialogState) {
ShareLocationState.Dialog.None -> Unit
ShareLocationState.Dialog.PermissionDenied -> PermissionDeniedDialog(
onContinue = { state.eventSink(ShareLocationEvent.OpenAppSettings) },
onDismiss = { state.eventSink(ShareLocationEvent.DismissDialog) },
is ShareLocationState.Dialog.Constraints -> LocationConstraintsDialog(
state = dialogState.state,
appName = state.appName,
)
ShareLocationState.Dialog.PermissionRationale -> PermissionRationaleDialog(
onContinue = { state.eventSink(ShareLocationEvent.RequestPermissions) },
onDismiss = { state.eventSink(ShareLocationEvent.DismissDialog) },
appName = state.appName,
)
ShareLocationState.Dialog.LocationServiceDisabled -> LocationServiceDisabledDialog(
onContinue = { state.eventSink(ShareLocationEvent.OpenLocationSettings) },
onRequestPermissions = { state.eventSink(ShareLocationEvent.RequestPermissions) },
onOpenAppSettings = { state.eventSink(ShareLocationEvent.OpenAppSettings) },
onOpenLocationSettings = { state.eventSink(ShareLocationEvent.OpenLocationSettings) },
onDismiss = { state.eventSink(ShareLocationEvent.DismissDialog) },
)
ShareLocationState.Dialog.LiveLocationDuration -> LiveLocationDurationDialog(
@@ -103,12 +90,12 @@ fun ShareLocationView(
val scaffoldState = rememberBottomSheetScaffoldState(
bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.Expanded)
)
val cameraState = rememberCameraState(firstPosition = CameraPosition(zoom = MapDefaults.DEFAULT_ZOOM))
val cameraState = rememberCameraState(firstPosition = MapDefaults.defaultCameraPosition)
val userLocationState = rememberUserLocationState(state.hasLocationPermission)
LaunchedEffect(cameraState.isCameraMoving) {
if (cameraState.moveReason == CameraMoveReason.GESTURE) {
state.eventSink(ShareLocationEvent.StopTrackingUserPosition)
state.eventSink(ShareLocationEvent.StopTrackingUserLocation)
}
}
@@ -159,7 +146,7 @@ fun ShareLocationView(
}
LocationFloatingActionButton(
isMapCenteredOnUser = state.trackUserLocation,
onClick = { state.eventSink(ShareLocationEvent.StartTrackingUserPosition) },
onClick = { state.eventSink(ShareLocationEvent.StartTrackingUserLocation) },
modifier = Modifier
.align(Alignment.TopEnd)
.padding(all = 16.dp),
@@ -176,6 +163,21 @@ private fun BottomSheetContent(
navigateUp: () -> Unit,
) {
Spacer(Modifier.height(20.dp))
val userLocation = userLocationState.location
if (state.trackUserLocation && userLocation != null) {
ShareCurrentLocationItem {
state.eventSink(
ShareLocationEvent.ShareStaticLocation(
location = Location(
lat = userLocation.position.latitude,
lon = userLocation.position.longitude
),
isPinned = false
)
)
navigateUp()
}
} else {
SharePinLocationItem(
onClick = {
val positionTarget = cameraState.position.target
@@ -188,27 +190,7 @@ private fun BottomSheetContent(
navigateUp()
}
)
ShareCurrentLocationItem(
onClick = {
val userLocation = userLocationState.location
if (state.hasLocationPermission) {
if (userLocation == null) {
//
} else {
state.eventSink(
ShareLocationEvent.ShareStaticLocation(
location = Location(
lat = userLocation.position.latitude,
lon = userLocation.position.longitude
),
isPinned = false
)
)
navigateUp()
}
}
}
)
if (state.canShareLiveLocation) {
ShareLiveLocationItem {
state.eventSink(ShareLocationEvent.ShowLiveLocationDurationPicker)

View File

@@ -19,11 +19,15 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import io.element.android.features.location.api.ShowLocationMode
import io.element.android.features.location.impl.common.LocationConstraintsCheckResult
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.actions.LocationActions
import io.element.android.features.location.impl.common.checkLocationConstraints
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.features.location.impl.common.toDialogState
import io.element.android.features.location.impl.common.ui.LocationMarkerData
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
@@ -54,13 +58,13 @@ class ShowLocationPresenter(
val permissionsState: PermissionsState = permissionsPresenter.present()
var isTrackMyLocation by remember { mutableStateOf(false) }
val appName by remember { derivedStateOf { buildMeta.applicationName } }
var permissionDialog: ShowLocationState.Dialog by remember {
mutableStateOf(ShowLocationState.Dialog.None)
var dialogState: LocationConstraintsDialogState by remember {
mutableStateOf(LocationConstraintsDialogState.None)
}
LaunchedEffect(permissionsState.permissions) {
if (permissionsState.isAnyGranted) {
permissionDialog = ShowLocationState.Dialog.None
dialogState = LocationConstraintsDialogState.None
}
}
@@ -71,29 +75,21 @@ class ShowLocationPresenter(
}
is ShowLocationEvents.TrackMyLocation -> {
if (event.enabled) {
when {
permissionsState.isAnyGranted -> {
if (!locationActions.isLocationEnabled()) {
permissionDialog = ShowLocationState.Dialog.LocationServiceDisabled
} else {
isTrackMyLocation = true
}
}
permissionsState.shouldShowRationale -> permissionDialog = ShowLocationState.Dialog.PermissionRationale
else -> permissionDialog = ShowLocationState.Dialog.PermissionDenied
}
val locationConstraints = checkLocationConstraints(permissionsState, locationActions)
isTrackMyLocation = locationConstraints is LocationConstraintsCheckResult.Success
dialogState = locationConstraints.toDialogState()
} else {
isTrackMyLocation = false
}
}
ShowLocationEvents.DismissDialog -> permissionDialog = ShowLocationState.Dialog.None
ShowLocationEvents.DismissDialog -> dialogState = LocationConstraintsDialogState.None
ShowLocationEvents.OpenAppSettings -> {
locationActions.openSettings()
permissionDialog = ShowLocationState.Dialog.None
locationActions.openAppSettings()
dialogState = LocationConstraintsDialogState.None
}
ShowLocationEvents.OpenLocationSettings -> {
locationActions.openLocationSettings()
permissionDialog = ShowLocationState.Dialog.None
dialogState = LocationConstraintsDialogState.None
}
ShowLocationEvents.RequestPermissions -> permissionsState.eventSink(PermissionsEvents.RequestPermissions)
}
@@ -154,7 +150,7 @@ class ShowLocationPresenter(
}
return ShowLocationState(
permissionDialog = permissionDialog,
dialogState = dialogState,
mode = mode,
markers = markers,
locationShares = locationShares,

View File

@@ -10,13 +10,14 @@ package io.element.android.features.location.impl.show
import io.element.android.features.location.api.Location
import io.element.android.features.location.api.ShowLocationMode
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.features.location.impl.common.ui.LocationMarkerData
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.location.AssetType
data class ShowLocationState(
val permissionDialog: Dialog,
val dialogState: LocationConstraintsDialogState,
val mode: ShowLocationMode,
val markers: List<LocationMarkerData>,
val locationShares: List<LocationShareItem>,
@@ -25,15 +26,7 @@ data class ShowLocationState(
val appName: String,
val eventSink: (ShowLocationEvents) -> Unit,
) {
val isSheetDraggable = locationShares.any { item -> item.isLive }
sealed interface Dialog {
data object None : Dialog
data object PermissionRationale : Dialog
data object PermissionDenied : Dialog
data object LocationServiceDisabled : Dialog
}
}
data class LocationShareItem(

View File

@@ -11,6 +11,7 @@ package io.element.android.features.location.impl.show
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.location.api.Location
import io.element.android.features.location.api.ShowLocationMode
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.features.location.impl.common.ui.LocationMarkerData
import io.element.android.libraries.designsystem.components.PinVariant
import io.element.android.libraries.designsystem.components.avatar.AvatarData
@@ -25,13 +26,13 @@ class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
get() = sequenceOf(
aShowLocationState(),
aShowLocationState(
permissionDialog = ShowLocationState.Dialog.PermissionDenied,
constraintsDialogState = LocationConstraintsDialogState.PermissionDenied,
),
aShowLocationState(
permissionDialog = ShowLocationState.Dialog.PermissionRationale,
constraintsDialogState = LocationConstraintsDialogState.PermissionRationale,
),
aShowLocationState(
permissionDialog = ShowLocationState.Dialog.LocationServiceDisabled,
constraintsDialogState = LocationConstraintsDialogState.LocationServiceDisabled,
hasLocationPermission = true,
),
aShowLocationState(
@@ -41,22 +42,11 @@ class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
hasLocationPermission = true,
isTrackMyLocation = true,
),
aShowLocationState(
mode = aStaticLocationMode(senderName = "My favourite place!"),
),
aShowLocationState(
mode = aStaticLocationMode(
senderName = "For some reason I decided to write a small essay that wraps at just two lines!"
),
),
aShowLocationState(
mode = ShowLocationMode.Live,
),
)
}
fun aShowLocationState(
permissionDialog: ShowLocationState.Dialog = ShowLocationState.Dialog.None,
constraintsDialogState: LocationConstraintsDialogState = LocationConstraintsDialogState.None,
mode: ShowLocationMode = aStaticLocationMode(),
markers: List<LocationMarkerData>? = null,
locationSharers: List<LocationShareItem>? = null,
@@ -107,7 +97,7 @@ fun aShowLocationState(
ShowLocationMode.Live -> emptyList()
}
return ShowLocationState(
permissionDialog = permissionDialog,
dialogState = constraintsDialogState,
mode = mode,
markers = effectiveMarkers,
locationShares = effectiveLocationSharers,

View File

@@ -27,10 +27,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.location.api.ShowLocationMode
import io.element.android.features.location.impl.common.ui.LocationServiceDisabledDialog
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialog
import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.PermissionDeniedDialog
import io.element.android.features.location.impl.common.PermissionRationaleDialog
import io.element.android.features.location.impl.common.ui.LocationFloatingActionButton
import io.element.android.features.location.impl.common.ui.LocationPinMarkers
import io.element.android.features.location.impl.common.ui.LocationShareRow
@@ -56,30 +54,21 @@ fun ShowLocationView(
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
when (state.permissionDialog) {
ShowLocationState.Dialog.None -> Unit
ShowLocationState.Dialog.PermissionDenied -> PermissionDeniedDialog(
onContinue = { state.eventSink(ShowLocationEvents.OpenAppSettings) },
onDismiss = { state.eventSink(ShowLocationEvents.DismissDialog) },
LocationConstraintsDialog(
state = state.dialogState,
appName = state.appName,
)
ShowLocationState.Dialog.PermissionRationale -> PermissionRationaleDialog(
onContinue = { state.eventSink(ShowLocationEvents.RequestPermissions) },
onDismiss = { state.eventSink(ShowLocationEvents.DismissDialog) },
appName = state.appName,
)
ShowLocationState.Dialog.LocationServiceDisabled -> LocationServiceDisabledDialog(
onContinue = { state.eventSink(ShowLocationEvents.OpenLocationSettings) },
onRequestPermissions = { state.eventSink(ShowLocationEvents.RequestPermissions) },
onOpenAppSettings = { state.eventSink(ShowLocationEvents.OpenAppSettings) },
onOpenLocationSettings = { state.eventSink(ShowLocationEvents.OpenLocationSettings) },
onDismiss = { state.eventSink(ShowLocationEvents.DismissDialog) },
)
}
val initialPosition = when (val mode = state.mode) {
is ShowLocationMode.Static -> CameraPosition(
target = Position(latitude = mode.location.lat, longitude = mode.location.lon),
zoom = MapDefaults.DEFAULT_ZOOM
)
ShowLocationMode.Live -> MapDefaults.centerCameraPosition
ShowLocationMode.Live -> MapDefaults.defaultCameraPosition
}
val cameraState = rememberCameraState(firstPosition = initialPosition)
val userLocationState = rememberUserLocationState(state.hasLocationPermission)

View File

@@ -0,0 +1,79 @@
/*
* Copyright (c) 2025 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.features.location.impl.common
import com.google.common.truth.Truth.assertThat
import io.element.android.features.location.impl.aPermissionsState
import io.element.android.features.location.impl.common.actions.FakeLocationActions
import io.element.android.features.location.impl.common.permissions.PermissionsState
import org.junit.Test
class LocationConstraintsCheckTest {
@Test
fun `checkLocationConstraints returns Success when permissions granted and location enabled`() {
val permissionsState = aPermissionsState(
permissions = PermissionsState.Permissions.AllGranted,
)
val locationActions = FakeLocationActions(isLocationEnabled = true)
val result = checkLocationConstraints(permissionsState, locationActions)
assertThat(result).isEqualTo(LocationConstraintsCheckResult.Success)
}
@Test
fun `checkLocationConstraints returns Success when some permissions granted and location enabled`() {
val permissionsState = aPermissionsState(
permissions = PermissionsState.Permissions.SomeGranted,
)
val locationActions = FakeLocationActions(isLocationEnabled = true)
val result = checkLocationConstraints(permissionsState, locationActions)
assertThat(result).isEqualTo(LocationConstraintsCheckResult.Success)
}
@Test
fun `checkLocationConstraints returns LocationServiceDisabled when permissions granted but location disabled`() {
val permissionsState = aPermissionsState(
permissions = PermissionsState.Permissions.AllGranted,
)
val locationActions = FakeLocationActions(isLocationEnabled = false)
val result = checkLocationConstraints(permissionsState, locationActions)
assertThat(result).isEqualTo(LocationConstraintsCheckResult.LocationServiceDisabled)
}
@Test
fun `checkLocationConstraints returns PermissionRationale when permissions denied with rationale`() {
val permissionsState = aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = true,
)
val locationActions = FakeLocationActions(isLocationEnabled = true)
val result = checkLocationConstraints(permissionsState, locationActions)
assertThat(result).isEqualTo(LocationConstraintsCheckResult.PermissionRationale)
}
@Test
fun `checkLocationConstraints returns PermissionDenied when permissions denied without rationale`() {
val permissionsState = aPermissionsState(
permissions = PermissionsState.Permissions.NoneGranted,
shouldShowRationale = false,
)
val locationActions = FakeLocationActions(isLocationEnabled = true)
val result = checkLocationConstraints(permissionsState, locationActions)
assertThat(result).isEqualTo(LocationConstraintsCheckResult.PermissionDenied)
}
}

View File

@@ -30,7 +30,7 @@ class FakeLocationActions(
sharedLabel = label
}
override fun openSettings() {
override fun openAppSettings() {
openSettingsInvocationsCount++
}

View File

@@ -14,7 +14,9 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.location.impl.common.actions.FakeLocationActions
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
import io.element.android.features.messages.test.FakeMessageComposerContext
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
import io.element.android.services.analytics.test.FakeAnalyticsService
@@ -42,6 +44,8 @@ class DefaultShareLocationEntryPointTest {
messageComposerContext = FakeMessageComposerContext(),
locationActions = FakeLocationActions(),
buildMeta = aBuildMeta(),
featureFlagService = FakeFeatureFlagService(),
client = FakeMatrixClient(),
)
},
analyticsService = FakeAnalyticsService(),

View File

@@ -228,7 +228,7 @@ class ShareLocationPresenterTest {
assertThat(myLocationState.hasLocationPermission).isFalse()
// Continue the dialog sends permission request to the permissions presenter
myLocationState.eventSink(ShareLocationEvent.RequestPermissions)
myLocationState.eventSink(ShareLocationEvent.StartTrackingUserLocation)
assertThat(fakePermissionsPresenter.events.last()).isEqualTo(PermissionsEvents.RequestPermissions)
}
}

View File

@@ -13,12 +13,15 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.location.api.Location
import io.element.android.features.location.api.ShowLocationMode
import io.element.android.features.location.impl.aPermissionsState
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.features.location.impl.common.actions.FakeLocationActions
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.delay
@@ -33,15 +36,25 @@ class ShowLocationPresenterTest {
private val fakePermissionsPresenter = FakePermissionsPresenter()
private val fakeLocationActions = FakeLocationActions()
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
private val fakeDateFormatter = FakeDateFormatter()
private val location = Location(1.23, 4.56, 7.8f)
private val presenter = ShowLocationPresenter(
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
override fun create(permissions: List<String>): PermissionsPresenter = fakePermissionsPresenter
},
locationActions = fakeLocationActions,
buildMeta = fakeBuildMeta,
private fun createShowLocationPresenter(
mode: ShowLocationMode = ShowLocationMode.Static(
location = location,
description = A_DESCRIPTION,
senderName = "Alice",
senderId = UserId("@alice:matrix.org"),
senderAvatarUrl = null,
timestamp = System.currentTimeMillis(),
assetType = null,
),
locationActions: FakeLocationActions = fakeLocationActions,
) = ShowLocationPresenter(
mode = mode,
permissionsPresenterFactory = { fakePermissionsPresenter },
locationActions = locationActions,
buildMeta = fakeBuildMeta,
dateFormatter = fakeDateFormatter,
)
@Test
@@ -54,11 +67,9 @@ class ShowLocationPresenterTest {
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
createShowLocationPresenter().present()
}.test {
val initialState = awaitItem()
assertThat(initialState.location).isEqualTo(location)
assertThat(initialState.description).isEqualTo(A_DESCRIPTION)
assertThat(initialState.hasLocationPermission).isFalse()
assertThat(initialState.isTrackMyLocation).isFalse()
}
@@ -74,11 +85,9 @@ class ShowLocationPresenterTest {
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
createShowLocationPresenter().present()
}.test {
val initialState = awaitItem()
assertThat(initialState.location).isEqualTo(location)
assertThat(initialState.description).isEqualTo(A_DESCRIPTION)
assertThat(initialState.hasLocationPermission).isFalse()
assertThat(initialState.isTrackMyLocation).isFalse()
}
@@ -89,11 +98,9 @@ class ShowLocationPresenterTest {
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
createShowLocationPresenter().present()
}.test {
val initialState = awaitItem()
assertThat(initialState.location).isEqualTo(location)
assertThat(initialState.description).isEqualTo(A_DESCRIPTION)
assertThat(initialState.hasLocationPermission).isTrue()
assertThat(initialState.isTrackMyLocation).isFalse()
}
@@ -104,11 +111,9 @@ class ShowLocationPresenterTest {
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.SomeGranted))
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
createShowLocationPresenter().present()
}.test {
val initialState = awaitItem()
assertThat(initialState.location).isEqualTo(location)
assertThat(initialState.description).isEqualTo(A_DESCRIPTION)
assertThat(initialState.hasLocationPermission).isTrue()
assertThat(initialState.isTrackMyLocation).isFalse()
}
@@ -117,13 +122,12 @@ class ShowLocationPresenterTest {
@Test
fun `uses action to share location`() = runTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
createShowLocationPresenter().present()
}.test {
val initialState = awaitItem()
initialState.eventSink(ShowLocationEvents.Share)
initialState.eventSink(ShowLocationEvents.Share(location))
assertThat(fakeLocationActions.sharedLocation).isEqualTo(location)
assertThat(fakeLocationActions.sharedLabel).isEqualTo(A_DESCRIPTION)
}
}
@@ -132,7 +136,7 @@ class ShowLocationPresenterTest {
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
createShowLocationPresenter().present()
}.test {
val initialState = awaitItem()
assertThat(initialState.hasLocationPermission).isTrue()
@@ -149,7 +153,7 @@ class ShowLocationPresenterTest {
// Swipe the map to switch mode
initialState.eventSink(ShowLocationEvents.TrackMyLocation(false))
val trackLocationDisabledState = awaitItem()
assertThat(trackLocationDisabledState.permissionDialog).isEqualTo(ShowLocationState.Dialog.None)
assertThat(trackLocationDisabledState.dialogState).isEqualTo(LocationConstraintsDialogState.None)
assertThat(trackLocationDisabledState.isTrackMyLocation).isFalse()
assertThat(trackLocationDisabledState.hasLocationPermission).isTrue()
}
@@ -165,7 +169,7 @@ class ShowLocationPresenterTest {
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
createShowLocationPresenter().present()
}.test {
// Skip initial state
val initialState = awaitItem()
@@ -173,14 +177,14 @@ class ShowLocationPresenterTest {
// Click on the button to switch mode
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
val trackLocationState = awaitItem()
assertThat(trackLocationState.permissionDialog).isEqualTo(ShowLocationState.Dialog.PermissionRationale)
assertThat(trackLocationState.dialogState).isEqualTo(LocationConstraintsDialogState.PermissionRationale)
assertThat(trackLocationState.isTrackMyLocation).isFalse()
assertThat(trackLocationState.hasLocationPermission).isFalse()
// Dismiss the dialog
initialState.eventSink(ShowLocationEvents.DismissDialog)
val dialogDismissedState = awaitItem()
assertThat(dialogDismissedState.permissionDialog).isEqualTo(ShowLocationState.Dialog.None)
assertThat(dialogDismissedState.dialogState).isEqualTo(LocationConstraintsDialogState.None)
assertThat(dialogDismissedState.isTrackMyLocation).isFalse()
assertThat(dialogDismissedState.hasLocationPermission).isFalse()
}
@@ -196,7 +200,7 @@ class ShowLocationPresenterTest {
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
createShowLocationPresenter().present()
}.test {
// Skip initial state
val initialState = awaitItem()
@@ -204,7 +208,7 @@ class ShowLocationPresenterTest {
// Click on the button to switch mode
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
val trackLocationState = awaitItem()
assertThat(trackLocationState.permissionDialog).isEqualTo(ShowLocationState.Dialog.PermissionRationale)
assertThat(trackLocationState.dialogState).isEqualTo(LocationConstraintsDialogState.PermissionRationale)
assertThat(trackLocationState.isTrackMyLocation).isFalse()
assertThat(trackLocationState.hasLocationPermission).isFalse()
@@ -224,7 +228,7 @@ class ShowLocationPresenterTest {
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
createShowLocationPresenter().present()
}.test {
// Skip initial state
val initialState = awaitItem()
@@ -232,14 +236,14 @@ class ShowLocationPresenterTest {
// Click on the button to switch mode
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
val trackLocationState = awaitItem()
assertThat(trackLocationState.permissionDialog).isEqualTo(ShowLocationState.Dialog.PermissionDenied)
assertThat(trackLocationState.dialogState).isEqualTo(LocationConstraintsDialogState.PermissionDenied)
assertThat(trackLocationState.isTrackMyLocation).isFalse()
assertThat(trackLocationState.hasLocationPermission).isFalse()
// Dismiss the dialog
initialState.eventSink(ShowLocationEvents.DismissDialog)
val dialogDismissedState = awaitItem()
assertThat(dialogDismissedState.permissionDialog).isEqualTo(ShowLocationState.Dialog.None)
assertThat(dialogDismissedState.dialogState).isEqualTo(LocationConstraintsDialogState.None)
assertThat(dialogDismissedState.isTrackMyLocation).isFalse()
assertThat(dialogDismissedState.hasLocationPermission).isFalse()
}
@@ -255,7 +259,7 @@ class ShowLocationPresenterTest {
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
createShowLocationPresenter().present()
}.test {
// Skip initial state
val initialState = awaitItem()
@@ -267,7 +271,7 @@ class ShowLocationPresenterTest {
dialogShownState.eventSink(ShowLocationEvents.OpenAppSettings)
val settingsOpenedState = awaitItem()
assertThat(settingsOpenedState.permissionDialog).isEqualTo(ShowLocationState.Dialog.None)
assertThat(settingsOpenedState.dialogState).isEqualTo(LocationConstraintsDialogState.None)
assertThat(fakeLocationActions.openSettingsInvocationsCount).isEqualTo(1)
}
}
@@ -275,14 +279,53 @@ class ShowLocationPresenterTest {
@Test
fun `application name is in state`() = runTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
createShowLocationPresenter().present()
}.test {
val initialState = awaitItem()
assertThat(initialState.appName).isEqualTo("app name")
}
}
companion object {
private const val A_DESCRIPTION = "My happy place"
@Test
fun `location service disabled shows dialog`() = runTest {
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
fakeLocationActions.givenLocationEnabled(false)
moleculeFlow(RecompositionMode.Immediate) {
createShowLocationPresenter(locationActions = fakeLocationActions).present()
}.test {
val initialState = awaitItem()
assertThat(initialState.hasLocationPermission).isTrue()
// Try to track location when location services are disabled
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
val dialogShownState = awaitItem()
assertThat(dialogShownState.dialogState).isEqualTo(LocationConstraintsDialogState.LocationServiceDisabled)
assertThat(dialogShownState.isTrackMyLocation).isFalse()
}
}
@Test
fun `open location settings from dialog`() = runTest {
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
fakeLocationActions.givenLocationEnabled(false)
moleculeFlow(RecompositionMode.Immediate) {
createShowLocationPresenter(locationActions = fakeLocationActions).present()
}.test {
val initialState = awaitItem()
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
val dialogShownState = awaitItem()
assertThat(dialogShownState.dialogState).isEqualTo(LocationConstraintsDialogState.LocationServiceDisabled)
// Open location settings
dialogShownState.eventSink(ShowLocationEvents.OpenLocationSettings)
val settingsOpenedState = awaitItem()
assertThat(settingsOpenedState.dialogState).isEqualTo(LocationConstraintsDialogState.None)
assertThat(fakeLocationActions.openLocationSettingsInvocationsCount).isEqualTo(1)
}
}
}

View File

@@ -17,6 +17,8 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.location.api.Location
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureNeverCalled
@@ -58,7 +60,8 @@ class ShowLocationViewTest {
)
val shareContentDescription = rule.activity.getString(CommonStrings.action_share)
rule.onNodeWithContentDescription(shareContentDescription).performClick()
eventsRecorder.assertSingle(ShowLocationEvents.Share)
// The default aStaticLocationMode uses Location(1.23, 2.34, 4f)
eventsRecorder.assertSingle(ShowLocationEvents.Share(Location(1.23, 2.34, 4f)))
}
@Test
@@ -79,7 +82,7 @@ class ShowLocationViewTest {
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
rule.setShowLocationView(
aShowLocationState(
permissionDialog = ShowLocationState.Dialog.PermissionDenied,
constraintsDialogState = LocationConstraintsDialogState.PermissionDenied,
eventSink = eventsRecorder
),
onBackClick = EnsureNeverCalled(),
@@ -93,7 +96,7 @@ class ShowLocationViewTest {
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
rule.setShowLocationView(
aShowLocationState(
permissionDialog = ShowLocationState.Dialog.PermissionDenied,
constraintsDialogState = LocationConstraintsDialogState.PermissionDenied,
eventSink = eventsRecorder
),
onBackClick = EnsureNeverCalled(),
@@ -107,7 +110,7 @@ class ShowLocationViewTest {
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
rule.setShowLocationView(
aShowLocationState(
permissionDialog = ShowLocationState.Dialog.PermissionRationale,
constraintsDialogState = LocationConstraintsDialogState.PermissionRationale,
eventSink = eventsRecorder
),
onBackClick = EnsureNeverCalled(),
@@ -121,7 +124,7 @@ class ShowLocationViewTest {
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
rule.setShowLocationView(
aShowLocationState(
permissionDialog = ShowLocationState.Dialog.PermissionRationale,
constraintsDialogState = LocationConstraintsDialogState.PermissionRationale,
eventSink = eventsRecorder
),
onBackClick = EnsureNeverCalled(),