diff --git a/places-compose-demo/src/main/AndroidManifest.xml b/places-compose-demo/src/main/AndroidManifest.xml index 31bb061..4c5ab86 100644 --- a/places-compose-demo/src/main/AndroidManifest.xml +++ b/places-compose-demo/src/main/AndroidManifest.xml @@ -36,7 +36,6 @@ diff --git a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/MainActivity.kt b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/MainActivity.kt index 0440b71..569afc4 100644 --- a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/MainActivity.kt +++ b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/MainActivity.kt @@ -20,6 +20,7 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -27,7 +28,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import com.google.android.libraries.places.compose.demo.ui.theme.AndroidPlacesComposeDemoTheme import android.content.Intent -import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button @@ -60,7 +60,7 @@ class MainActivity : ComponentActivity() { setContent { AndroidPlacesComposeDemoTheme { Surface( - modifier = Modifier.fillMaxSize().systemBarsPadding(), + modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) { ActivityButtons() @@ -70,11 +70,17 @@ class MainActivity : ComponentActivity() { } } +data class ActivityItem( + val activityClass: Class<*>, + val titleRes: Int, + val descRes: Int, +) + val activities = listOf( - AutocompleteActivity::class.java to R.string.autocomplete_button, - LandmarkSelectionActivity::class.java to R.string.landmark_selection_button, - AddressCompletionActivity::class.java to R.string.address_validation_button, - PlacesAutocompleteMinimalActivity::class.java to R.string.minimal_autocomplete_button + ActivityItem(AutocompleteActivity::class.java, R.string.autocomplete_button, R.string.autocomplete_button_desc), + ActivityItem(LandmarkSelectionActivity::class.java, R.string.landmark_selection_button, R.string.landmark_selection_button_desc), + ActivityItem(AddressCompletionActivity::class.java, R.string.address_validation_button, R.string.address_validation_button_desc), + ActivityItem(PlacesAutocompleteMinimalActivity::class.java, R.string.minimal_autocomplete_button, R.string.minimal_autocomplete_button_desc), ) @OptIn(ExperimentalMaterial3Api::class) @@ -98,15 +104,33 @@ fun ActivityButtons() { Column( Modifier .padding(paddingValues) - .padding(top = 16.dp) .fillMaxSize() + .padding(horizontal = 24.dp, vertical = 20.dp) .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(10.dp) + verticalArrangement = Arrangement.spacedBy(14.dp) ) { - for ((activityClass, buttonTextResId) in activities) { - Button(onClick = { context.startActivity(Intent(context, activityClass)) }) { - Text(stringResource(buttonTextResId)) + for (item in activities) { + Button( + onClick = { context.startActivity(Intent(context, item.activityClass)) }, + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(item.titleRes), + style = MaterialTheme.typography.titleMedium + ) + Text( + text = stringResource(item.descRes), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.85f) + ) + } } } } diff --git a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/data/repositories/GeocoderRepository.kt b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/data/repositories/GeocoderRepository.kt index f650b1e..a4fe459 100644 --- a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/data/repositories/GeocoderRepository.kt +++ b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/data/repositories/GeocoderRepository.kt @@ -15,6 +15,7 @@ package com.google.android.libraries.places.compose.demo.data.repositories import android.content.Context import android.net.Uri +import android.util.Log import com.android.volley.Request import com.android.volley.toolbox.StringRequest import com.android.volley.toolbox.Volley @@ -67,26 +68,42 @@ class GeocoderRepository( suspend fun reverseGeocode( latLng: LatLng, includeAddressDescriptors: Boolean = true - ): ReverseGeocodingResponse { + ): ReverseGeocodingResponse? { val url = buildRequestUrl(latLng, includeAddressDescriptors) - return suspendCancellableCoroutine { cont -> - val queue = Volley.newRequestQueue(context) - val stringRequest = - StringRequest( - Request.Method.GET, - url, - { response -> cont.resume(parseFullGeocodeResponse(response)) }, - { error -> cont.resumeWithException(RuntimeException(error.localizedMessage)) }, - ) - queue.add(stringRequest) + return try { + suspendCancellableCoroutine { cont -> + val queue = Volley.newRequestQueue(context) + val stringRequest = + StringRequest( + Request.Method.GET, + url, + { response -> + try { + cont.resume(parseFullGeocodeResponse(response)) + } catch (e: Exception) { + Log.e("GeocoderRepository", "Error parsing geocoder response", e) + cont.resume(null) + } + }, + { error -> + Log.e("GeocoderRepository", "Volley reverse geocode error: ${error.networkResponse?.statusCode} - ${error.localizedMessage}", error) + cont.resume(null) + }, + ) + queue.add(stringRequest) + } + } catch (e: Exception) { + Log.e("GeocoderRepository", "Reverse geocode exception", e) + null } } private fun parseFullGeocodeResponse(response: String): ReverseGeocodingResponse { val fullResult = gson.fromJson(response, ReverseGeocodingResponse::class.java) - // TODO: handle the status -- consider a monad pattern (Result?) - + if (fullResult.status != "OK" && fullResult.status != "ZERO_RESULTS") { + Log.w("GeocoderRepository", "Geocode API warning: status=${fullResult.status}, error_message=${fullResult.errorMessage}") + } return fullResult } diff --git a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/addresscompletion/AddressCompletionScreen.kt b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/addresscompletion/AddressCompletionScreen.kt index 6729aba..986c36a 100644 --- a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/addresscompletion/AddressCompletionScreen.kt +++ b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/addresscompletion/AddressCompletionScreen.kt @@ -24,11 +24,19 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon import com.google.android.gms.maps.model.LatLng import com.google.android.libraries.places.compose.autocomplete.components.PlacesAutocompleteTextField import com.google.android.libraries.places.compose.autocomplete.models.AutocompletePlace @@ -150,6 +158,42 @@ private fun AddressEntryForm( .padding(top = 16.dp) .verticalScroll(rememberScrollState()), ) { + if (addressEntry.nearbyObjects.isEmpty()) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + Column( + modifier = Modifier.padding(14.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Text( + text = stringResource(R.string.address_descriptors_info_title), + style = MaterialTheme.typography.titleMedium + ) + } + Text( + text = stringResource(R.string.address_descriptors_unavailable_message), + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + AddressDisplay( address = addressEntry.displayAddress, modifier = Modifier.fillMaxWidth(), diff --git a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/common/CommonScreen.kt b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/common/CommonScreen.kt index 70c4fb8..c427dc6 100644 --- a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/common/CommonScreen.kt +++ b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/common/CommonScreen.kt @@ -3,7 +3,7 @@ package com.google.android.libraries.places.compose.demo.presentation.common import androidx.annotation.StringRes import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api @@ -33,6 +33,10 @@ import com.google.android.libraries.places.compose.demo.presentation.landmark.Ge import com.google.android.libraries.places.compose.demo.ui.theme.AndroidPlacesComposeDemoTheme import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.material3.SnackbarDuration +import androidx.compose.runtime.rememberCoroutineScope +import kotlinx.coroutines.launch + @OptIn(ExperimentalMaterial3Api::class) @Composable fun CommonScreen( @@ -51,57 +55,75 @@ fun CommonScreen( getUnitsConverter(country) } + val coroutineScope = rememberCoroutineScope() + fun onEvent(event: CommonEvent) { commonViewModel.onEvent(event) } AndroidPlacesComposeDemoTheme { - GetLocationPermission { - CompositionLocalProvider(LocalUnitsConverter provides unitsConverter) { - Scaffold( - modifier = Modifier.fillMaxSize().systemBarsPadding(), - topBar = { - TopAppBar( - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - titleContentColor = MaterialTheme.colorScheme.primary, - actionIconContentColor = MaterialTheme.colorScheme.primary, - navigationIconContentColor = MaterialTheme.colorScheme.primary, - ), - title = { Text(stringResource(titleId)) }, - navigationIcon = { - IconButton(onClick = { onNavigateUp() }) { - Icon( - imageVector = Icons.AutoMirrored.Default.ArrowBack, - contentDescription = stringResource(R.string.back) - ) - } - }, - actions = { - SelectableButton( - buttonState = commonViewState.buttonStates.currentLocation, - onClick = { onEvent(CommonEvent.OnUseSystemLocation) }, - iconId = R.drawable.baseline_my_location_24, - contentDescription = R.string.fill_address_from_current_location + CompositionLocalProvider(LocalUnitsConverter provides unitsConverter) { + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + actionIconContentColor = MaterialTheme.colorScheme.primary, + navigationIconContentColor = MaterialTheme.colorScheme.primary, + ), + title = { Text(stringResource(titleId)) }, + navigationIcon = { + IconButton(onClick = { onNavigateUp() }) { + Icon( + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = stringResource(R.string.back) ) + } + }, + actions = { + SelectableButton( + buttonState = commonViewState.buttonStates.currentLocation, + onClick = { onEvent(CommonEvent.OnUseSystemLocation) }, + iconId = R.drawable.baseline_my_location_24, + contentDescription = R.string.fill_address_from_current_location + ) - NextLocationButton( - isSelected = commonViewState.buttonStates.mockLocation == ButtonState.SELECTED - ) { - onEvent(CommonEvent.OnNextMockLocation) + NextLocationButton( + isSelected = commonViewState.buttonStates.mockLocation == ButtonState.SELECTED + ) { + onEvent(CommonEvent.OnNextMockLocation) + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = "Location: ${commonViewState.locationLabel ?: "Mock Location"}", + duration = SnackbarDuration.Short + ) } - - SelectableButton( - buttonState = commonViewState.buttonStates.map, - iconId = R.drawable.baseline_map_24, - contentDescription = R.string.toggle_map, - onClick = { onEvent(CommonEvent.OnToggleMap) } - ) } - ) - }, - snackbarHost = { SnackbarHost(snackbarHostState) }, - ) { paddingValues -> + + SelectableButton( + buttonState = commonViewState.buttonStates.map, + iconId = R.drawable.baseline_map_24, + contentDescription = R.string.toggle_map, + onClick = { onEvent(CommonEvent.OnToggleMap) } + ) + } + ) + }, + snackbarHost = { SnackbarHost(snackbarHostState) }, + ) { paddingValues -> + GetLocationPermission( + modifier = Modifier.padding(paddingValues), + onFallbackToMock = { + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = "Falling back to mock locations. You can cycle preset locations via the top bar.", + duration = SnackbarDuration.Long + ) + } + } + ) { content(paddingValues) } } diff --git a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/GetLocationPermission.kt b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/GetLocationPermission.kt index ea8fc78..cea9d9c 100644 --- a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/GetLocationPermission.kt +++ b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/GetLocationPermission.kt @@ -14,36 +14,54 @@ package com.google.android.libraries.places.compose.demo.presentation.landmark import android.Manifest +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberMultiplePermissionsState +import com.google.android.libraries.places.compose.demo.R /** - * A composable that handles the logic for requesting location permissions. + * A composable that handles the logic for requesting location permissions, with an optional + * fallback to preset mock locations if the user chooses not to grant device permissions. * * @param modifier The modifier to apply to the composable. - * @param content The content to display if the location permissions are granted. - * - * This composable uses the `rememberMultiplePermissionsState` API to manage the state of the location permissions. - * It displays a different message and button based on the current state of the permissions. - * - * If the user has granted all the permissions, the `content` composable is displayed. - * If the user has denied all the permissions, a message explaining why the permissions are needed is displayed, - * along with a button to request the permissions. - * If the user has only granted the coarse location permission, a message asking the user to grant the fine location permission is displayed, - * along with a button to request the fine location permission. + * @param onFallbackToMock Callback invoked when the user elects to use mock location fallback. + * @param content The content to display if permissions are granted or mock fallback is selected. */ @OptIn(ExperimentalPermissionsApi::class) @Composable -fun GetLocationPermission(modifier: Modifier = Modifier, content: @Composable () -> Unit) { +fun GetLocationPermission( + modifier: Modifier = Modifier, + onFallbackToMock: (() -> Unit)? = null, + content: @Composable () -> Unit +) { val locationPermissionsState = rememberMultiplePermissionsState( listOf( Manifest.permission.ACCESS_COARSE_LOCATION, @@ -51,41 +69,97 @@ fun GetLocationPermission(modifier: Modifier = Modifier, content: @Composable () ) ) - if (locationPermissionsState.allPermissionsGranted) { + var useMockFallback by rememberSaveable { mutableStateOf(false) } + + if (locationPermissionsState.allPermissionsGranted || useMockFallback) { content() } else { - Column(modifier = modifier.padding(16.dp)) { - val allPermissionsRevoked = - locationPermissionsState.permissions.size == - locationPermissionsState.revokedPermissions.size + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp) + .verticalScroll(rememberScrollState()), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant + ), + shape = MaterialTheme.shapes.large + ) { + Column( + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + val allPermissionsRevoked = + locationPermissionsState.permissions.size == + locationPermissionsState.revokedPermissions.size - val textToShow = if (!allPermissionsRevoked) { - // If not all the permissions are revoked, it's because the user accepted the COARSE - // location permission, but not the FINE one. - "Thank you for granting access to your approximate location. To provide you with " + - "the most accurate and relevant information, please grant permission " + - "to access your precise location. This will enable the app to tailor " + - "responses to your specific needs and circumstances, ensuring a more " + - "relevant and effective experience." - } else if (locationPermissionsState.shouldShowRationale) { - // Both location permissions have been denied - "Getting your exact location is important for this app. " + - "Please grant us fine location. Thank you :D" - } else { - // First time the user sees this feature or the user doesn't want to be asked again - "This feature requires location permission" - } + val titleText = if (!allPermissionsRevoked) { + "Precise Location Access" + } else { + "Location Access Required" + } - val buttonText = if (!allPermissionsRevoked) { - "Allow precise location" - } else { - "Request permissions" - } + val textToShow = if (!allPermissionsRevoked) { + "Thank you for granting access to your approximate location. To provide you with " + + "the most accurate and relevant information, please grant permission " + + "to access your precise location." + } else if (locationPermissionsState.shouldShowRationale) { + "Getting your location is important for this demo to provide relevant places and autocomplete suggestions." + } else { + "This feature requires location permission to bias place suggestions and display relevant map markers." + } + + val buttonText = if (!allPermissionsRevoked) { + "Allow Precise Location" + } else { + "Grant Location Permission" + } + + Icon( + painter = painterResource(R.drawable.baseline_my_location_24), + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.primary + ) + + Text( + text = titleText, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Text( + text = textToShow, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Button( + onClick = { locationPermissionsState.launchMultiplePermissionRequest() }, + modifier = Modifier.fillMaxWidth() + ) { + Text(buttonText) + } - Text(text = textToShow) - Spacer(modifier = Modifier.height(8.dp)) - Button(onClick = { locationPermissionsState.launchMultiplePermissionRequest() }) { - Text(buttonText) + OutlinedButton( + onClick = { + useMockFallback = true + onFallbackToMock?.invoke() + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Use Mock Location Instead") + } + } } } } diff --git a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/LandmarkSelectionActivity.kt b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/LandmarkSelectionActivity.kt index d99ae6d..6a63af6 100644 --- a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/LandmarkSelectionActivity.kt +++ b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/LandmarkSelectionActivity.kt @@ -25,8 +25,10 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import kotlinx.coroutines.launch import com.google.android.libraries.places.compose.demo.R import com.google.android.libraries.places.compose.demo.presentation.ViewModelEvent import com.google.android.libraries.places.compose.demo.presentation.common.CommonEvent @@ -82,6 +84,8 @@ class LandmarkSelectionActivity : ComponentActivity() { val userMarker = rememberUpdatedMarkerState(position = location) val commonViewState by commonViewModel.commonViewState.collectAsStateWithLifecycle() val landmarkMarkers by landmarkSelectionViewModel.landmarkMarkers.collectAsStateWithLifecycle() + val isLoading by landmarkSelectionViewModel.isLoading.collectAsStateWithLifecycle() + val coroutineScope = rememberCoroutineScope() LandmarkSelectionContent( modifier = Modifier @@ -89,9 +93,40 @@ class LandmarkSelectionActivity : ComponentActivity() { .padding(paddingValues), userLocation = location, userMarker = userMarker, + isLoading = isLoading, onMapClicked = { latLng -> landmarkSelectionViewModel.onEvent(LandmarkSelectionEvent.OnUserLocationChanged(latLng)) }, + onAddressChanged = { address -> + landmarkSelectionViewModel.onEvent(LandmarkSelectionEvent.OnAddressChanged(address)) + }, + onConfirmAddress = { + val formatted = when (val addr = displayAddress) { + is com.google.android.libraries.places.compose.demo.presentation.landmark.addresshandlers.`in`.IndiaDisplayAddress -> listOfNotNull( + addr.aptSuiteUnit.ifBlank { null }, + addr.streetAddress.ifBlank { null }, + addr.city.ifBlank { null }, + addr.state.ifBlank { null }, + addr.pinCode.ifBlank { null }, + addr.country.ifBlank { null } + ).joinToString(", ") + is com.google.android.libraries.places.compose.demo.presentation.landmark.addresshandlers.us.UsDisplayAddress -> listOfNotNull( + addr.streetAddress.ifBlank { null }, + addr.additionalAddressInfo.ifBlank { null }, + addr.city.ifBlank { null }, + addr.state.ifBlank { null }, + addr.zipCode.ifBlank { null }, + addr.country.ifBlank { null } + ).joinToString(", ") + else -> "No address selected" + } + coroutineScope.launch { + snackbarHostState.showSnackbar( + message = "Address saved: $formatted", + duration = SnackbarDuration.Short + ) + } + }, nearbyObjectsWithLocations = nearbyObjectsWithLocations, address = displayAddress, showMap = commonViewState.showMap, diff --git a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/LandmarkSelectionEvent.kt b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/LandmarkSelectionEvent.kt index 9d2a1b9..ee8143e 100644 --- a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/LandmarkSelectionEvent.kt +++ b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/LandmarkSelectionEvent.kt @@ -15,6 +15,7 @@ package com.google.android.libraries.places.compose.demo.presentation.landmark import com.google.android.gms.maps.model.LatLng import com.google.android.libraries.places.compose.autocomplete.models.NearbyObject +import com.google.android.libraries.places.compose.demo.presentation.landmark.addresshandlers.DisplayAddress /** * A sealed class representing events that can occur during landmark selection. @@ -22,6 +23,7 @@ import com.google.android.libraries.places.compose.autocomplete.models.NearbyObj sealed class LandmarkSelectionEvent { data class OnUserLocationChanged(val location: LatLng) : LandmarkSelectionEvent() data class OnNearbyObjectSelected(val nearbyObject: NearbyObject) : LandmarkSelectionEvent() + data class OnAddressChanged(val address: DisplayAddress) : LandmarkSelectionEvent() data object OnCloseAddressDisplayClicked: LandmarkSelectionEvent() } diff --git a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/LandmarkSelectionScreen.kt b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/LandmarkSelectionScreen.kt index 4967f1e..b2ca909 100644 --- a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/LandmarkSelectionScreen.kt +++ b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/LandmarkSelectionScreen.kt @@ -13,14 +13,25 @@ // limitations under the License. package com.google.android.libraries.places.compose.demo.presentation.landmark +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.Place +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -36,6 +47,8 @@ import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.LatLng import com.google.android.libraries.places.api.model.Place import com.google.android.libraries.places.compose.autocomplete.models.NearbyObject +import androidx.compose.material3.LinearProgressIndicator +import com.google.android.gms.maps.CameraUpdateFactory import com.google.android.libraries.places.compose.demo.R import com.google.android.libraries.places.compose.demo.presentation.landmark.addresshandlers.DisplayAddress import com.google.android.libraries.places.compose.demo.presentation.landmark.components.AddressDisplay @@ -52,6 +65,9 @@ fun LandmarkSelectionContent( showMap: Boolean, onMapClicked: (LatLng) -> Unit, modifier: Modifier = Modifier, + isLoading: Boolean = false, + onAddressChanged: ((DisplayAddress) -> Unit)? = null, + onConfirmAddress: (() -> Unit)? = null, ) { val cameraPositionState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom(userLocation, 15f) @@ -59,7 +75,11 @@ fun LandmarkSelectionContent( LaunchedEffect(userLocation) { userMarker.position = userLocation - cameraPositionState.position = CameraPosition.fromLatLngZoom(userLocation, 15f) + try { + cameraPositionState.animate(CameraUpdateFactory.newLatLng(userLocation)) + } catch (_: Exception) { + cameraPositionState.position = CameraPosition.fromLatLngZoom(userLocation, 15f) + } } var selectedPlaceId by remember(landmarkMarkers) { @@ -75,21 +95,123 @@ fun LandmarkSelectionContent( userMarker = userMarker, modifier = Modifier .fillMaxWidth() - .height(350.dp), - onMapClick = onMapClicked, + .height(300.dp), + onMapClick = { latLng -> + userMarker.position = latLng + onMapClicked(latLng) + }, selectedPlaceId = selectedPlaceId, onLandmarkSelected = { selectedPlaceId = it }, landmarkMarkers = landmarkMarkers ) + Text( + text = stringResource(R.string.map_tap_instruction), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 24.dp, vertical = 2.dp) + ) + } + + if (isLoading) { + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 4.dp) + ) + Text( + text = "Looking up address & landmarks for (${"%.4f".format(userLocation.latitude)}, ${"%.4f".format(userLocation.longitude)})...", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(horizontal = 24.dp) + ) } + Column( modifier = Modifier .fillMaxSize() - .padding(24.dp) + .padding(horizontal = 24.dp, vertical = 12.dp) .verticalScroll(rememberScrollState()), ) { + if (nearbyObjectsWithLocations.isEmpty()) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Outlined.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Text( + text = stringResource(R.string.address_descriptors_info_title), + style = MaterialTheme.typography.titleMedium + ) + } + Text( + text = stringResource(R.string.address_descriptors_unavailable_message), + style = MaterialTheme.typography.bodyMedium + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button( + onClick = { onMapClicked(LatLng(12.9794404, 77.7179181)) }, + modifier = Modifier.weight(1f) + ) { + Text( + text = stringResource(R.string.switch_to_india_bangalore), + style = MaterialTheme.typography.labelMedium + ) + } + } + } + } + } else { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ) + ) { + Row( + modifier = Modifier.padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Outlined.Place, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + Text( + text = stringResource( + R.string.address_descriptors_available_hint, + landmarkMarkers.size + ), + style = MaterialTheme.typography.bodySmall + ) + } + } + } if (address != null) { AddressDisplay( @@ -97,10 +219,23 @@ fun LandmarkSelectionContent( modifier = Modifier.fillMaxWidth(), nearbyObjects = nearbyObjectsWithLocations.map { it.first }, selectedPlaceId = selectedPlaceId, + onAddressChanged = onAddressChanged, onNearbyLandmarkSelected = { selectedPlaceId = it } ) + + Button( + onClick = { onConfirmAddress?.invoke() }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp) + ) { + Text( + text = stringResource(R.string.confirm_address_button), + style = MaterialTheme.typography.labelLarge + ) + } } else { Box( modifier = Modifier diff --git a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/LandmarkSelectionViewModel.kt b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/LandmarkSelectionViewModel.kt index 1e3bfe4..c297893 100644 --- a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/LandmarkSelectionViewModel.kt +++ b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/LandmarkSelectionViewModel.kt @@ -20,13 +20,16 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.android.gms.maps.model.LatLng +import com.google.android.libraries.places.api.model.Place import com.google.android.libraries.places.compose.autocomplete.domain.mappers.toAddress import com.google.android.libraries.places.compose.autocomplete.models.NearbyObject +import com.google.android.libraries.places.compose.autocomplete.models.geocoder.ReverseGeocodingResponse import com.google.android.libraries.places.compose.demo.data.repositories.GeocoderRepository import com.google.android.libraries.places.compose.demo.data.repositories.MergedLocationRepository import com.google.android.libraries.places.compose.demo.data.repositories.PlaceRepository import com.google.android.libraries.places.compose.demo.mappers.toNearbyObjects import com.google.android.libraries.places.compose.demo.presentation.ViewModelEvent +import com.google.android.libraries.places.compose.demo.presentation.landmark.addresshandlers.DisplayAddress import com.google.android.libraries.places.compose.demo.presentation.landmark.addresshandlers.toDisplayAddress import com.google.android.libraries.places.compose.demo.presentation.landmark.addresshandlers.us.UsDisplayAddress import com.google.maps.android.compose.MarkerState @@ -47,6 +50,11 @@ import kotlinx.coroutines.flow.stateIn import javax.inject.Inject import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + data class LandmarkMarker( val landmark: NearbyObject.NearbyLandmark, val marker: MarkerState, @@ -75,12 +83,13 @@ class LandmarkSelectionViewModel initialValue = LatLng(0.0, 0.0) ) - private val geocoderResult = location.filterNotNull().mapNotNull { location -> - geocoderRepository.reverseGeocode(location, includeAddressDescriptors = true) - } + private val _isLoading = MutableStateFlow(false) + val isLoading = _isLoading.asStateFlow() - private val nearbyObjects = geocoderResult.mapNotNull { result -> - result.addressDescriptor?.toNearbyObjects() + private val _geocoderResult = MutableStateFlow(null) + + private val nearbyObjects: StateFlow> = _geocoderResult.map { result -> + result?.addressDescriptor?.toNearbyObjects() ?: emptyList() }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5.seconds), @@ -88,11 +97,11 @@ class LandmarkSelectionViewModel ) @OptIn(ExperimentalCoroutinesApi::class) - val nearbyObjectsWithLatLngs = nearbyObjects.mapLatest { nearbyObjects -> - nearbyObjects.map { nearbyObject -> + val nearbyObjectsWithLatLngs: StateFlow>> = nearbyObjects.mapLatest { objects -> + objects.map { nearbyObject -> viewModelScope.async { placesRepository.getPlaceLatLng(nearbyObject.placeId) } }.awaitAll().map { place -> - nearbyObjects.first { address -> address.placeId == place.first } to place.second + objects.first { address -> address.placeId == place.first } to place.second } }.stateIn( scope = viewModelScope, @@ -100,22 +109,58 @@ class LandmarkSelectionViewModel initialValue = emptyList() ) - @OptIn(ExperimentalCoroutinesApi::class) - val displayAddress = geocoderResult.mapLatest { geocoderDto -> - geocoderDto.addresses.firstOrNull()?.let { address -> - address.toAddress(address.getCountryCode() ?: "US").toDisplayAddress() - } ?: UsDisplayAddress() - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(5.seconds), - initialValue = null - ) + private val _displayAddress = MutableStateFlow(null) + val displayAddress = _displayAddress.asStateFlow() private val _viewModelEventChannel = MutableSharedFlow() val viewModelEventChannel: SharedFlow = _viewModelEventChannel.asSharedFlow() - val landmarkMarkers = nearbyObjectsWithLatLngs.map { nearbyObjectsWithLatLngs -> - nearbyObjectsWithLatLngs.filter{ + init { + viewModelScope.launch { + location.collect { loc -> + if (loc.latitude != 0.0 || loc.longitude != 0.0) { + _isLoading.value = true + try { + val response = geocoderRepository.reverseGeocode(loc, includeAddressDescriptors = true) + _geocoderResult.value = response + if (response != null) { + if (response.status == "OK") { + val addr = response.addresses.firstOrNull()?.let { address -> + address.toAddress(address.getCountryCode() ?: "US").toDisplayAddress() + } ?: UsDisplayAddress() + _displayAddress.value = addr + + val landmarkCount = response.addressDescriptor?.landmarks?.size ?: 0 + if (landmarkCount > 0) { + _viewModelEventChannel.emit( + ViewModelEvent.UserMessage("Found $landmarkCount nearby landmarks.") + ) + } + } else { + val err = response.errorMessage ?: "Geocoding status: ${response.status}" + _viewModelEventChannel.emit( + ViewModelEvent.UserMessage("Geocoding notice: $err") + ) + } + } else { + _viewModelEventChannel.emit( + ViewModelEvent.UserMessage("Reverse geocoding request failed. Check network or API key restrictions.") + ) + } + } catch (e: Exception) { + _viewModelEventChannel.emit( + ViewModelEvent.UserMessage("Geocoding error: ${e.localizedMessage ?: "Unknown"}") + ) + } finally { + _isLoading.value = false + } + } + } + } + } + + val landmarkMarkers: StateFlow> = nearbyObjectsWithLatLngs.map { list -> + list.filter { it.first is NearbyObject.NearbyLandmark }.mapNotNull { (nearbyObject, place) -> place.location?.let { latLng -> @@ -125,7 +170,7 @@ class LandmarkSelectionViewModel marker = MarkerState(position = latLng) ) } - } + } }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5.seconds), @@ -138,6 +183,13 @@ class LandmarkSelectionViewModel fun onEvent(event: LandmarkSelectionEvent) { when (event) { is LandmarkSelectionEvent.OnUserLocationChanged -> { + viewModelScope.launch { + val lat = "%.4f".format(event.location.latitude) + val lng = "%.4f".format(event.location.longitude) + _viewModelEventChannel.emit( + ViewModelEvent.UserMessage("Pin set to ($lat, $lng). Looking up address...") + ) + } mergedLocationRepository.setMockLocation(event.location) } @@ -145,6 +197,10 @@ class LandmarkSelectionViewModel selectedNearbyObject = event.nearbyObject } + is LandmarkSelectionEvent.OnAddressChanged -> { + _displayAddress.value = event.address + } + LandmarkSelectionEvent.OnCloseAddressDisplayClicked -> { selectedNearbyObject = null } diff --git a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/addresshandlers/in/IndiaAddressForm.kt b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/addresshandlers/in/IndiaAddressForm.kt index 9586832..ec6e98a 100644 --- a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/addresshandlers/in/IndiaAddressForm.kt +++ b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/landmark/addresshandlers/in/IndiaAddressForm.kt @@ -44,14 +44,18 @@ fun IndiaAddressForm( value = address.aptSuiteUnit, label = R.string.india_address_unit_number, modifier = Modifier.fillMaxWidth(), - onValueChange = { onAddressChanged?.invoke(address.copy(aptSuiteUnit = it)) }, + onValueChange = onAddressChanged?.let { + { onAddressChanged.invoke(address.copy(aptSuiteUnit = it)) } + }, ) AddressTextField( value = address.streetAddress, label = R.string.india_address_street_address, modifier = Modifier.fillMaxWidth(), - onValueChange = { onAddressChanged?.invoke(address.copy(streetAddress = it)) }, + onValueChange = onAddressChanged?.let { + { onAddressChanged.invoke(address.copy(streetAddress = it)) } + }, ) if (nearbyObjects.isNotEmpty()) { @@ -66,7 +70,9 @@ fun IndiaAddressForm( value = address.city, label = R.string.india_address_city, modifier = Modifier.fillMaxWidth(), - onValueChange = { onAddressChanged?.invoke(address.copy(city = it)) }, + onValueChange = onAddressChanged?.let { + { onAddressChanged.invoke(address.copy(city = it)) } + }, ) Row( @@ -78,14 +84,18 @@ fun IndiaAddressForm( value = address.state, label = R.string.india_address_state, modifier = Modifier.weight(1f), - onValueChange = { onAddressChanged?.invoke(address.copy(state = it)) }, + onValueChange = onAddressChanged?.let { + { onAddressChanged.invoke(address.copy(state = it)) } + }, ) AddressTextField( value = address.pinCode, label = R.string.india_address_pincode, modifier = Modifier.weight(1f), - onValueChange = { onAddressChanged?.invoke(address.copy(pinCode = it)) }, + onValueChange = onAddressChanged?.let { + { onAddressChanged.invoke(address.copy(pinCode = it)) } + }, ) } diff --git a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/minimal/PlacesAutocompleteMinimalActivity.kt b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/minimal/PlacesAutocompleteMinimalActivity.kt index 70d803b..c1fdabc 100644 --- a/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/minimal/PlacesAutocompleteMinimalActivity.kt +++ b/places-compose-demo/src/main/java/com/google/android/libraries/places/compose/demo/presentation/minimal/PlacesAutocompleteMinimalActivity.kt @@ -63,6 +63,14 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlin.time.Duration.Companion.milliseconds +import androidx.activity.enableEdgeToEdge +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TopAppBarDefaults + class PlacesAutocompleteMinimalActivity : ComponentActivity() { @SuppressLint("MissingPermission") @OptIn( @@ -71,6 +79,7 @@ class PlacesAutocompleteMinimalActivity : ComponentActivity() { ) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + enableEdgeToEdge() Places.initializeWithNewPlacesApiEnabled(this, BuildConfig.PLACES_API_KEY) val placesClient = Places.createClient(this) @@ -86,56 +95,85 @@ class PlacesAutocompleteMinimalActivity : ComponentActivity() { setContent { AndroidPlacesComposeDemoTheme { - GetLocationPermission { - val location by locationFlow.collectAsStateWithLifecycle() + val location by locationFlow.collectAsStateWithLifecycle() - // We just want to get the last location once. - LaunchedEffect(Unit) { - locationFlow.value = locationRepository.getLastLocation() + // We just want to get the last location once. + LaunchedEffect(Unit) { + val lastLoc = locationRepository.getLastLocation() + if (lastLoc != null) { + locationFlow.value = lastLoc + } else if (locationFlow.value == null) { + locationFlow.value = LatLng(40.01924246438453, -105.259858527573) } + } - val searchText by searchTextFlow.collectAsStateWithLifecycle() + val searchText by searchTextFlow.collectAsStateWithLifecycle() - val country by remember { - locationFlow.mapNotNull { location -> - location?.let { - geocoder.reverseGeocode(it).addresses.firstOrNull() - ?.getCountryCode() ?: deviceCountry - } + val country by remember { + locationFlow.mapNotNull { location -> + location?.let { + geocoder.reverseGeocode(it)?.addresses?.firstOrNull() + ?.getCountryCode() ?: deviceCountry } - }.collectAsState(initial = deviceCountry) - - // Determine which units converter to use based on the country. - val unitsConverter = remember(country) { - getUnitsConverter(country) } + }.collectAsState(initial = deviceCountry) - // TODO: this feels like it would make a great UseCase. - val predictions by remember(country, location) { - searchTextFlow.debounce(500.milliseconds).map { query -> - if (query.isBlank()) { - emptyList() - } else { - placesClient.awaitFindAutocompletePredictions { - origin = location - locationBias = location?.toRectangularBounds() - typesFilter = listOf(PlaceTypes.ESTABLISHMENT) - this.query = query - countries = listOf(country) - }.autocompletePredictions.map { it.toPlaceDetails() } - } + // Determine which units converter to use based on the country. + val unitsConverter = remember(country) { + getUnitsConverter(country) + } + + // TODO: this feels like it would make a great UseCase. + val predictions by remember(country, location) { + searchTextFlow.debounce(500.milliseconds).map { query -> + if (query.isBlank()) { + emptyList() + } else { + placesClient.awaitFindAutocompletePredictions { + origin = location + locationBias = location?.toRectangularBounds() + typesFilter = listOf(PlaceTypes.ESTABLISHMENT) + this.query = query + countries = listOf(country) + }.autocompletePredictions.map { it.toPlaceDetails() } } - }.collectAsState(initial = emptyList()) + } + }.collectAsState(initial = emptyList()) - Scaffold( - topBar = { - TopAppBar( - title = { Text("Places Autocomplete") } - ) + Scaffold( + modifier = Modifier.fillMaxSize(), + topBar = { + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + actionIconContentColor = MaterialTheme.colorScheme.primary, + navigationIconContentColor = MaterialTheme.colorScheme.primary, + ), + title = { Text("Places Autocomplete") }, + navigationIcon = { + IconButton(onClick = { finish() }) { + Icon( + imageVector = Icons.AutoMirrored.Default.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + } + } + ) + } + ) { paddingValues -> + GetLocationPermission( + modifier = Modifier.padding(paddingValues), + onFallbackToMock = { + locationFlow.value = LatLng(40.01924246438453, -105.259858527573) + Toast.makeText( + this@PlacesAutocompleteMinimalActivity, + "Falling back to mock location (Boulder, CO)", + Toast.LENGTH_LONG + ).show() } - ) { paddingValues -> + ) { CompositionLocalProvider(LocalUnitsConverter provides unitsConverter) { - if (location == null) { Box( modifier = Modifier diff --git a/places-compose-demo/src/main/res/drawable/ic_launcher_foreground.xml b/places-compose-demo/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..d403f6a --- /dev/null +++ b/places-compose-demo/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/places-compose-demo/src/main/res/values/strings.xml b/places-compose-demo/src/main/res/values/strings.xml index 6411809..1da22c0 100644 --- a/places-compose-demo/src/main/res/values/strings.xml +++ b/places-compose-demo/src/main/res/values/strings.xml @@ -13,7 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - + Android Places Compose Demo Maps compose widgets @@ -78,4 +78,19 @@ Landmark Area/sector/locality Fill in the address from the current location + + + Address Descriptors + Google Maps Address Descriptors (landmarks and areas) are available in select regions such as India. No landmarks are found for the current location. + Try Bangalore, India (KTPO) + Showing %1$d nearby landmarks. Tap a yellow marker on the map or select from the dropdown below to attach a landmark. + + Tap anywhere on the map to set location & reverse-geocode + Save & Confirm Address + + + Search places with interactive predictions + Address Descriptors & nearby landmarks on map + Autocomplete to structured address with descriptors + Standalone autocomplete text field \ No newline at end of file