diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/LocationConstraintsCheck.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/LocationConstraintsCheck.kt new file mode 100644 index 0000000000..51d57713c5 --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/LocationConstraintsCheck.kt @@ -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 + } +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/MapDefaults.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/MapDefaults.kt index c515ff6c72..1093e5760a 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/MapDefaults.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/MapDefaults.kt @@ -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 diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/PermissionDeniedDialog.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/PermissionDeniedDialog.kt deleted file mode 100644 index 6817f579e5..0000000000 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/PermissionDeniedDialog.kt +++ /dev/null @@ -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), - ) -} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/PermissionRationaleDialog.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/PermissionRationaleDialog.kt deleted file mode 100644 index 7aef07e32b..0000000000 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/PermissionRationaleDialog.kt +++ /dev/null @@ -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), - ) -} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActions.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActions.kt index d69eb018e1..7994a6e6b1 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActions.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/AndroidLocationActions.kt @@ -43,7 +43,7 @@ class AndroidLocationActions( } } - override fun openSettings() { + override fun openAppSettings() { context.openAppSettingsPage() } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/LocationActions.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/LocationActions.kt index c4c5db40d0..bc8e558c55 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/LocationActions.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/actions/LocationActions.kt @@ -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() } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationConstraintsDialog.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationConstraintsDialog.kt new file mode 100644 index 0000000000..d42a551254 --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationConstraintsDialog.kt @@ -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), + ) + } +} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationServiceDisabledDialog.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationServiceDisabledDialog.kt deleted file mode 100644 index 5184845632..0000000000 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationServiceDisabledDialog.kt +++ /dev/null @@ -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), - ) -} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationEvent.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationEvent.kt index 7e68ecf358..b95e86084b 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationEvent.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationEvent.kt @@ -20,10 +20,11 @@ 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 RequestPermissions: ShareLocationEvent data object OpenAppSettings : ShareLocationEvent data object OpenLocationSettings : ShareLocationEvent } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt index df70cddacb..7ca8cbb7e3 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenter.kt @@ -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 { - ShareLocationState.Dialog.LiveLocationDuration - } + 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) + } } } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationState.kt index c204357a7a..72f94d5b06 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationState.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationState.kt @@ -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 } } diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt index 71b143fcef..768d1a5d50 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationStateProvider.kt @@ -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 override val values: Sequence 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 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, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt index 6fb649a59a..e5fa9d8c0c 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/share/ShareLocationView.kt @@ -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,39 +163,34 @@ private fun BottomSheetContent( navigateUp: () -> Unit, ) { Spacer(Modifier.height(20.dp)) - SharePinLocationItem( - onClick = { - val positionTarget = cameraState.position.target + val userLocation = userLocationState.location + if (state.trackUserLocation && userLocation != null) { + ShareCurrentLocationItem { state.eventSink( ShareLocationEvent.ShareStaticLocation( - location = Location(lat = positionTarget.latitude, lon = positionTarget.longitude), - isPinned = true + location = Location( + lat = userLocation.position.latitude, + lon = userLocation.position.longitude + ), + isPinned = false ) ) 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 - ) + } else { + SharePinLocationItem( + onClick = { + val positionTarget = cameraState.position.target + state.eventSink( + ShareLocationEvent.ShareStaticLocation( + location = Location(lat = positionTarget.latitude, lon = positionTarget.longitude), + isPinned = true ) - navigateUp() - } + ) + navigateUp() } - } - ) + ) + } if (state.canShareLiveLocation) { ShareLiveLocationItem { state.eventSink(ShareLocationEvent.ShowLiveLocationDurationPicker) diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt index 2fe40af256..c48c2c27d2 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenter.kt @@ -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, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt index 2f66dcb26c..3a8fcf51de 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationState.kt @@ -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, val locationShares: List, @@ -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( diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt index a28edf11d0..6880ed1bf5 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationStateProvider.kt @@ -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 { 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 { 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? = null, locationSharers: List? = null, @@ -107,7 +97,7 @@ fun aShowLocationState( ShowLocationMode.Live -> emptyList() } return ShowLocationState( - permissionDialog = permissionDialog, + dialogState = constraintsDialogState, mode = mode, markers = effectiveMarkers, locationShares = effectiveLocationSharers, diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt index eec67ad956..3d3dc6f5c6 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationView.kt @@ -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) }, - 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) }, - onDismiss = { state.eventSink(ShowLocationEvents.DismissDialog) }, - ) - } + LocationConstraintsDialog( + state = state.dialogState, + appName = state.appName, + 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) diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/LocationConstraintsCheckTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/LocationConstraintsCheckTest.kt new file mode 100644 index 0000000000..801d58e5da --- /dev/null +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/LocationConstraintsCheckTest.kt @@ -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) + } + +} diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/FakeLocationActions.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/FakeLocationActions.kt index 795e36fa1a..e05787d6a6 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/FakeLocationActions.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/common/actions/FakeLocationActions.kt @@ -30,7 +30,7 @@ class FakeLocationActions( sharedLabel = label } - override fun openSettings() { + override fun openAppSettings() { openSettingsInvocationsCount++ } diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/DefaultShareLocationEntryPointTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/DefaultShareLocationEntryPointTest.kt index b6161b3a9c..100e660820 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/DefaultShareLocationEntryPointTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/DefaultShareLocationEntryPointTest.kt @@ -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(), diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt index 8d34fb99d6..32d862893a 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/share/ShareLocationPresenterTest.kt @@ -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) } } diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt index df22863c7c..ebbad8e9d1 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationPresenterTest.kt @@ -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): PermissionsPresenter = fakePermissionsPresenter - }, - locationActions = fakeLocationActions, + + private fun createShowLocationPresenter( + mode: ShowLocationMode = ShowLocationMode.Static( + location = location, + 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, - location = location, - description = A_DESCRIPTION, + 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) + } } } diff --git a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt index 2245360bb2..a70d3441c4 100644 --- a/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt +++ b/features/location/impl/src/test/kotlin/io/element/android/features/location/impl/show/ShowLocationViewTest.kt @@ -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() rule.setShowLocationView( aShowLocationState( - permissionDialog = ShowLocationState.Dialog.PermissionDenied, + constraintsDialogState = LocationConstraintsDialogState.PermissionDenied, eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), @@ -93,7 +96,7 @@ class ShowLocationViewTest { val eventsRecorder = EventsRecorder() rule.setShowLocationView( aShowLocationState( - permissionDialog = ShowLocationState.Dialog.PermissionDenied, + constraintsDialogState = LocationConstraintsDialogState.PermissionDenied, eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), @@ -107,7 +110,7 @@ class ShowLocationViewTest { val eventsRecorder = EventsRecorder() rule.setShowLocationView( aShowLocationState( - permissionDialog = ShowLocationState.Dialog.PermissionRationale, + constraintsDialogState = LocationConstraintsDialogState.PermissionRationale, eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(), @@ -121,7 +124,7 @@ class ShowLocationViewTest { val eventsRecorder = EventsRecorder() rule.setShowLocationView( aShowLocationState( - permissionDialog = ShowLocationState.Dialog.PermissionRationale, + constraintsDialogState = LocationConstraintsDialogState.PermissionRationale, eventSink = eventsRecorder ), onBackClick = EnsureNeverCalled(),