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 0284c25a0a..d69eb018e1 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 @@ -10,8 +10,11 @@ package io.element.android.features.location.impl.common.actions import android.content.Context import android.content.Intent +import android.location.LocationManager import android.net.Uri +import android.provider.Settings import androidx.annotation.VisibleForTesting +import androidx.core.location.LocationManagerCompat import androidx.core.net.toUri import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding @@ -43,6 +46,23 @@ class AndroidLocationActions( override fun openSettings() { context.openAppSettingsPage() } + + override fun isLocationEnabled(): Boolean { + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + return LocationManagerCompat.isLocationEnabled(locationManager) + } + + override fun openLocationSettings() { + runCatchingExceptions { + val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + }.onSuccess { + Timber.v("Open location settings succeed") + }.onFailure { + Timber.e(it, "Open location settings failed") + } + } } // Ref: https://developer.android.com/guide/components/intents-common#ViewMap 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 cd9efbd261..c4c5db40d0 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 @@ -13,4 +13,6 @@ import io.element.android.features.location.api.Location interface LocationActions { fun share(location: Location, label: String?) fun openSettings() + fun isLocationEnabled(): Boolean + fun openLocationSettings() } 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 new file mode 100644 index 0000000000..5184845632 --- /dev/null +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/common/ui/LocationServiceDisabledDialog.kt @@ -0,0 +1,27 @@ +/* + * 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 eb641df9fb..7e68ecf358 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 @@ -25,4 +25,5 @@ sealed interface ShareLocationEvent { data object DismissDialog : 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 63d4b7ce8d..df70cddacb 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 @@ -33,12 +33,10 @@ import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.CreateTimelineParams import io.element.android.libraries.matrix.api.room.JoinedRoom import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.timeline.Timeline -import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.textcomposer.model.MessageComposerMode import io.element.android.services.analytics.api.AnalyticsService import kotlinx.coroutines.launch @@ -89,7 +87,13 @@ class ShareLocationPresenter( shareStaticLocation(event) } ShareLocationEvent.StartTrackingUserPosition -> when { - permissionsState.isAnyGranted -> trackUserPosition = true + permissionsState.isAnyGranted -> { + if (!locationActions.isLocationEnabled()) { + dialogState = ShareLocationState.Dialog.LocationServiceDisabled + } else { + trackUserPosition = true + } + } permissionsState.shouldShowRationale -> dialogState = ShareLocationState.Dialog.PermissionRationale else -> dialogState = ShareLocationState.Dialog.PermissionDenied } @@ -99,9 +103,19 @@ class ShareLocationPresenter( locationActions.openSettings() dialogState = ShareLocationState.Dialog.None } + ShareLocationEvent.OpenLocationSettings -> { + locationActions.openLocationSettings() + dialogState = ShareLocationState.Dialog.None + } ShareLocationEvent.RequestPermissions -> permissionsState.eventSink(PermissionsEvents.RequestPermissions) ShareLocationEvent.ShowLiveLocationDurationPicker -> dialogState = when { - permissionsState.isAnyGranted -> ShareLocationState.Dialog.LiveLocationDuration + permissionsState.isAnyGranted -> { + if (!locationActions.isLocationEnabled()) { + ShareLocationState.Dialog.LocationServiceDisabled + } else { + ShareLocationState.Dialog.LiveLocationDuration + } + } permissionsState.shouldShowRationale -> ShareLocationState.Dialog.PermissionRationale else -> ShareLocationState.Dialog.PermissionDenied } 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 547bb99856..c204357a7a 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 @@ -23,6 +23,7 @@ data class ShareLocationState( data object None : Dialog data object PermissionRationale : Dialog data object PermissionDenied : Dialog + data object LocationServiceDisabled : 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 0d3c448f15..71b143fcef 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 @@ -32,6 +32,11 @@ class ShareLocationStateProvider : PreviewParameterProvider trackUserPosition = false, hasLocationPermission = false, ), + aShareLocationState( + permissionDialog = ShareLocationState.Dialog.LocationServiceDisabled, + trackUserPosition = false, + hasLocationPermission = true, + ), aShareLocationState( permissionDialog = ShareLocationState.Dialog.None, trackUserPosition = false, 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 a0705221c0..cc88a2d427 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 @@ -38,6 +38,7 @@ 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.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.libraries.designsystem.components.LocationPin @@ -90,6 +91,10 @@ fun ShareLocationView( onDismiss = { state.eventSink(ShareLocationEvent.DismissDialog) }, appName = state.appName, ) + ShareLocationState.Dialog.LocationServiceDisabled -> LocationServiceDisabledDialog( + onContinue = { state.eventSink(ShareLocationEvent.OpenLocationSettings) }, + onDismiss = { state.eventSink(ShareLocationEvent.DismissDialog) }, + ) ShareLocationState.Dialog.LiveLocationDuration -> LiveLocationDurationDialog( onSelectDuration = { duration -> state.eventSink(ShareLocationEvent.StartLiveLocationShare(duration)) diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationEvents.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationEvents.kt index 672cab9beb..52ff87b393 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationEvents.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/show/ShowLocationEvents.kt @@ -16,4 +16,5 @@ sealed interface ShowLocationEvents { data object DismissDialog : ShowLocationEvents data object RequestPermissions : ShowLocationEvents data object OpenAppSettings : ShowLocationEvents + data object OpenLocationSettings : ShowLocationEvents } 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 b131d0d137..2fe40af256 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 @@ -72,7 +72,13 @@ class ShowLocationPresenter( is ShowLocationEvents.TrackMyLocation -> { if (event.enabled) { when { - permissionsState.isAnyGranted -> isTrackMyLocation = true + permissionsState.isAnyGranted -> { + if (!locationActions.isLocationEnabled()) { + permissionDialog = ShowLocationState.Dialog.LocationServiceDisabled + } else { + isTrackMyLocation = true + } + } permissionsState.shouldShowRationale -> permissionDialog = ShowLocationState.Dialog.PermissionRationale else -> permissionDialog = ShowLocationState.Dialog.PermissionDenied } @@ -85,6 +91,10 @@ class ShowLocationPresenter( locationActions.openSettings() permissionDialog = ShowLocationState.Dialog.None } + ShowLocationEvents.OpenLocationSettings -> { + locationActions.openLocationSettings() + permissionDialog = ShowLocationState.Dialog.None + } ShowLocationEvents.RequestPermissions -> permissionsState.eventSink(PermissionsEvents.RequestPermissions) } } 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 118d7e9f61..2f66dcb26c 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 @@ -32,6 +32,7 @@ data class ShowLocationState( data object None : Dialog data object PermissionRationale : Dialog data object PermissionDenied : Dialog + data object LocationServiceDisabled : Dialog } } 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 a11b317170..a28edf11d0 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 @@ -30,6 +30,10 @@ class ShowLocationStateProvider : PreviewParameterProvider { aShowLocationState( permissionDialog = ShowLocationState.Dialog.PermissionRationale, ), + aShowLocationState( + permissionDialog = ShowLocationState.Dialog.LocationServiceDisabled, + hasLocationPermission = true, + ), aShowLocationState( hasLocationPermission = true, ), 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 0193e52827..5b7c9b3d08 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 @@ -9,6 +9,8 @@ package io.element.android.features.location.impl.show import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -23,23 +25,21 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import io.element.android.compound.tokens.generated.CompoundIcons +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.MapDefaults import io.element.android.features.location.impl.common.PermissionDeniedDialog import io.element.android.features.location.impl.common.PermissionRationaleDialog -import io.element.android.compound.theme.ElementTheme 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 import io.element.android.features.location.impl.common.ui.MapBottomSheetScaffold import io.element.android.features.location.impl.common.ui.UserLocationPuck -import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.coroutines.launch @@ -72,6 +72,10 @@ fun ShowLocationView( onDismiss = { state.eventSink(ShowLocationEvents.DismissDialog) }, appName = state.appName, ) + ShowLocationState.Dialog.LocationServiceDisabled -> LocationServiceDisabledDialog( + onContinue = { state.eventSink(ShowLocationEvents.OpenLocationSettings) }, + onDismiss = { state.eventSink(ShowLocationEvents.DismissDialog) }, + ) } val initialPosition = when (val mode = state.mode) { @@ -99,18 +103,18 @@ fun ShowLocationView( } val scaffoldState = rememberBottomSheetScaffoldState( - bottomSheetState = rememberStandardBottomSheetState(initialValue = - if(state.isSheetDraggable) { - SheetValue.PartiallyExpanded - }else { - SheetValue.Expanded - } + bottomSheetState = rememberStandardBottomSheetState( + initialValue = + if (state.isSheetDraggable) { + SheetValue.PartiallyExpanded + } else { + SheetValue.Expanded + } ) ) MapBottomSheetScaffold( - //sheetPeekHeight = 180.dp, - sheetDragHandle = if(state.isSheetDraggable) { - {BottomSheetDefaults.DragHandle()} + sheetDragHandle = if (state.isSheetDraggable) { + { BottomSheetDefaults.DragHandle() } } else { null }, @@ -130,8 +134,9 @@ fun ShowLocationView( }, sheetContent = { sheetPaddings -> val coroutineScope = rememberCoroutineScope() + Spacer(Modifier.height(20.dp)) Text( - text = "On the map", + text = stringResource(CommonStrings.screen_static_location_sheet_title), style = ElementTheme.typography.fontBodyLgMedium, color = ElementTheme.colors.textPrimary, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), @@ -142,7 +147,11 @@ fun ShowLocationView( onShareClick = { state.eventSink(ShowLocationEvents.Share(locationShare.location)) }, modifier = Modifier.clickable { state.eventSink(ShowLocationEvents.TrackMyLocation(false)) - val position = CameraPosition(padding = sheetPaddings, target = Position(locationShare.location.lon, locationShare.location.lat), zoom = MapDefaults.DEFAULT_ZOOM) + val position = CameraPosition( + padding = sheetPaddings, + target = Position(locationShare.location.lon, locationShare.location.lat), + zoom = MapDefaults.DEFAULT_ZOOM + ) coroutineScope.launch { cameraState.animateTo(finalPosition = position) } 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 94dc972213..795e36fa1a 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 @@ -10,7 +10,9 @@ package io.element.android.features.location.impl.common.actions import io.element.android.features.location.api.Location -class FakeLocationActions : LocationActions { +class FakeLocationActions( + private var isLocationEnabled: Boolean = true, +) : LocationActions { var sharedLocation: Location? = null private set @@ -20,6 +22,9 @@ class FakeLocationActions : LocationActions { var openSettingsInvocationsCount = 0 private set + var openLocationSettingsInvocationsCount = 0 + private set + override fun share(location: Location, label: String?) { sharedLocation = location sharedLabel = label @@ -28,4 +33,16 @@ class FakeLocationActions : LocationActions { override fun openSettings() { openSettingsInvocationsCount++ } + + override fun isLocationEnabled(): Boolean { + return isLocationEnabled + } + + override fun openLocationSettings() { + openLocationSettingsInvocationsCount++ + } + + fun givenLocationEnabled(enabled: Boolean) { + isLocationEnabled = enabled + } }