Google Map

Navigating

The Google Map SDK for Android's terms of use forbid using Google's navigation data for real-time navigation in your own application. If you want to show the user how to get from point A to B in real time, you need to launch the Google Maps application itself.

We launch navigation using an Android Intent. An Intent describes something you would like to do, typically with a different application or system service. This intent contains a URI that represents the navigation request:

https://www.google.com/maps/dir/?api=1&origin=${curr.latitude},${curr.longitude}&destination=${car.latitude},${car.longitude}&travelmode=walking

The Google Maps application registers IntentFilters that watch for URIs starting with "https://www.google.com/maps". The Android platform directs this Intent to Google Maps, and it presents navigation options:

Google Maps Application Navigation

Code Changes

CHANGED: /app/src/main/java/com/androidbyexample/googlemap/MainActivity.kt
package com.androidbyexample.googlemap

import android.Manifest
import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Intent
import android.content.pm.PackageManager
import android.location.Location
import android.net.Uri
import android.os.Bundle
import android.os.Looper
import android.provider.Settings
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
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.foundation.layout.wrapContentSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.app.ActivityCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.androidbyexample.googlemap.ui.theme.GoogleMapTheme
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.model.BitmapDescriptor
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.maps.android.compose.CameraPositionState
import com.google.maps.android.compose.DragState
import com.google.maps.android.compose.GoogleMap
import com.google.maps.android.compose.MapProperties
import com.google.maps.android.compose.MapType
import com.google.maps.android.compose.MarkerInfoWindowContent
import com.google.maps.android.compose.MarkerState
import com.google.maps.android.compose.rememberCameraPositionState
import com.google.maps.android.compose.rememberMarkerState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    private val viewModel: CarViewModel by viewModels()

