From e2fcc7bfefded4117afbc7e936f12374b2b9cec7 Mon Sep 17 00:00:00 2001 From: Akshay Nandwana Date: Wed, 13 Nov 2024 19:10:12 +0530 Subject: [PATCH] [Capture location] Prompt for location permissions when capture location task is opened (#2818) * dialog ui added * show dialog for the first time when enter * added permission check * string and view fix --- .../ground/system/PermissionsManager.kt | 2 +- .../CaptureLocationTaskMapFragment.kt | 44 ++++++++++- .../location/CaptureLocationTaskViewModel.kt | 18 +++-- .../location/LocationPermissionDialog.kt | 73 +++++++++++++++++++ ground/src/main/res/values/strings.xml | 4 + 5 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/LocationPermissionDialog.kt diff --git a/ground/src/main/java/com/google/android/ground/system/PermissionsManager.kt b/ground/src/main/java/com/google/android/ground/system/PermissionsManager.kt index a2be631959..7ed2d712a3 100644 --- a/ground/src/main/java/com/google/android/ground/system/PermissionsManager.kt +++ b/ground/src/main/java/com/google/android/ground/system/PermissionsManager.kt @@ -60,7 +60,7 @@ constructor( false } - /** Returns `true` iff the app has been granted the specified permission. */ + /** Returns `true` if the app has been granted the specified permission. */ fun isGranted(permission: String): Boolean = checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskMapFragment.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskMapFragment.kt index 474a00e138..1f89e4921b 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskMapFragment.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskMapFragment.kt @@ -19,10 +19,15 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.lifecycleScope import com.google.android.ground.ui.common.AbstractMapFragmentWithControls import com.google.android.ground.ui.common.BaseMapViewModel import com.google.android.ground.ui.map.MapFragment +import com.google.android.ground.ui.theme.AppTheme import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.launch @@ -57,9 +62,46 @@ class CaptureLocationTaskMapFragment @Inject constructor() : AbstractMapFragment return root } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + lifecycleScope.launch { + viewModel.enableLocationLockFlow.collect { + if (it == LocationLockEnabledState.NEEDS_ENABLE) { + showLocationPermissionDialog() + } + } + } + } + override fun onMapReady(map: MapFragment) { super.onMapReady(map) - binding.locationLockBtn.isClickable = false viewLifecycleOwner.lifecycleScope.launch { viewModel.onMapReady(mapViewModel) } } + + @Suppress("LabeledExpression") + private fun showLocationPermissionDialog() { + val dialogComposeView = + ComposeView(requireContext()).apply { + setContent { + val openAlertDialog = remember { mutableStateOf(true) } + when { + openAlertDialog.value -> { + AppTheme { + LocationPermissionDialog( + onCancel = { + binding.locationLockBtn.isClickable = false + openAlertDialog.value = false + }, + onDismiss = { openAlertDialog.value = false }, + ) + } + } + } + + DisposableEffect(Unit) { onDispose { (parent as? ViewGroup)?.removeView(this@apply) } } + } + } + + (view as ViewGroup).addView(dialogComposeView) + } } diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskViewModel.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskViewModel.kt index 73f10d8227..6513a87330 100644 --- a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskViewModel.kt +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/CaptureLocationTaskViewModel.kt @@ -23,10 +23,11 @@ import com.google.android.ground.ui.common.BaseMapViewModel import com.google.android.ground.ui.datacollection.tasks.AbstractTaskViewModel import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch /** Location lock states relevant for attempting to enable it or not. */ -private enum class LocationLockEnabledState { +enum class LocationLockEnabledState { /** The default, unknown state. */ UNKNOWN, @@ -44,7 +45,8 @@ class CaptureLocationTaskViewModel @Inject constructor() : AbstractTaskViewModel private val lastLocation = MutableStateFlow(null) /** Allows control for triggering the location lock programmatically. */ - private val enableLocationLockFlow = MutableStateFlow(LocationLockEnabledState.UNKNOWN) + private val _enableLocationLockFlow = MutableStateFlow(LocationLockEnabledState.UNKNOWN) + val enableLocationLockFlow = _enableLocationLockFlow.asStateFlow() suspend fun updateLocation(location: Location) { lastLocation.emit(location.toCaptureLocationResult()) @@ -52,15 +54,15 @@ class CaptureLocationTaskViewModel @Inject constructor() : AbstractTaskViewModel fun updateResponse() { if (lastLocation.value == null) { - viewModelScope.launch { enableLocationLockFlow.emit(LocationLockEnabledState.ENABLE) } + viewModelScope.launch { _enableLocationLockFlow.emit(LocationLockEnabledState.ENABLE) } } else { setValue(lastLocation.value) } } fun enableLocationLock() { - if (enableLocationLockFlow.value == LocationLockEnabledState.NEEDS_ENABLE) { - viewModelScope.launch { enableLocationLockFlow.emit(LocationLockEnabledState.ENABLE) } + if (_enableLocationLockFlow.value == LocationLockEnabledState.NEEDS_ENABLE) { + viewModelScope.launch { _enableLocationLockFlow.emit(LocationLockEnabledState.ENABLE) } } } @@ -74,12 +76,12 @@ class CaptureLocationTaskViewModel @Inject constructor() : AbstractTaskViewModel // Otherwise, wait to enable location lock until later. LocationLockEnabledState.NEEDS_ENABLE } - enableLocationLockFlow.value = locationLockEnabledState - enableLocationLockFlow.collect { + _enableLocationLockFlow.value = locationLockEnabledState + _enableLocationLockFlow.collect { if (it == LocationLockEnabledState.ENABLE) { // No-op if permission is already granted and location updates are enabled. mapViewModel.enableLocationLockAndGetUpdates() - enableLocationLockFlow.value = LocationLockEnabledState.ALREADY_ENABLED + _enableLocationLockFlow.value = LocationLockEnabledState.ALREADY_ENABLED } } } diff --git a/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/LocationPermissionDialog.kt b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/LocationPermissionDialog.kt new file mode 100644 index 0000000000..ab67663c4a --- /dev/null +++ b/ground/src/main/java/com/google/android/ground/ui/datacollection/tasks/location/LocationPermissionDialog.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.ground.ui.datacollection.tasks.location + +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import com.google.android.ground.R +import com.google.android.ground.ui.theme.AppTheme + +@Composable +fun LocationPermissionDialog(onDismiss: () -> Unit, onCancel: () -> Unit) { + val context = LocalContext.current + AlertDialog( + containerColor = MaterialTheme.colorScheme.surface, + onDismissRequest = { onDismiss() }, + title = { + Text( + stringResource(R.string.allow_location_title), + color = MaterialTheme.colorScheme.onSurface, + ) + }, + text = { + Text( + stringResource(R.string.allow_location_description), + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + dismissButton = { + TextButton(onClick = { onCancel() }) { Text(text = stringResource(R.string.cancel)) } + }, + confirmButton = { + TextButton( + onClick = { + // Open the app settings + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + val uri = Uri.fromParts("package", context.packageName, null) + intent.data = uri + context.startActivity(intent) + } + ) { + Text(text = stringResource(R.string.allow_location_confirmation)) + } + }, + ) +} + +@Composable +@Preview +fun PreviewUserDetailsDialog() { + AppTheme { LocationPermissionDialog({}, {}) } +} diff --git a/ground/src/main/res/values/strings.xml b/ground/src/main/res/values/strings.xml index 71a45c1f01..3c38b35dfb 100644 --- a/ground/src/main/res/values/strings.xml +++ b/ground/src/main/res/values/strings.xml @@ -188,4 +188,8 @@ Data submitted image Data collection complete! Your data has been saved and will be automatically synced when you’re online. + + Allow location + Allow location sharing + If you don’t allow Ground to access this device’s location, you won’t be able to continue collecting data for this site.