Add Constraints check for permissions and GPS check
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -43,7 +43,7 @@ class AndroidLocationActions(
|
||||
}
|
||||
}
|
||||
|
||||
override fun openSettings() {
|
||||
override fun openAppSettings() {
|
||||
context.openAppSettingsPage()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -30,7 +30,7 @@ class FakeLocationActions(
|
||||
sharedLabel = label
|
||||
}
|
||||
|
||||
override fun openSettings() {
|
||||
override fun openAppSettings() {
|
||||
openSettingsInvocationsCount++
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>): 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ShowLocationEvents>()
|
||||
rule.setShowLocationView(
|
||||
aShowLocationState(
|
||||
permissionDialog = ShowLocationState.Dialog.PermissionDenied,
|
||||
constraintsDialogState = LocationConstraintsDialogState.PermissionDenied,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onBackClick = EnsureNeverCalled(),
|
||||
@@ -93,7 +96,7 @@ class ShowLocationViewTest {
|
||||
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
|
||||
rule.setShowLocationView(
|
||||
aShowLocationState(
|
||||
permissionDialog = ShowLocationState.Dialog.PermissionDenied,
|
||||
constraintsDialogState = LocationConstraintsDialogState.PermissionDenied,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onBackClick = EnsureNeverCalled(),
|
||||
@@ -107,7 +110,7 @@ class ShowLocationViewTest {
|
||||
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
|
||||
rule.setShowLocationView(
|
||||
aShowLocationState(
|
||||
permissionDialog = ShowLocationState.Dialog.PermissionRationale,
|
||||
constraintsDialogState = LocationConstraintsDialogState.PermissionRationale,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onBackClick = EnsureNeverCalled(),
|
||||
@@ -121,7 +124,7 @@ class ShowLocationViewTest {
|
||||
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
|
||||
rule.setShowLocationView(
|
||||
aShowLocationState(
|
||||
permissionDialog = ShowLocationState.Dialog.PermissionRationale,
|
||||
constraintsDialogState = LocationConstraintsDialogState.PermissionRationale,
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onBackClick = EnsureNeverCalled(),
|
||||
|
||||
Reference in New Issue
Block a user