Check location is enabled

This commit is contained in:
ganfra
2026-03-10 21:48:37 +01:00
parent 4bfe467ac1
commit b284984dad
14 changed files with 139 additions and 22 deletions

View File

@@ -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

View File

@@ -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()
}

View File

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

View File

@@ -25,4 +25,5 @@ sealed interface ShareLocationEvent {
data object DismissDialog : ShareLocationEvent
data object RequestPermissions : ShareLocationEvent
data object OpenAppSettings : ShareLocationEvent
data object OpenLocationSettings : ShareLocationEvent
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -32,6 +32,11 @@ class ShareLocationStateProvider : PreviewParameterProvider<ShareLocationState>
trackUserPosition = false,
hasLocationPermission = false,
),
aShareLocationState(
permissionDialog = ShareLocationState.Dialog.LocationServiceDisabled,
trackUserPosition = false,
hasLocationPermission = true,
),
aShareLocationState(
permissionDialog = ShareLocationState.Dialog.None,
trackUserPosition = false,

View File

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

View File

@@ -16,4 +16,5 @@ sealed interface ShowLocationEvents {
data object DismissDialog : ShowLocationEvents
data object RequestPermissions : ShowLocationEvents
data object OpenAppSettings : ShowLocationEvents
data object OpenLocationSettings : ShowLocationEvents
}

View File

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

View File

@@ -32,6 +32,7 @@ data class ShowLocationState(
data object None : Dialog
data object PermissionRationale : Dialog
data object PermissionDenied : Dialog
data object LocationServiceDisabled : Dialog
}
}

View File

@@ -30,6 +30,10 @@ class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
aShowLocationState(
permissionDialog = ShowLocationState.Dialog.PermissionRationale,
),
aShowLocationState(
permissionDialog = ShowLocationState.Dialog.LocationServiceDisabled,
hasLocationPermission = true,
),
aShowLocationState(
hasLocationPermission = true,
),

View File

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

View File

@@ -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
}
}