change(location): ensure permissions are always requested at least once

This commit is contained in:
ganfra
2026-06-08 21:56:22 +02:00
parent bea2903093
commit 6b9aa31c19
11 changed files with 190 additions and 36 deletions
@@ -14,6 +14,7 @@ import io.element.android.features.location.impl.common.ui.LocationConstraintsDi
sealed interface LocationConstraintsCheck {
data object Success : LocationConstraintsCheck
data object PermissionRationale : LocationConstraintsCheck
data object PermissionShouldBeRequested : LocationConstraintsCheck
data object PermissionDenied : LocationConstraintsCheck
data object LocationServiceDisabled : LocationConstraintsCheck
data object NotEnoughPowerLevel : LocationConstraintsCheck
@@ -34,6 +35,7 @@ fun checkLocationConstraints(
}
}
permissionsState.shouldShowRationale -> LocationConstraintsCheck.PermissionRationale
!permissionsState.permissionsAlreadyRequested -> LocationConstraintsCheck.PermissionShouldBeRequested
else -> LocationConstraintsCheck.PermissionDenied
}
}
@@ -41,6 +43,7 @@ fun checkLocationConstraints(
fun LocationConstraintsCheck.toDialogState(): LocationConstraintsDialogState {
return when (this) {
LocationConstraintsCheck.Success -> LocationConstraintsDialogState.None
LocationConstraintsCheck.PermissionShouldBeRequested -> LocationConstraintsDialogState.None
LocationConstraintsCheck.PermissionRationale -> LocationConstraintsDialogState.PermissionRationale
LocationConstraintsCheck.PermissionDenied -> LocationConstraintsDialogState.PermissionDenied
LocationConstraintsCheck.LocationServiceDisabled -> LocationConstraintsDialogState.LocationServiceDisabled
@@ -9,6 +9,10 @@
package io.element.android.features.location.impl.common.permissions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState
@@ -32,11 +36,16 @@ class DefaultPermissionsPresenter(
@OptIn(ExperimentalPermissionsApi::class)
@Composable
override fun present(): PermissionsState {
val multiplePermissionsState = rememberMultiplePermissionsState(permissions = permissions)
var permissionsRequested by remember { mutableStateOf(false) }
val multiplePermissionsState = rememberMultiplePermissionsState(permissions = permissions) {
permissionsRequested = true
}
fun handleEvent(event: PermissionsEvents) {
when (event) {
PermissionsEvents.RequestPermissions -> multiplePermissionsState.launchMultiplePermissionRequest()
PermissionsEvents.RequestPermissions -> {
multiplePermissionsState.launchMultiplePermissionRequest()
}
}
}
@@ -47,6 +56,7 @@ class DefaultPermissionsPresenter(
else -> PermissionsState.Permissions.NoneGranted
},
shouldShowRationale = multiplePermissionsState.shouldShowRationale,
permissionsAlreadyRequested = permissionsRequested,
eventSink = ::handleEvent,
)
}
@@ -11,6 +11,7 @@ package io.element.android.features.location.impl.common.permissions
data class PermissionsState(
val permissions: Permissions,
val shouldShowRationale: Boolean,
val permissionsAlreadyRequested: Boolean,
val eventSink: (PermissionsEvents) -> Unit,
) {
sealed interface Permissions {
@@ -88,6 +88,8 @@ class ShareLocationPresenter(
var dialogState: ShareLocationState.Dialog by remember {
mutableStateOf(ShareLocationState.Dialog.None)
}
// true when trying to initiate the live location share
var pendingLiveLocationShare by remember { mutableStateOf(false) }
val startLiveLocationAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
val currentUser by client.userProfile.collectAsState()
val customMapStyleUrl by produceState(AsyncData.Loading()) {
@@ -100,34 +102,55 @@ class ShareLocationPresenter(
val scope = rememberCoroutineScope()
fun checkLocationConstraints() {
// No need to check SendLiveLocationPermissions here
val locationConstraints = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED)
val locationConstraints = checkLocationConstraints(
permissionsState = permissionsState,
locationActions = locationActions,
// No need to check SendLiveLocationPermissions here
sendLiveLocationPermissions = SendLiveLocationPermissions.GRANTED
)
if (locationConstraints is LocationConstraintsCheck.PermissionShouldBeRequested) {
permissionsState.eventSink(PermissionsEvents.RequestPermissions)
}
trackUserPosition = locationConstraints is LocationConstraintsCheck.Success
dialogState = ShareLocationState.Dialog.Constraints(locationConstraints.toDialogState())
}
suspend fun computeLiveLocationDialogState(): ShareLocationState.Dialog {
val hasAcceptedDisclaimer = liveLocationStore.hasAcceptedLiveLocationDisclaimer()
val constraintsResult = checkLocationConstraints(permissionsState, locationActions, sendLiveLocationPermissions)
return when {
!hasAcceptedDisclaimer -> {
ShareLocationState.Dialog.LiveLocationDisclaimer
}
constraintsResult is LocationConstraintsCheck.Success -> {
val durations = LIVE_LOCATION_DURATIONS.map {
LiveLocationDuration(duration = it, formatted = durationFormatter.format(it))
suspend fun checkLiveLocationConstraints() {
val locationConstraints = checkLocationConstraints(
permissionsState = permissionsState,
locationActions = locationActions,
sendLiveLocationPermissions = sendLiveLocationPermissions,
)
when (locationConstraints) {
LocationConstraintsCheck.Success -> {
val hasAcceptedDisclaimer = liveLocationStore.hasAcceptedLiveLocationDisclaimer()
dialogState = if (!hasAcceptedDisclaimer) {
ShareLocationState.Dialog.LiveLocationDisclaimer
} else {
val durations = LIVE_LOCATION_DURATIONS.map {
LiveLocationDuration(duration = it, formatted = durationFormatter.format(it))
}
ShareLocationState.Dialog.LiveLocationDurations(durations.toImmutableList())
}
ShareLocationState.Dialog.LiveLocationDurations(durations.toImmutableList())
}
else -> {
ShareLocationState.Dialog.Constraints(constraintsResult.toDialogState())
if (locationConstraints is LocationConstraintsCheck.PermissionShouldBeRequested) {
permissionsState.eventSink(PermissionsEvents.RequestPermissions)
}
dialogState = ShareLocationState.Dialog.Constraints(locationConstraints.toDialogState())
}
}
}
val userLocationState = userLocationStateFactory.create(permissionsState.isAnyGranted)
LaunchedEffect(permissionsState.permissions) { checkLocationConstraints() }
LaunchedEffect(permissionsState) {
if (pendingLiveLocationShare) {
checkLiveLocationConstraints()
} else {
checkLocationConstraints()
}
}
fun handleEvent(event: ShareLocationEvent) {
when (event) {
@@ -136,7 +159,10 @@ class ShareLocationPresenter(
}
ShareLocationEvent.StartTrackingUserLocation -> checkLocationConstraints()
ShareLocationEvent.StopTrackingUserLocation -> trackUserPosition = false
ShareLocationEvent.DismissDialog -> dialogState = ShareLocationState.Dialog.None
ShareLocationEvent.DismissDialog -> {
pendingLiveLocationShare = false
dialogState = ShareLocationState.Dialog.None
}
ShareLocationEvent.OpenAppSettings -> {
locationActions.openAppSettings()
dialogState = ShareLocationState.Dialog.None
@@ -146,15 +172,17 @@ class ShareLocationPresenter(
dialogState = ShareLocationState.Dialog.None
}
ShareLocationEvent.InitiateLiveLocationShare -> scope.launch {
dialogState = computeLiveLocationDialogState()
pendingLiveLocationShare = true
checkLiveLocationConstraints()
}
ShareLocationEvent.AcceptLiveLocationDisclaimer -> scope.launch {
liveLocationStore.setAcceptedLiveLocationDisclaimer()
.onSuccess {
dialogState = computeLiveLocationDialogState()
checkLiveLocationConstraints()
}
}
is ShareLocationEvent.StartLiveLocationShare -> scope.launch {
pendingLiveLocationShare = false
dialogState = ShareLocationState.Dialog.None
startLiveLocationAction.runUpdatingState {
liveLocationShareManager.startShare(
@@ -90,10 +90,22 @@ import kotlinx.coroutines.launch
value = AsyncData.Success(client.getMapStyleUrl().getOrNull())
}
LaunchedEffect(permissionsState.permissions) {
if (permissionsState.isAnyGranted) {
dialogState = LocationConstraintsDialogState.None
fun checkLocationConstraints() {
val locationConstraints = checkLocationConstraints(
permissionsState = permissionsState,
locationActions = locationActions,
// No need to check SendLiveLocationPermissions here
sendLiveLocationPermissions = SendLiveLocationPermissions.GRANTED
)
if (locationConstraints is LocationConstraintsCheck.PermissionShouldBeRequested) {
permissionsState.eventSink(PermissionsEvents.RequestPermissions)
}
isTrackMyLocation = locationConstraints is LocationConstraintsCheck.Success
dialogState = locationConstraints.toDialogState()
}
LaunchedEffect(permissionsState) {
checkLocationConstraints()
}
fun handleEvent(event: ShowLocationEvent) {
@@ -103,9 +115,7 @@ import kotlinx.coroutines.launch
}
is ShowLocationEvent.TrackMyLocation -> {
if (event.enabled) {
val locationConstraints = checkLocationConstraints(permissionsState, locationActions, SendLiveLocationPermissions.GRANTED)
isTrackMyLocation = locationConstraints is LocationConstraintsCheck.Success
dialogState = locationConstraints.toDialogState()
checkLocationConstraints()
} else {
isTrackMyLocation = false
}