From 19194acf202d14cb3845290959a9886f1b6316d4 Mon Sep 17 00:00:00 2001 From: Ellen Poe Date: Fri, 3 Oct 2025 10:48:01 -0700 Subject: [PATCH] feat: prompt for location permission on startup --- .../java/earth/maps/cardinal/MainActivity.kt | 26 ++++++++++- .../cardinal/data/AppPreferenceRepository.kt | 10 +++++ .../maps/cardinal/data/AppPreferences.kt | 19 ++++++++ .../earth/maps/cardinal/ui/core/AppContent.kt | 43 +++++++++++++++++++ .../app/src/main/res/values/strings.xml | 6 +++ 5 files changed, 103 insertions(+), 1 deletion(-) diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/MainActivity.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/MainActivity.kt index 7c654de..f022292 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/MainActivity.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/MainActivity.kt @@ -91,6 +91,7 @@ class MainActivity : ComponentActivity() { private var hasLocationPermission by mutableStateOf(false) private var hasNotificationPermission by mutableStateOf(false) private var deepLinkDestination by mutableStateOf(null) + private var showLocationPermissionDialog by mutableStateOf(false) companion object { private const val LOCATION_PERMISSION_REQUEST_CODE = 1001 @@ -157,6 +158,14 @@ class MainActivity : ComponentActivity() { hasLocationPermission = isGranted if (isGranted) { Log.d(TAG, "Location permission granted") + // Request fresh location and animate camera to user's location + lifecycleScope.launch { + locationRepository.getCurrentLocation(this@MainActivity)?.let { location -> + Log.d(TAG, "Got location: ${location.latitude}, ${location.longitude}") + // The MapViewModel will handle the camera animation through its + // handlePermissionStateChange method + } + } } else { Log.d(TAG, "Location permission denied") } @@ -193,6 +202,11 @@ class MainActivity : ComponentActivity() { hasNotificationPermission = checkNotificationPermission() hasLocationPermission = checkLocationPermission() + // Check if we should show the location permission dialog on first startup + if (!appPreferenceRepository.hasPromptedLocation.value && !hasLocationPermission) { + showLocationPermissionDialog = true + } + CoroutineScope(Dispatchers.IO).launch { migrationHelper.migratePlacesToSavedPlaces() savedListRepository.cleanupUnparentedElements() @@ -233,7 +247,17 @@ class MainActivity : ComponentActivity() { routeRepository = routeRepository, appPreferenceRepository = appPreferenceRepository, onRequestNotificationPermission = { requestNotificationPermission() }, - hasNotificationPermission = hasNotificationPermission + hasNotificationPermission = hasNotificationPermission, + showLocationPermissionDialog = showLocationPermissionDialog, + onDismissLocationDialog = { + showLocationPermissionDialog = false + appPreferenceRepository.setHasPromptedLocation(true) + }, + onAcceptLocationDialog = { + showLocationPermissionDialog = false + appPreferenceRepository.setHasPromptedLocation(true) + requestLocationPermission() + } ) } } diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/AppPreferenceRepository.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/AppPreferenceRepository.kt index ed82fd7..a9ad61a 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/AppPreferenceRepository.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/AppPreferenceRepository.kt @@ -73,6 +73,9 @@ class AppPreferenceRepository @Inject constructor( private val _lastRoutingMode = MutableStateFlow(appPreferences.loadLastRoutingMode()) val lastRoutingMode: StateFlow = _lastRoutingMode.asStateFlow() + private val _hasPromptedLocation = MutableStateFlow(appPreferences.loadHasPromptedLocation()) + val hasPromptedLocation: StateFlow = _hasPromptedLocation.asStateFlow() + // Pelias API configuration private val _peliasApiConfig = MutableStateFlow( ApiConfiguration( @@ -212,6 +215,13 @@ class AppPreferenceRepository @Inject constructor( } } + fun setHasPromptedLocation(hasPrompted: Boolean) { + _hasPromptedLocation.value = hasPrompted + viewModelScope.launch { + appPreferences.saveHasPromptedLocation(hasPrompted) + } + } + private fun loadApiConfigurations() { // Load Pelias configuration val peliasBaseUrl = appPreferences.loadPeliasBaseUrl() diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/AppPreferences.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/AppPreferences.kt index c2ca12d..66fa70b 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/data/AppPreferences.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/data/AppPreferences.kt @@ -45,6 +45,8 @@ class AppPreferences(private val context: Context) { private const val KEY_LAST_ROUTING_MODE = "last_routing_mode" private const val KEY_USE_24_HOUR_FORMAT = "use_24_hour_format" + + private const val KEY_HAS_PROMPTED_LOCATION = "has_prompted_location" // API configuration keys private const val KEY_PELIAS_BASE_URL = "pelias_base_url" @@ -287,6 +289,23 @@ class AppPreferences(private val context: Context) { return prefs.getBoolean(KEY_USE_24_HOUR_FORMAT, systemDefault) } + /** + * Saves whether the user has been prompted for location permission. + */ + fun saveHasPromptedLocation(hasPrompted: Boolean) { + prefs.edit { + putBoolean(KEY_HAS_PROMPTED_LOCATION, hasPrompted) + } + } + + /** + * Loads whether the user has been prompted for location permission. + * Returns false as default (user has not been prompted yet). + */ + fun loadHasPromptedLocation(): Boolean { + return prefs.getBoolean(KEY_HAS_PROMPTED_LOCATION, false) + } + /** * Gets the default distance unit based on the system locale. * Returns imperial for countries that use imperial system (US, Liberia, Myanmar), diff --git a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/AppContent.kt b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/AppContent.kt index 52feab1..ee9c1cd 100644 --- a/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/AppContent.kt +++ b/cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/AppContent.kt @@ -150,6 +150,9 @@ fun AppContent( hasNotificationPermission: Boolean, routeRepository: RouteRepository, appPreferenceRepository: AppPreferenceRepository, + showLocationPermissionDialog: Boolean = false, + onDismissLocationDialog: () -> Unit = {}, + onAcceptLocationDialog: () -> Unit = {}, state: AppContentState = rememberAppContentState(), ) { @@ -380,6 +383,19 @@ fun AppContent( CardinalToolbar(navController, onSearchDoublePress = { homeViewModel.expandSearch() }) } } + + // Show location permission dialog on first startup + if (showLocationPermissionDialog) { + LocationPermissionDialog( + onDismiss = onDismissLocationDialog, + onAccept = { + // Mark that we have a pending location request so the camera will animate + // when permission is granted + mapViewModel.markLocationRequestPending() + onAcceptLocationDialog() + } + ) + } } @OptIn(ExperimentalMaterial3Api::class) @@ -1243,3 +1259,30 @@ fun BirdSettingsFab(navController: NavController) { } } } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LocationPermissionDialog( + onDismiss: () -> Unit, + onAccept: () -> Unit +) { + androidx.compose.material3.AlertDialog( + onDismissRequest = onDismiss, + title = { + androidx.compose.material3.Text(text = stringResource(string.enable_location_title)) + }, + text = { + androidx.compose.material3.Text(text = stringResource(string.enable_location_message)) + }, + confirmButton = { + androidx.compose.material3.TextButton(onClick = onAccept) { + androidx.compose.material3.Text(text = stringResource(string.allow)) + } + }, + dismissButton = { + androidx.compose.material3.TextButton(onClick = onDismiss) { + androidx.compose.material3.Text(text = stringResource(string.not_now)) + } + } + ) +} diff --git a/cardinal-android/app/src/main/res/values/strings.xml b/cardinal-android/app/src/main/res/values/strings.xml index 6fef334..fce9953 100644 --- a/cardinal-android/app/src/main/res/values/strings.xml +++ b/cardinal-android/app/src/main/res/values/strings.xml @@ -248,4 +248,10 @@ Zoom out Searching… Remove recent search + + + Enable Location? + Allow Cardinal Maps to access your location to show you where you are on the map and provide a better experience. + Allow + Not Now -- GitLab