private lateinit var fusedLocationProviderClient: FusedLocationProviderClient private val locationCallback = object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult) { viewModel.updateLocation(locationResult.lastLocation) } }
private val getLocationPermission = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { isGranted -> if (isGranted.values.any { it }) { startLocationAndMap() } else { // if the user denied permissions, tell them they // cannot use the app without them. In general, // you should try to just reduce function and let the // user continue, but location is a key part of this // application. // (Note that a real version of this application // might allow the user to manually click on the map // to set their current location, and we wouldn't // show this dialog, or perhaps only show it once) // NOTE: This is a normal Android-View-based dialog, not a compose one! AlertDialog.Builder(this) .setTitle("Permissions Needed") .setMessage( "We need coarse-location or fine-location permission " + "to locate a car (fine location is highly " + "recommended for accurate car locating). " + "Please allow these permissions via App Info " + "settings") .setCancelable(false) .setNegativeButton("Quit") { _, _ -> finish() } .setPositiveButton("App Info") { _, _ -> startActivity( Intent( Settings.ACTION_APPLICATION_DETAILS_SETTINGS ).apply { data = Uri.parse("package:$packageName") } ) finish() } .show() } }
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
GoogleApiAvailability.getInstance() .makeGooglePlayServicesAvailable(this) .addOnSuccessListener {
if (ActivityCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( this, Manifest.permission.ACCESS_COARSE_LOCATION ) != PackageManager.PERMISSION_GRANTED ) { getLocationPermission.launch( arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION ) ) } else { startLocationAndMap() }
}.addOnFailureListener(this) { Toast.makeText( this, "Google Play services required (or upgrade required)", Toast.LENGTH_SHORT ).show() finish() }
} @SuppressLint("MissingPermission") fun startLocationAndMap() {
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000) .setWaitForAccurateLocation(false) .setMinUpdateIntervalMillis(0) .setMaxUpdateDelayMillis(5000) .build() fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this) fusedLocationProviderClient.requestLocationUpdates( locationRequest, locationCallback, Looper.getMainLooper() )
setContent { GoogleMapTheme { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background ) {
val cameraPositionState = rememberCameraPositionState()
val currentLocation by viewModel.currentLocation.collectAsStateWithLifecycle( initialValue = null )
val carLatLng by viewModel.carLatLng.collectAsStateWithLifecycle(initialValue = null)
GoogleMapDisplay( currentLocation = currentLocation, carLatLng = carLatLng, cameraPositionState = cameraPositionState, onSetCarLocation = viewModel::setCarLocation, onClearCarLocation = viewModel::clearCarLocation, onMoveCar = viewModel::setCarLocation, modifier = Modifier.fillMaxSize(), )
} } } } }
@Composable fun GoogleMapDisplay( currentLocation: Location?, carLatLng: LatLng?, cameraPositionState: CameraPositionState, onSetCarLocation: () -> Unit, onClearCarLocation: () -> Unit,
onMoveCar: (LatLng) -> Unit,
modifier: Modifier, ) {
var mapLoaded by remember { mutableStateOf(false) }
var currentMapType by remember { mutableStateOf(MapType.NORMAL) } var mapProperties by remember { mutableStateOf(MapProperties(mapType = MapType.NORMAL)) }
val currentLocationState = remember(currentLocation) { currentLocation?.let { MarkerState( LatLng( it.latitude, it.longitude ) ) } }
val carState = rememberMarkerState("car")
val context = LocalContext.current var currentLocationIcon by remember { mutableStateOf<BitmapDescriptor?>(null) } var carIcon by remember { mutableStateOf<BitmapDescriptor?>(null) } val scope = rememberCoroutineScope()
var initialBoundsSet by remember { mutableStateOf(false) } LaunchedEffect(key1 = currentLocation) { if (currentLocation != null) { if (!initialBoundsSet) { initialBoundsSet = true val current = LatLng(currentLocation.latitude, currentLocation.longitude) cameraPositionState.animate( CameraUpdateFactory.newLatLngZoom( current, 16f ), 1000 ) } } }
LaunchedEffect(true) { var dragged = false snapshotFlow { carState.dragState } .collect { // Make sure we've seen at least one drag state before updating // the view model. Otherwise we'll see the initial (0.0, 0.0) // value that was set when the MarkerState was created if (it == DragState.DRAG) { dragged = true } else if (it == DragState.END && dragged) { dragged = false onMoveCar(carState.position) } } }
Scaffold( topBar = { CarTopBar( currentLocation = currentLocation, carLatLng = carLatLng, onSetCarLocation = onSetCarLocation, onClearCarLocation = onClearCarLocation,
// onWalkToCar = { TODO() }, onWalkToCar = { currentLocation?.let { curr -> carLatLng?.let { car -> val uri = Uri.parse( "https://www.google.com/maps/dir/" + "?api=1&origin=${curr.latitude}," + "${curr.longitude}&" + "destination=${car.latitude}," + "${car.longitude}&travelmode=walking") context.startActivity( Intent( Intent.ACTION_VIEW, uri ).apply { setPackage("com.google.android.apps.maps") }) } ?: Toast.makeText( context, "Cannot navigate; no car location available", Toast.LENGTH_LONG ).show() } ?: Toast.makeText( context, "Cannot navigate; no current location available", Toast.LENGTH_LONG ).show() },
onGoToCurrentLocation = { currentLocation?.let { curr -> scope.launch { cameraPositionState.animate( CameraUpdateFactory.newLatLngZoom( LatLng(curr.latitude, curr.longitude), 16f ), 1000 ) } } ?: Toast.makeText( context, "No current location available", Toast.LENGTH_LONG ).show() }, ) }, content = { paddingValues -> Box(
modifier = modifier.padding(paddingValues),
) {
Column( modifier = Modifier.fillMaxSize() ) { MapTypeSelector( currentValue = currentMapType, modifier = Modifier.fillMaxWidth(), ) { mapProperties = mapProperties.copy(mapType = it) currentMapType = it }
GoogleMap(
cameraPositionState = cameraPositionState,
onMapLoaded = { mapLoaded = true
scope.launch(Dispatchers.IO) { currentLocationIcon = context.loadBitmapDescriptor( R.drawable.ic_current_location ) carIcon = context.loadBitmapDescriptor( R.drawable.ic_car ) }
},
properties = mapProperties,
modifier = Modifier .fillMaxWidth() .weight(1f),
) {
currentLocationState?.let { MarkerInfoWindowContent( state = it,
icon = currentLocationIcon, anchor = Offset(0.5f, 0.5f),
title = stringResource( id = R.string.current_location ), ) }
carLatLng?.let { carState.position = it MarkerInfoWindowContent( state = carState,
draggable = true,
icon = carIcon, anchor = Offset(0.5f, 0.5f), title = stringResource( id = R.string.car_location ), ) } } } }
if (!mapLoaded) { AnimatedVisibility( visible = true, modifier = Modifier.fillMaxSize(), enter = EnterTransition.None, exit = fadeOut() ) { CircularProgressIndicator( modifier = Modifier .background(MaterialTheme.colorScheme.background) .wrapContentSize() ) } }
} ) }