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 defaultCameraPosition = CameraPosition(
|
||||||
val uiSettings: MapUiSettings
|
target = Position(0.0, 0.0),
|
||||||
@Composable
|
zoom = 0.0,
|
||||||
@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,
|
|
||||||
)
|
)
|
||||||
const val DEFAULT_ZOOM = 15.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()
|
context.openAppSettingsPage()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import io.element.android.features.location.api.Location
|
|||||||
|
|
||||||
interface LocationActions {
|
interface LocationActions {
|
||||||
fun share(location: Location, label: String?)
|
fun share(location: Location, label: String?)
|
||||||
fun openSettings()
|
fun openAppSettings()
|
||||||
fun isLocationEnabled(): Boolean
|
fun isLocationEnabled(): Boolean
|
||||||
fun openLocationSettings()
|
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 object ShowLiveLocationDurationPicker : ShareLocationEvent
|
||||||
data class StartLiveLocationShare(val duration: Duration) : ShareLocationEvent
|
data class StartLiveLocationShare(val duration: Duration) : ShareLocationEvent
|
||||||
|
|
||||||
data object StartTrackingUserPosition : ShareLocationEvent
|
data object StartTrackingUserLocation : ShareLocationEvent
|
||||||
data object StopTrackingUserPosition : ShareLocationEvent
|
data object StopTrackingUserLocation : ShareLocationEvent
|
||||||
data object DismissDialog : ShareLocationEvent
|
data object DismissDialog : ShareLocationEvent
|
||||||
data object RequestPermissions : ShareLocationEvent
|
|
||||||
|
data object RequestPermissions: ShareLocationEvent
|
||||||
data object OpenAppSettings : ShareLocationEvent
|
data object OpenAppSettings : ShareLocationEvent
|
||||||
data object OpenLocationSettings : ShareLocationEvent
|
data object OpenLocationSettings : ShareLocationEvent
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,15 @@ import dev.zacsweers.metro.Assisted
|
|||||||
import dev.zacsweers.metro.AssistedFactory
|
import dev.zacsweers.metro.AssistedFactory
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.Composer
|
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.MapDefaults
|
||||||
import io.element.android.features.location.impl.common.actions.LocationActions
|
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.PermissionsEvents
|
||||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
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.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.features.messages.api.MessageComposerContext
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.extensions.flatMap
|
import io.element.android.libraries.core.extensions.flatMap
|
||||||
@@ -63,7 +67,7 @@ class ShareLocationPresenter(
|
|||||||
@Composable
|
@Composable
|
||||||
override fun present(): ShareLocationState {
|
override fun present(): ShareLocationState {
|
||||||
val permissionsState: PermissionsState = permissionsPresenter.present()
|
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 {
|
val isLiveLocationSharingEnabled by remember {
|
||||||
featureFlagService.isFeatureEnabledFlow(FeatureFlags.LiveLocationSharing)
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.LiveLocationSharing)
|
||||||
}.collectAsState(false)
|
}.collectAsState(false)
|
||||||
@@ -74,55 +78,46 @@ class ShareLocationPresenter(
|
|||||||
val currentUser by client.userProfile.collectAsState()
|
val currentUser by client.userProfile.collectAsState()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(permissionsState.permissions) {
|
fun checkLocationConstraints() {
|
||||||
if (permissionsState.isAnyGranted) {
|
val locationConstraints = checkLocationConstraints(permissionsState, locationActions)
|
||||||
trackUserPosition = true
|
dialogState = Constraints(locationConstraints.toDialogState())
|
||||||
dialogState = ShareLocationState.Dialog.None
|
trackUserPosition = locationConstraints is LocationConstraintsCheckResult.Success
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(permissionsState.permissions) { checkLocationConstraints() }
|
||||||
|
|
||||||
fun handleEvent(event: ShareLocationEvent) {
|
fun handleEvent(event: ShareLocationEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is ShareLocationEvent.ShareStaticLocation -> scope.launch {
|
is ShareLocationEvent.ShareStaticLocation -> scope.launch {
|
||||||
shareStaticLocation(event)
|
shareStaticLocation(event)
|
||||||
}
|
}
|
||||||
ShareLocationEvent.StartTrackingUserPosition -> when {
|
ShareLocationEvent.StartTrackingUserLocation -> checkLocationConstraints()
|
||||||
permissionsState.isAnyGranted -> {
|
ShareLocationEvent.StopTrackingUserLocation -> trackUserPosition = false
|
||||||
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.DismissDialog -> dialogState = ShareLocationState.Dialog.None
|
ShareLocationEvent.DismissDialog -> dialogState = ShareLocationState.Dialog.None
|
||||||
ShareLocationEvent.OpenAppSettings -> {
|
ShareLocationEvent.OpenAppSettings -> {
|
||||||
locationActions.openSettings()
|
locationActions.openAppSettings()
|
||||||
dialogState = ShareLocationState.Dialog.None
|
dialogState = ShareLocationState.Dialog.None
|
||||||
}
|
}
|
||||||
ShareLocationEvent.OpenLocationSettings -> {
|
ShareLocationEvent.OpenLocationSettings -> {
|
||||||
locationActions.openLocationSettings()
|
locationActions.openLocationSettings()
|
||||||
dialogState = ShareLocationState.Dialog.None
|
dialogState = ShareLocationState.Dialog.None
|
||||||
}
|
}
|
||||||
ShareLocationEvent.RequestPermissions -> permissionsState.eventSink(PermissionsEvents.RequestPermissions)
|
ShareLocationEvent.ShowLiveLocationDurationPicker -> {
|
||||||
ShareLocationEvent.ShowLiveLocationDurationPicker -> dialogState = when {
|
val constraintsResult = checkLocationConstraints(permissionsState, locationActions)
|
||||||
permissionsState.isAnyGranted -> {
|
dialogState = if (constraintsResult is LocationConstraintsCheckResult.Success) {
|
||||||
if (!locationActions.isLocationEnabled()) {
|
ShareLocationState.Dialog.LiveLocationDuration
|
||||||
ShareLocationState.Dialog.LocationServiceDisabled
|
} else {
|
||||||
} else {
|
Constraints(constraintsResult.toDialogState())
|
||||||
ShareLocationState.Dialog.LiveLocationDuration
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
permissionsState.shouldShowRationale -> ShareLocationState.Dialog.PermissionRationale
|
|
||||||
else -> ShareLocationState.Dialog.PermissionDenied
|
|
||||||
}
|
}
|
||||||
is ShareLocationEvent.StartLiveLocationShare -> scope.launch {
|
is ShareLocationEvent.StartLiveLocationShare -> scope.launch {
|
||||||
dialogState = ShareLocationState.Dialog.None
|
dialogState = ShareLocationState.Dialog.None
|
||||||
//room.startLiveLocationShare(event.duration.inWholeMilliseconds)
|
//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
|
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
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
data class ShareLocationState(
|
data class ShareLocationState(
|
||||||
@@ -21,9 +22,7 @@ data class ShareLocationState(
|
|||||||
) {
|
) {
|
||||||
sealed interface Dialog {
|
sealed interface Dialog {
|
||||||
data object None : Dialog
|
data object None : Dialog
|
||||||
data object PermissionRationale : Dialog
|
data class Constraints(val state: LocationConstraintsDialogState) : Dialog
|
||||||
data object PermissionDenied : Dialog
|
|
||||||
data object LocationServiceDisabled : Dialog
|
|
||||||
data object LiveLocationDuration : Dialog
|
data object LiveLocationDuration : Dialog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
package io.element.android.features.location.impl.share
|
package io.element.android.features.location.impl.share
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
||||||
@@ -18,37 +19,37 @@ class ShareLocationStateProvider : PreviewParameterProvider<ShareLocationState>
|
|||||||
override val values: Sequence<ShareLocationState>
|
override val values: Sequence<ShareLocationState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aShareLocationState(
|
aShareLocationState(
|
||||||
permissionDialog = ShareLocationState.Dialog.None,
|
dialogState = ShareLocationState.Dialog.None,
|
||||||
trackUserPosition = false,
|
trackUserPosition = false,
|
||||||
hasLocationPermission = false,
|
hasLocationPermission = false,
|
||||||
),
|
),
|
||||||
aShareLocationState(
|
aShareLocationState(
|
||||||
permissionDialog = ShareLocationState.Dialog.PermissionDenied,
|
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionDenied),
|
||||||
trackUserPosition = false,
|
trackUserPosition = false,
|
||||||
hasLocationPermission = false,
|
hasLocationPermission = false,
|
||||||
),
|
),
|
||||||
aShareLocationState(
|
aShareLocationState(
|
||||||
permissionDialog = ShareLocationState.Dialog.PermissionRationale,
|
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.PermissionRationale),
|
||||||
trackUserPosition = false,
|
trackUserPosition = false,
|
||||||
hasLocationPermission = false,
|
hasLocationPermission = false,
|
||||||
),
|
),
|
||||||
aShareLocationState(
|
aShareLocationState(
|
||||||
permissionDialog = ShareLocationState.Dialog.LocationServiceDisabled,
|
dialogState = ShareLocationState.Dialog.Constraints(LocationConstraintsDialogState.LocationServiceDisabled),
|
||||||
trackUserPosition = false,
|
trackUserPosition = false,
|
||||||
hasLocationPermission = true,
|
hasLocationPermission = true,
|
||||||
),
|
),
|
||||||
aShareLocationState(
|
aShareLocationState(
|
||||||
permissionDialog = ShareLocationState.Dialog.None,
|
dialogState = ShareLocationState.Dialog.None,
|
||||||
trackUserPosition = false,
|
trackUserPosition = false,
|
||||||
hasLocationPermission = true,
|
hasLocationPermission = true,
|
||||||
),
|
),
|
||||||
aShareLocationState(
|
aShareLocationState(
|
||||||
permissionDialog = ShareLocationState.Dialog.None,
|
dialogState = ShareLocationState.Dialog.None,
|
||||||
trackUserPosition = true,
|
trackUserPosition = true,
|
||||||
hasLocationPermission = true,
|
hasLocationPermission = true,
|
||||||
),
|
),
|
||||||
aShareLocationState(
|
aShareLocationState(
|
||||||
permissionDialog = ShareLocationState.Dialog.LiveLocationDuration,
|
dialogState = ShareLocationState.Dialog.LiveLocationDuration,
|
||||||
trackUserPosition = true,
|
trackUserPosition = true,
|
||||||
hasLocationPermission = true,
|
hasLocationPermission = true,
|
||||||
canShareLiveLocation = true,
|
canShareLiveLocation = true,
|
||||||
@@ -58,14 +59,14 @@ class ShareLocationStateProvider : PreviewParameterProvider<ShareLocationState>
|
|||||||
|
|
||||||
private fun aShareLocationState(
|
private fun aShareLocationState(
|
||||||
currentUser: MatrixUser = MatrixUser(UserId("@user:matrix.org")),
|
currentUser: MatrixUser = MatrixUser(UserId("@user:matrix.org")),
|
||||||
permissionDialog: ShareLocationState.Dialog,
|
dialogState: ShareLocationState.Dialog,
|
||||||
trackUserPosition: Boolean,
|
trackUserPosition: Boolean,
|
||||||
hasLocationPermission: Boolean,
|
hasLocationPermission: Boolean,
|
||||||
canShareLiveLocation: Boolean = false,
|
canShareLiveLocation: Boolean = false,
|
||||||
): ShareLocationState {
|
): ShareLocationState {
|
||||||
return ShareLocationState(
|
return ShareLocationState(
|
||||||
currentUser = currentUser,
|
currentUser = currentUser,
|
||||||
dialogState = permissionDialog,
|
dialogState = dialogState,
|
||||||
trackUserLocation = trackUserPosition,
|
trackUserLocation = trackUserPosition,
|
||||||
hasLocationPermission = hasLocationPermission,
|
hasLocationPermission = hasLocationPermission,
|
||||||
canShareLiveLocation = canShareLiveLocation,
|
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.Location
|
||||||
import io.element.android.features.location.api.internal.centerBottomEdge
|
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.MapDefaults
|
||||||
import io.element.android.features.location.impl.common.PermissionDeniedDialog
|
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialog
|
||||||
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.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.MapBottomSheetScaffold
|
||||||
import io.element.android.features.location.impl.common.ui.UserLocationPuck
|
import io.element.android.features.location.impl.common.ui.UserLocationPuck
|
||||||
import io.element.android.features.location.impl.common.ui.rememberUserLocationState
|
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.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import org.maplibre.compose.camera.CameraMoveReason
|
import org.maplibre.compose.camera.CameraMoveReason
|
||||||
import org.maplibre.compose.camera.CameraPosition
|
|
||||||
import org.maplibre.compose.camera.CameraState
|
import org.maplibre.compose.camera.CameraState
|
||||||
import org.maplibre.compose.camera.rememberCameraState
|
import org.maplibre.compose.camera.rememberCameraState
|
||||||
import org.maplibre.compose.location.UserLocationState
|
import org.maplibre.compose.location.UserLocationState
|
||||||
@@ -71,24 +68,14 @@ fun ShareLocationView(
|
|||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(Unit) {
|
when (val dialogState = state.dialogState) {
|
||||||
state.eventSink(ShareLocationEvent.RequestPermissions)
|
|
||||||
}
|
|
||||||
|
|
||||||
when (state.dialogState) {
|
|
||||||
ShareLocationState.Dialog.None -> Unit
|
ShareLocationState.Dialog.None -> Unit
|
||||||
ShareLocationState.Dialog.PermissionDenied -> PermissionDeniedDialog(
|
is ShareLocationState.Dialog.Constraints -> LocationConstraintsDialog(
|
||||||
onContinue = { state.eventSink(ShareLocationEvent.OpenAppSettings) },
|
state = dialogState.state,
|
||||||
onDismiss = { state.eventSink(ShareLocationEvent.DismissDialog) },
|
|
||||||
appName = state.appName,
|
appName = state.appName,
|
||||||
)
|
onRequestPermissions = { state.eventSink(ShareLocationEvent.RequestPermissions) },
|
||||||
ShareLocationState.Dialog.PermissionRationale -> PermissionRationaleDialog(
|
onOpenAppSettings = { state.eventSink(ShareLocationEvent.OpenAppSettings) },
|
||||||
onContinue = { state.eventSink(ShareLocationEvent.RequestPermissions) },
|
onOpenLocationSettings = { state.eventSink(ShareLocationEvent.OpenLocationSettings) },
|
||||||
onDismiss = { state.eventSink(ShareLocationEvent.DismissDialog) },
|
|
||||||
appName = state.appName,
|
|
||||||
)
|
|
||||||
ShareLocationState.Dialog.LocationServiceDisabled -> LocationServiceDisabledDialog(
|
|
||||||
onContinue = { state.eventSink(ShareLocationEvent.OpenLocationSettings) },
|
|
||||||
onDismiss = { state.eventSink(ShareLocationEvent.DismissDialog) },
|
onDismiss = { state.eventSink(ShareLocationEvent.DismissDialog) },
|
||||||
)
|
)
|
||||||
ShareLocationState.Dialog.LiveLocationDuration -> LiveLocationDurationDialog(
|
ShareLocationState.Dialog.LiveLocationDuration -> LiveLocationDurationDialog(
|
||||||
@@ -103,12 +90,12 @@ fun ShareLocationView(
|
|||||||
val scaffoldState = rememberBottomSheetScaffoldState(
|
val scaffoldState = rememberBottomSheetScaffoldState(
|
||||||
bottomSheetState = rememberStandardBottomSheetState(initialValue = SheetValue.Expanded)
|
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)
|
val userLocationState = rememberUserLocationState(state.hasLocationPermission)
|
||||||
|
|
||||||
LaunchedEffect(cameraState.isCameraMoving) {
|
LaunchedEffect(cameraState.isCameraMoving) {
|
||||||
if (cameraState.moveReason == CameraMoveReason.GESTURE) {
|
if (cameraState.moveReason == CameraMoveReason.GESTURE) {
|
||||||
state.eventSink(ShareLocationEvent.StopTrackingUserPosition)
|
state.eventSink(ShareLocationEvent.StopTrackingUserLocation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +146,7 @@ fun ShareLocationView(
|
|||||||
}
|
}
|
||||||
LocationFloatingActionButton(
|
LocationFloatingActionButton(
|
||||||
isMapCenteredOnUser = state.trackUserLocation,
|
isMapCenteredOnUser = state.trackUserLocation,
|
||||||
onClick = { state.eventSink(ShareLocationEvent.StartTrackingUserPosition) },
|
onClick = { state.eventSink(ShareLocationEvent.StartTrackingUserLocation) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
.padding(all = 16.dp),
|
.padding(all = 16.dp),
|
||||||
@@ -176,39 +163,34 @@ private fun BottomSheetContent(
|
|||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
SharePinLocationItem(
|
val userLocation = userLocationState.location
|
||||||
onClick = {
|
if (state.trackUserLocation && userLocation != null) {
|
||||||
val positionTarget = cameraState.position.target
|
ShareCurrentLocationItem {
|
||||||
state.eventSink(
|
state.eventSink(
|
||||||
ShareLocationEvent.ShareStaticLocation(
|
ShareLocationEvent.ShareStaticLocation(
|
||||||
location = Location(lat = positionTarget.latitude, lon = positionTarget.longitude),
|
location = Location(
|
||||||
isPinned = true
|
lat = userLocation.position.latitude,
|
||||||
|
lon = userLocation.position.longitude
|
||||||
|
),
|
||||||
|
isPinned = false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
navigateUp()
|
navigateUp()
|
||||||
}
|
}
|
||||||
)
|
} else {
|
||||||
ShareCurrentLocationItem(
|
SharePinLocationItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
val userLocation = userLocationState.location
|
val positionTarget = cameraState.position.target
|
||||||
if (state.hasLocationPermission) {
|
state.eventSink(
|
||||||
if (userLocation == null) {
|
ShareLocationEvent.ShareStaticLocation(
|
||||||
//
|
location = Location(lat = positionTarget.latitude, lon = positionTarget.longitude),
|
||||||
} else {
|
isPinned = true
|
||||||
state.eventSink(
|
|
||||||
ShareLocationEvent.ShareStaticLocation(
|
|
||||||
location = Location(
|
|
||||||
lat = userLocation.position.latitude,
|
|
||||||
lon = userLocation.position.longitude
|
|
||||||
),
|
|
||||||
isPinned = false
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
navigateUp()
|
)
|
||||||
}
|
navigateUp()
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
if (state.canShareLiveLocation) {
|
if (state.canShareLiveLocation) {
|
||||||
ShareLiveLocationItem {
|
ShareLiveLocationItem {
|
||||||
state.eventSink(ShareLocationEvent.ShowLiveLocationDurationPicker)
|
state.eventSink(ShareLocationEvent.ShowLiveLocationDurationPicker)
|
||||||
|
|||||||
@@ -19,11 +19,15 @@ import dev.zacsweers.metro.Assisted
|
|||||||
import dev.zacsweers.metro.AssistedFactory
|
import dev.zacsweers.metro.AssistedFactory
|
||||||
import dev.zacsweers.metro.AssistedInject
|
import dev.zacsweers.metro.AssistedInject
|
||||||
import io.element.android.features.location.api.ShowLocationMode
|
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.MapDefaults
|
||||||
import io.element.android.features.location.impl.common.actions.LocationActions
|
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.PermissionsEvents
|
||||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
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.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.features.location.impl.common.ui.LocationMarkerData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
@@ -54,13 +58,13 @@ class ShowLocationPresenter(
|
|||||||
val permissionsState: PermissionsState = permissionsPresenter.present()
|
val permissionsState: PermissionsState = permissionsPresenter.present()
|
||||||
var isTrackMyLocation by remember { mutableStateOf(false) }
|
var isTrackMyLocation by remember { mutableStateOf(false) }
|
||||||
val appName by remember { derivedStateOf { buildMeta.applicationName } }
|
val appName by remember { derivedStateOf { buildMeta.applicationName } }
|
||||||
var permissionDialog: ShowLocationState.Dialog by remember {
|
var dialogState: LocationConstraintsDialogState by remember {
|
||||||
mutableStateOf(ShowLocationState.Dialog.None)
|
mutableStateOf(LocationConstraintsDialogState.None)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(permissionsState.permissions) {
|
LaunchedEffect(permissionsState.permissions) {
|
||||||
if (permissionsState.isAnyGranted) {
|
if (permissionsState.isAnyGranted) {
|
||||||
permissionDialog = ShowLocationState.Dialog.None
|
dialogState = LocationConstraintsDialogState.None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,29 +75,21 @@ class ShowLocationPresenter(
|
|||||||
}
|
}
|
||||||
is ShowLocationEvents.TrackMyLocation -> {
|
is ShowLocationEvents.TrackMyLocation -> {
|
||||||
if (event.enabled) {
|
if (event.enabled) {
|
||||||
when {
|
val locationConstraints = checkLocationConstraints(permissionsState, locationActions)
|
||||||
permissionsState.isAnyGranted -> {
|
isTrackMyLocation = locationConstraints is LocationConstraintsCheckResult.Success
|
||||||
if (!locationActions.isLocationEnabled()) {
|
dialogState = locationConstraints.toDialogState()
|
||||||
permissionDialog = ShowLocationState.Dialog.LocationServiceDisabled
|
|
||||||
} else {
|
|
||||||
isTrackMyLocation = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
permissionsState.shouldShowRationale -> permissionDialog = ShowLocationState.Dialog.PermissionRationale
|
|
||||||
else -> permissionDialog = ShowLocationState.Dialog.PermissionDenied
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
isTrackMyLocation = false
|
isTrackMyLocation = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ShowLocationEvents.DismissDialog -> permissionDialog = ShowLocationState.Dialog.None
|
ShowLocationEvents.DismissDialog -> dialogState = LocationConstraintsDialogState.None
|
||||||
ShowLocationEvents.OpenAppSettings -> {
|
ShowLocationEvents.OpenAppSettings -> {
|
||||||
locationActions.openSettings()
|
locationActions.openAppSettings()
|
||||||
permissionDialog = ShowLocationState.Dialog.None
|
dialogState = LocationConstraintsDialogState.None
|
||||||
}
|
}
|
||||||
ShowLocationEvents.OpenLocationSettings -> {
|
ShowLocationEvents.OpenLocationSettings -> {
|
||||||
locationActions.openLocationSettings()
|
locationActions.openLocationSettings()
|
||||||
permissionDialog = ShowLocationState.Dialog.None
|
dialogState = LocationConstraintsDialogState.None
|
||||||
}
|
}
|
||||||
ShowLocationEvents.RequestPermissions -> permissionsState.eventSink(PermissionsEvents.RequestPermissions)
|
ShowLocationEvents.RequestPermissions -> permissionsState.eventSink(PermissionsEvents.RequestPermissions)
|
||||||
}
|
}
|
||||||
@@ -154,7 +150,7 @@ class ShowLocationPresenter(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ShowLocationState(
|
return ShowLocationState(
|
||||||
permissionDialog = permissionDialog,
|
dialogState = dialogState,
|
||||||
mode = mode,
|
mode = mode,
|
||||||
markers = markers,
|
markers = markers,
|
||||||
locationShares = locationShares,
|
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.Location
|
||||||
import io.element.android.features.location.api.ShowLocationMode
|
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.features.location.impl.common.ui.LocationMarkerData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
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.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.room.location.AssetType
|
import io.element.android.libraries.matrix.api.room.location.AssetType
|
||||||
|
|
||||||
data class ShowLocationState(
|
data class ShowLocationState(
|
||||||
val permissionDialog: Dialog,
|
val dialogState: LocationConstraintsDialogState,
|
||||||
val mode: ShowLocationMode,
|
val mode: ShowLocationMode,
|
||||||
val markers: List<LocationMarkerData>,
|
val markers: List<LocationMarkerData>,
|
||||||
val locationShares: List<LocationShareItem>,
|
val locationShares: List<LocationShareItem>,
|
||||||
@@ -25,15 +26,7 @@ data class ShowLocationState(
|
|||||||
val appName: String,
|
val appName: String,
|
||||||
val eventSink: (ShowLocationEvents) -> Unit,
|
val eventSink: (ShowLocationEvents) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val isSheetDraggable = locationShares.any { item -> item.isLive }
|
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(
|
data class LocationShareItem(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ package io.element.android.features.location.impl.show
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
import io.element.android.features.location.api.ShowLocationMode
|
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.features.location.impl.common.ui.LocationMarkerData
|
||||||
import io.element.android.libraries.designsystem.components.PinVariant
|
import io.element.android.libraries.designsystem.components.PinVariant
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
@@ -25,13 +26,13 @@ class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
|||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aShowLocationState(),
|
aShowLocationState(),
|
||||||
aShowLocationState(
|
aShowLocationState(
|
||||||
permissionDialog = ShowLocationState.Dialog.PermissionDenied,
|
constraintsDialogState = LocationConstraintsDialogState.PermissionDenied,
|
||||||
),
|
),
|
||||||
aShowLocationState(
|
aShowLocationState(
|
||||||
permissionDialog = ShowLocationState.Dialog.PermissionRationale,
|
constraintsDialogState = LocationConstraintsDialogState.PermissionRationale,
|
||||||
),
|
),
|
||||||
aShowLocationState(
|
aShowLocationState(
|
||||||
permissionDialog = ShowLocationState.Dialog.LocationServiceDisabled,
|
constraintsDialogState = LocationConstraintsDialogState.LocationServiceDisabled,
|
||||||
hasLocationPermission = true,
|
hasLocationPermission = true,
|
||||||
),
|
),
|
||||||
aShowLocationState(
|
aShowLocationState(
|
||||||
@@ -41,22 +42,11 @@ class ShowLocationStateProvider : PreviewParameterProvider<ShowLocationState> {
|
|||||||
hasLocationPermission = true,
|
hasLocationPermission = true,
|
||||||
isTrackMyLocation = 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(
|
fun aShowLocationState(
|
||||||
permissionDialog: ShowLocationState.Dialog = ShowLocationState.Dialog.None,
|
constraintsDialogState: LocationConstraintsDialogState = LocationConstraintsDialogState.None,
|
||||||
mode: ShowLocationMode = aStaticLocationMode(),
|
mode: ShowLocationMode = aStaticLocationMode(),
|
||||||
markers: List<LocationMarkerData>? = null,
|
markers: List<LocationMarkerData>? = null,
|
||||||
locationSharers: List<LocationShareItem>? = null,
|
locationSharers: List<LocationShareItem>? = null,
|
||||||
@@ -107,7 +97,7 @@ fun aShowLocationState(
|
|||||||
ShowLocationMode.Live -> emptyList()
|
ShowLocationMode.Live -> emptyList()
|
||||||
}
|
}
|
||||||
return ShowLocationState(
|
return ShowLocationState(
|
||||||
permissionDialog = permissionDialog,
|
dialogState = constraintsDialogState,
|
||||||
mode = mode,
|
mode = mode,
|
||||||
markers = effectiveMarkers,
|
markers = effectiveMarkers,
|
||||||
locationShares = effectiveLocationSharers,
|
locationShares = effectiveLocationSharers,
|
||||||
|
|||||||
@@ -27,10 +27,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.location.api.ShowLocationMode
|
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.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.LocationFloatingActionButton
|
||||||
import io.element.android.features.location.impl.common.ui.LocationPinMarkers
|
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.LocationShareRow
|
||||||
@@ -56,30 +54,21 @@ fun ShowLocationView(
|
|||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
when (state.permissionDialog) {
|
LocationConstraintsDialog(
|
||||||
ShowLocationState.Dialog.None -> Unit
|
state = state.dialogState,
|
||||||
ShowLocationState.Dialog.PermissionDenied -> PermissionDeniedDialog(
|
appName = state.appName,
|
||||||
onContinue = { state.eventSink(ShowLocationEvents.OpenAppSettings) },
|
onRequestPermissions = { state.eventSink(ShowLocationEvents.RequestPermissions) },
|
||||||
onDismiss = { state.eventSink(ShowLocationEvents.DismissDialog) },
|
onOpenAppSettings = { state.eventSink(ShowLocationEvents.OpenAppSettings) },
|
||||||
appName = state.appName,
|
onOpenLocationSettings = { state.eventSink(ShowLocationEvents.OpenLocationSettings) },
|
||||||
)
|
onDismiss = { state.eventSink(ShowLocationEvents.DismissDialog) },
|
||||||
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) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val initialPosition = when (val mode = state.mode) {
|
val initialPosition = when (val mode = state.mode) {
|
||||||
is ShowLocationMode.Static -> CameraPosition(
|
is ShowLocationMode.Static -> CameraPosition(
|
||||||
target = Position(latitude = mode.location.lat, longitude = mode.location.lon),
|
target = Position(latitude = mode.location.lat, longitude = mode.location.lon),
|
||||||
zoom = MapDefaults.DEFAULT_ZOOM
|
zoom = MapDefaults.DEFAULT_ZOOM
|
||||||
)
|
)
|
||||||
ShowLocationMode.Live -> MapDefaults.centerCameraPosition
|
ShowLocationMode.Live -> MapDefaults.defaultCameraPosition
|
||||||
}
|
}
|
||||||
val cameraState = rememberCameraState(firstPosition = initialPosition)
|
val cameraState = rememberCameraState(firstPosition = initialPosition)
|
||||||
val userLocationState = rememberUserLocationState(state.hasLocationPermission)
|
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
|
sharedLabel = label
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openSettings() {
|
override fun openAppSettings() {
|
||||||
openSettingsInvocationsCount++
|
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.actions.FakeLocationActions
|
||||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
||||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
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.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.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
import io.element.android.services.analytics.test.FakeAnalyticsService
|
import io.element.android.services.analytics.test.FakeAnalyticsService
|
||||||
@@ -42,6 +44,8 @@ class DefaultShareLocationEntryPointTest {
|
|||||||
messageComposerContext = FakeMessageComposerContext(),
|
messageComposerContext = FakeMessageComposerContext(),
|
||||||
locationActions = FakeLocationActions(),
|
locationActions = FakeLocationActions(),
|
||||||
buildMeta = aBuildMeta(),
|
buildMeta = aBuildMeta(),
|
||||||
|
featureFlagService = FakeFeatureFlagService(),
|
||||||
|
client = FakeMatrixClient(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
analyticsService = FakeAnalyticsService(),
|
analyticsService = FakeAnalyticsService(),
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ class ShareLocationPresenterTest {
|
|||||||
assertThat(myLocationState.hasLocationPermission).isFalse()
|
assertThat(myLocationState.hasLocationPermission).isFalse()
|
||||||
|
|
||||||
// Continue the dialog sends permission request to the permissions presenter
|
// 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)
|
assertThat(fakePermissionsPresenter.events.last()).isEqualTo(PermissionsEvents.RequestPermissions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,15 @@ import app.cash.molecule.moleculeFlow
|
|||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.location.api.Location
|
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.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.actions.FakeLocationActions
|
||||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
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.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.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.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -33,15 +36,25 @@ class ShowLocationPresenterTest {
|
|||||||
private val fakePermissionsPresenter = FakePermissionsPresenter()
|
private val fakePermissionsPresenter = FakePermissionsPresenter()
|
||||||
private val fakeLocationActions = FakeLocationActions()
|
private val fakeLocationActions = FakeLocationActions()
|
||||||
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
|
private val fakeBuildMeta = aBuildMeta(applicationName = "app name")
|
||||||
|
private val fakeDateFormatter = FakeDateFormatter()
|
||||||
private val location = Location(1.23, 4.56, 7.8f)
|
private val location = Location(1.23, 4.56, 7.8f)
|
||||||
private val presenter = ShowLocationPresenter(
|
|
||||||
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
|
private fun createShowLocationPresenter(
|
||||||
override fun create(permissions: List<String>): PermissionsPresenter = fakePermissionsPresenter
|
mode: ShowLocationMode = ShowLocationMode.Static(
|
||||||
},
|
location = location,
|
||||||
locationActions = fakeLocationActions,
|
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,
|
buildMeta = fakeBuildMeta,
|
||||||
location = location,
|
dateFormatter = fakeDateFormatter,
|
||||||
description = A_DESCRIPTION,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -54,11 +67,9 @@ class ShowLocationPresenterTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
createShowLocationPresenter().present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.location).isEqualTo(location)
|
|
||||||
assertThat(initialState.description).isEqualTo(A_DESCRIPTION)
|
|
||||||
assertThat(initialState.hasLocationPermission).isFalse()
|
assertThat(initialState.hasLocationPermission).isFalse()
|
||||||
assertThat(initialState.isTrackMyLocation).isFalse()
|
assertThat(initialState.isTrackMyLocation).isFalse()
|
||||||
}
|
}
|
||||||
@@ -74,11 +85,9 @@ class ShowLocationPresenterTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
createShowLocationPresenter().present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.location).isEqualTo(location)
|
|
||||||
assertThat(initialState.description).isEqualTo(A_DESCRIPTION)
|
|
||||||
assertThat(initialState.hasLocationPermission).isFalse()
|
assertThat(initialState.hasLocationPermission).isFalse()
|
||||||
assertThat(initialState.isTrackMyLocation).isFalse()
|
assertThat(initialState.isTrackMyLocation).isFalse()
|
||||||
}
|
}
|
||||||
@@ -89,11 +98,9 @@ class ShowLocationPresenterTest {
|
|||||||
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||||
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
createShowLocationPresenter().present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.location).isEqualTo(location)
|
|
||||||
assertThat(initialState.description).isEqualTo(A_DESCRIPTION)
|
|
||||||
assertThat(initialState.hasLocationPermission).isTrue()
|
assertThat(initialState.hasLocationPermission).isTrue()
|
||||||
assertThat(initialState.isTrackMyLocation).isFalse()
|
assertThat(initialState.isTrackMyLocation).isFalse()
|
||||||
}
|
}
|
||||||
@@ -104,11 +111,9 @@ class ShowLocationPresenterTest {
|
|||||||
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.SomeGranted))
|
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.SomeGranted))
|
||||||
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
createShowLocationPresenter().present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.location).isEqualTo(location)
|
|
||||||
assertThat(initialState.description).isEqualTo(A_DESCRIPTION)
|
|
||||||
assertThat(initialState.hasLocationPermission).isTrue()
|
assertThat(initialState.hasLocationPermission).isTrue()
|
||||||
assertThat(initialState.isTrackMyLocation).isFalse()
|
assertThat(initialState.isTrackMyLocation).isFalse()
|
||||||
}
|
}
|
||||||
@@ -117,13 +122,12 @@ class ShowLocationPresenterTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `uses action to share location`() = runTest {
|
fun `uses action to share location`() = runTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
createShowLocationPresenter().present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
initialState.eventSink(ShowLocationEvents.Share)
|
initialState.eventSink(ShowLocationEvents.Share(location))
|
||||||
|
|
||||||
assertThat(fakeLocationActions.sharedLocation).isEqualTo(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))
|
fakePermissionsPresenter.givenState(aPermissionsState(permissions = PermissionsState.Permissions.AllGranted))
|
||||||
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
createShowLocationPresenter().present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.hasLocationPermission).isTrue()
|
assertThat(initialState.hasLocationPermission).isTrue()
|
||||||
@@ -149,7 +153,7 @@ class ShowLocationPresenterTest {
|
|||||||
// Swipe the map to switch mode
|
// Swipe the map to switch mode
|
||||||
initialState.eventSink(ShowLocationEvents.TrackMyLocation(false))
|
initialState.eventSink(ShowLocationEvents.TrackMyLocation(false))
|
||||||
val trackLocationDisabledState = awaitItem()
|
val trackLocationDisabledState = awaitItem()
|
||||||
assertThat(trackLocationDisabledState.permissionDialog).isEqualTo(ShowLocationState.Dialog.None)
|
assertThat(trackLocationDisabledState.dialogState).isEqualTo(LocationConstraintsDialogState.None)
|
||||||
assertThat(trackLocationDisabledState.isTrackMyLocation).isFalse()
|
assertThat(trackLocationDisabledState.isTrackMyLocation).isFalse()
|
||||||
assertThat(trackLocationDisabledState.hasLocationPermission).isTrue()
|
assertThat(trackLocationDisabledState.hasLocationPermission).isTrue()
|
||||||
}
|
}
|
||||||
@@ -165,7 +169,7 @@ class ShowLocationPresenterTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
createShowLocationPresenter().present()
|
||||||
}.test {
|
}.test {
|
||||||
// Skip initial state
|
// Skip initial state
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
@@ -173,14 +177,14 @@ class ShowLocationPresenterTest {
|
|||||||
// Click on the button to switch mode
|
// Click on the button to switch mode
|
||||||
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
|
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
|
||||||
val trackLocationState = awaitItem()
|
val trackLocationState = awaitItem()
|
||||||
assertThat(trackLocationState.permissionDialog).isEqualTo(ShowLocationState.Dialog.PermissionRationale)
|
assertThat(trackLocationState.dialogState).isEqualTo(LocationConstraintsDialogState.PermissionRationale)
|
||||||
assertThat(trackLocationState.isTrackMyLocation).isFalse()
|
assertThat(trackLocationState.isTrackMyLocation).isFalse()
|
||||||
assertThat(trackLocationState.hasLocationPermission).isFalse()
|
assertThat(trackLocationState.hasLocationPermission).isFalse()
|
||||||
|
|
||||||
// Dismiss the dialog
|
// Dismiss the dialog
|
||||||
initialState.eventSink(ShowLocationEvents.DismissDialog)
|
initialState.eventSink(ShowLocationEvents.DismissDialog)
|
||||||
val dialogDismissedState = awaitItem()
|
val dialogDismissedState = awaitItem()
|
||||||
assertThat(dialogDismissedState.permissionDialog).isEqualTo(ShowLocationState.Dialog.None)
|
assertThat(dialogDismissedState.dialogState).isEqualTo(LocationConstraintsDialogState.None)
|
||||||
assertThat(dialogDismissedState.isTrackMyLocation).isFalse()
|
assertThat(dialogDismissedState.isTrackMyLocation).isFalse()
|
||||||
assertThat(dialogDismissedState.hasLocationPermission).isFalse()
|
assertThat(dialogDismissedState.hasLocationPermission).isFalse()
|
||||||
}
|
}
|
||||||
@@ -196,7 +200,7 @@ class ShowLocationPresenterTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
createShowLocationPresenter().present()
|
||||||
}.test {
|
}.test {
|
||||||
// Skip initial state
|
// Skip initial state
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
@@ -204,7 +208,7 @@ class ShowLocationPresenterTest {
|
|||||||
// Click on the button to switch mode
|
// Click on the button to switch mode
|
||||||
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
|
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
|
||||||
val trackLocationState = awaitItem()
|
val trackLocationState = awaitItem()
|
||||||
assertThat(trackLocationState.permissionDialog).isEqualTo(ShowLocationState.Dialog.PermissionRationale)
|
assertThat(trackLocationState.dialogState).isEqualTo(LocationConstraintsDialogState.PermissionRationale)
|
||||||
assertThat(trackLocationState.isTrackMyLocation).isFalse()
|
assertThat(trackLocationState.isTrackMyLocation).isFalse()
|
||||||
assertThat(trackLocationState.hasLocationPermission).isFalse()
|
assertThat(trackLocationState.hasLocationPermission).isFalse()
|
||||||
|
|
||||||
@@ -224,7 +228,7 @@ class ShowLocationPresenterTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
createShowLocationPresenter().present()
|
||||||
}.test {
|
}.test {
|
||||||
// Skip initial state
|
// Skip initial state
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
@@ -232,14 +236,14 @@ class ShowLocationPresenterTest {
|
|||||||
// Click on the button to switch mode
|
// Click on the button to switch mode
|
||||||
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
|
initialState.eventSink(ShowLocationEvents.TrackMyLocation(true))
|
||||||
val trackLocationState = awaitItem()
|
val trackLocationState = awaitItem()
|
||||||
assertThat(trackLocationState.permissionDialog).isEqualTo(ShowLocationState.Dialog.PermissionDenied)
|
assertThat(trackLocationState.dialogState).isEqualTo(LocationConstraintsDialogState.PermissionDenied)
|
||||||
assertThat(trackLocationState.isTrackMyLocation).isFalse()
|
assertThat(trackLocationState.isTrackMyLocation).isFalse()
|
||||||
assertThat(trackLocationState.hasLocationPermission).isFalse()
|
assertThat(trackLocationState.hasLocationPermission).isFalse()
|
||||||
|
|
||||||
// Dismiss the dialog
|
// Dismiss the dialog
|
||||||
initialState.eventSink(ShowLocationEvents.DismissDialog)
|
initialState.eventSink(ShowLocationEvents.DismissDialog)
|
||||||
val dialogDismissedState = awaitItem()
|
val dialogDismissedState = awaitItem()
|
||||||
assertThat(dialogDismissedState.permissionDialog).isEqualTo(ShowLocationState.Dialog.None)
|
assertThat(dialogDismissedState.dialogState).isEqualTo(LocationConstraintsDialogState.None)
|
||||||
assertThat(dialogDismissedState.isTrackMyLocation).isFalse()
|
assertThat(dialogDismissedState.isTrackMyLocation).isFalse()
|
||||||
assertThat(dialogDismissedState.hasLocationPermission).isFalse()
|
assertThat(dialogDismissedState.hasLocationPermission).isFalse()
|
||||||
}
|
}
|
||||||
@@ -255,7 +259,7 @@ class ShowLocationPresenterTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
createShowLocationPresenter().present()
|
||||||
}.test {
|
}.test {
|
||||||
// Skip initial state
|
// Skip initial state
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
@@ -267,7 +271,7 @@ class ShowLocationPresenterTest {
|
|||||||
dialogShownState.eventSink(ShowLocationEvents.OpenAppSettings)
|
dialogShownState.eventSink(ShowLocationEvents.OpenAppSettings)
|
||||||
val settingsOpenedState = awaitItem()
|
val settingsOpenedState = awaitItem()
|
||||||
|
|
||||||
assertThat(settingsOpenedState.permissionDialog).isEqualTo(ShowLocationState.Dialog.None)
|
assertThat(settingsOpenedState.dialogState).isEqualTo(LocationConstraintsDialogState.None)
|
||||||
assertThat(fakeLocationActions.openSettingsInvocationsCount).isEqualTo(1)
|
assertThat(fakeLocationActions.openSettingsInvocationsCount).isEqualTo(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,14 +279,53 @@ class ShowLocationPresenterTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `application name is in state`() = runTest {
|
fun `application name is in state`() = runTest {
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
createShowLocationPresenter().present()
|
||||||
}.test {
|
}.test {
|
||||||
val initialState = awaitItem()
|
val initialState = awaitItem()
|
||||||
assertThat(initialState.appName).isEqualTo("app name")
|
assertThat(initialState.appName).isEqualTo("app name")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
@Test
|
||||||
private const val A_DESCRIPTION = "My happy place"
|
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.onNodeWithTag
|
||||||
import androidx.compose.ui.test.performClick
|
import androidx.compose.ui.test.performClick
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
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.testtags.TestTags
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
@@ -58,7 +60,8 @@ class ShowLocationViewTest {
|
|||||||
)
|
)
|
||||||
val shareContentDescription = rule.activity.getString(CommonStrings.action_share)
|
val shareContentDescription = rule.activity.getString(CommonStrings.action_share)
|
||||||
rule.onNodeWithContentDescription(shareContentDescription).performClick()
|
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
|
@Test
|
||||||
@@ -79,7 +82,7 @@ class ShowLocationViewTest {
|
|||||||
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
|
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
|
||||||
rule.setShowLocationView(
|
rule.setShowLocationView(
|
||||||
aShowLocationState(
|
aShowLocationState(
|
||||||
permissionDialog = ShowLocationState.Dialog.PermissionDenied,
|
constraintsDialogState = LocationConstraintsDialogState.PermissionDenied,
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
),
|
),
|
||||||
onBackClick = EnsureNeverCalled(),
|
onBackClick = EnsureNeverCalled(),
|
||||||
@@ -93,7 +96,7 @@ class ShowLocationViewTest {
|
|||||||
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
|
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
|
||||||
rule.setShowLocationView(
|
rule.setShowLocationView(
|
||||||
aShowLocationState(
|
aShowLocationState(
|
||||||
permissionDialog = ShowLocationState.Dialog.PermissionDenied,
|
constraintsDialogState = LocationConstraintsDialogState.PermissionDenied,
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
),
|
),
|
||||||
onBackClick = EnsureNeverCalled(),
|
onBackClick = EnsureNeverCalled(),
|
||||||
@@ -107,7 +110,7 @@ class ShowLocationViewTest {
|
|||||||
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
|
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
|
||||||
rule.setShowLocationView(
|
rule.setShowLocationView(
|
||||||
aShowLocationState(
|
aShowLocationState(
|
||||||
permissionDialog = ShowLocationState.Dialog.PermissionRationale,
|
constraintsDialogState = LocationConstraintsDialogState.PermissionRationale,
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
),
|
),
|
||||||
onBackClick = EnsureNeverCalled(),
|
onBackClick = EnsureNeverCalled(),
|
||||||
@@ -121,7 +124,7 @@ class ShowLocationViewTest {
|
|||||||
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
|
val eventsRecorder = EventsRecorder<ShowLocationEvents>()
|
||||||
rule.setShowLocationView(
|
rule.setShowLocationView(
|
||||||
aShowLocationState(
|
aShowLocationState(
|
||||||
permissionDialog = ShowLocationState.Dialog.PermissionRationale,
|
constraintsDialogState = LocationConstraintsDialogState.PermissionRationale,
|
||||||
eventSink = eventsRecorder
|
eventSink = eventsRecorder
|
||||||
),
|
),
|
||||||
onBackClick = EnsureNeverCalled(),
|
onBackClick = EnsureNeverCalled(),
|
||||||
|
|||||||
Reference in New Issue
Block a user