Loading cardinal-android/app/src/main/java/earth/maps/cardinal/MainActivity.kt +25 −1 Original line number Diff line number Diff line Loading @@ -91,6 +91,7 @@ class MainActivity : ComponentActivity() { private var hasLocationPermission by mutableStateOf(false) private var hasNotificationPermission by mutableStateOf(false) private var deepLinkDestination by mutableStateOf<String?>(null) private var showLocationPermissionDialog by mutableStateOf(false) companion object { private const val LOCATION_PERMISSION_REQUEST_CODE = 1001 Loading Loading @@ -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") } Loading Loading @@ -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() Loading Loading @@ -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() } ) } } Loading cardinal-android/app/src/main/java/earth/maps/cardinal/data/AppPreferenceRepository.kt +10 −0 Original line number Diff line number Diff line Loading @@ -73,6 +73,9 @@ class AppPreferenceRepository @Inject constructor( private val _lastRoutingMode = MutableStateFlow(appPreferences.loadLastRoutingMode()) val lastRoutingMode: StateFlow<String> = _lastRoutingMode.asStateFlow() private val _hasPromptedLocation = MutableStateFlow(appPreferences.loadHasPromptedLocation()) val hasPromptedLocation: StateFlow<Boolean> = _hasPromptedLocation.asStateFlow() // Pelias API configuration private val _peliasApiConfig = MutableStateFlow( ApiConfiguration( Loading Loading @@ -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() Loading cardinal-android/app/src/main/java/earth/maps/cardinal/data/AppPreferences.kt +19 −0 Original line number Diff line number Diff line Loading @@ -46,6 +46,8 @@ class AppPreferences(private val context: Context) { 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" private const val KEY_PELIAS_API_KEY = "pelias_api_key" Loading Loading @@ -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), Loading cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/AppContent.kt +43 −0 Original line number Diff line number Diff line Loading @@ -150,6 +150,9 @@ fun AppContent( hasNotificationPermission: Boolean, routeRepository: RouteRepository, appPreferenceRepository: AppPreferenceRepository, showLocationPermissionDialog: Boolean = false, onDismissLocationDialog: () -> Unit = {}, onAcceptLocationDialog: () -> Unit = {}, state: AppContentState = rememberAppContentState(), ) { Loading Loading @@ -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) Loading Loading @@ -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)) } } ) } cardinal-android/app/src/main/res/values/strings.xml +6 −0 Original line number Diff line number Diff line Loading @@ -248,4 +248,10 @@ <string name="zoom_out">Zoom out</string> <string name="searching">Searching…</string> <string name="remove_recent_search">Remove recent search</string> <!-- Location Permission Dialog --> <string name="enable_location_title">Enable Location?</string> <string name="enable_location_message">Allow Cardinal Maps to access your location to show you where you are on the map and provide a better experience.</string> <string name="allow">Allow</string> <string name="not_now">Not Now</string> </resources> Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/MainActivity.kt +25 −1 Original line number Diff line number Diff line Loading @@ -91,6 +91,7 @@ class MainActivity : ComponentActivity() { private var hasLocationPermission by mutableStateOf(false) private var hasNotificationPermission by mutableStateOf(false) private var deepLinkDestination by mutableStateOf<String?>(null) private var showLocationPermissionDialog by mutableStateOf(false) companion object { private const val LOCATION_PERMISSION_REQUEST_CODE = 1001 Loading Loading @@ -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") } Loading Loading @@ -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() Loading Loading @@ -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() } ) } } Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/data/AppPreferenceRepository.kt +10 −0 Original line number Diff line number Diff line Loading @@ -73,6 +73,9 @@ class AppPreferenceRepository @Inject constructor( private val _lastRoutingMode = MutableStateFlow(appPreferences.loadLastRoutingMode()) val lastRoutingMode: StateFlow<String> = _lastRoutingMode.asStateFlow() private val _hasPromptedLocation = MutableStateFlow(appPreferences.loadHasPromptedLocation()) val hasPromptedLocation: StateFlow<Boolean> = _hasPromptedLocation.asStateFlow() // Pelias API configuration private val _peliasApiConfig = MutableStateFlow( ApiConfiguration( Loading Loading @@ -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() Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/data/AppPreferences.kt +19 −0 Original line number Diff line number Diff line Loading @@ -46,6 +46,8 @@ class AppPreferences(private val context: Context) { 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" private const val KEY_PELIAS_API_KEY = "pelias_api_key" Loading Loading @@ -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), Loading
cardinal-android/app/src/main/java/earth/maps/cardinal/ui/core/AppContent.kt +43 −0 Original line number Diff line number Diff line Loading @@ -150,6 +150,9 @@ fun AppContent( hasNotificationPermission: Boolean, routeRepository: RouteRepository, appPreferenceRepository: AppPreferenceRepository, showLocationPermissionDialog: Boolean = false, onDismissLocationDialog: () -> Unit = {}, onAcceptLocationDialog: () -> Unit = {}, state: AppContentState = rememberAppContentState(), ) { Loading Loading @@ -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) Loading Loading @@ -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)) } } ) }
cardinal-android/app/src/main/res/values/strings.xml +6 −0 Original line number Diff line number Diff line Loading @@ -248,4 +248,10 @@ <string name="zoom_out">Zoom out</string> <string name="searching">Searching…</string> <string name="remove_recent_search">Remove recent search</string> <!-- Location Permission Dialog --> <string name="enable_location_title">Enable Location?</string> <string name="enable_location_message">Allow Cardinal Maps to access your location to show you where you are on the map and provide a better experience.</string> <string name="allow">Allow</string> <string name="not_now">Not Now</string> </resources>