Add Constraints check for permissions and GPS check

This commit is contained in:
ganfra 2026-03-12 11:53:36 +01:00
parent a56ecb9c69
commit e8308d9d8e
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,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
}

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

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,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)

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