Location - the Android 14 (maybe 15 too) way

Introduction

Location awareness is becoming an essential part of many successful mobile applications. Whether you're building a fitness tracker, a navigation app, a ride-sharing app, a weather app, an augmented reality experience or a service that connects users based on proximity, incorporating location functionality can significantly enhance your app's value and user experience.

This article helps you leverage the Location API in Android, providing a step-by-step approach to accessing the user's location, handling permissions, and setting up real-time location updates. By the end of this tutorial, you'll be well-equipped to integrate location-based features seamlessly into your Android projects.

Setting up your project to use the Location API

If you don't already have a project set up, follow the following steps:

Open Android Studio.

Go to File > New > New Project.

Select Basic Views Activity

Enter a name for your app

Click Finish

Open your app/build.gradle file and add the following dependency

dependencies {
  //Location
  implementation 'com.google.android.gms:play-services-location:20.0.0'
  }

If you use the Gradle Kotlin DSL, add the following to the libs.versions.toml file

[versions]
...
playlocation = "21.3.0"

[libraries]
...
play-location = { group = "com.google.android.gms", name = "play-services-location", version.ref = "playlocation"}

[plugins]
...

And in the app/build.gradle.kts file, add this

dependencies {
    ...
    implementation(libs.play.location)
}

Click on Sync Project with Gradle Files so that the just added dependency is available for use in your code.

Add the following permissions to your AndroidManifest file

<manifest ...>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
...
</manifest>
  • ACCESS_FINE_LOCATION: Grants access to the device's most precise location data (GPS, Wi-Fi, cell tower triangulation).

  • ACCESS_COARSE_LOCATION: Provides a less accurate estimate using cell tower and Wi-Fi data (suitable for scenarios where exact coordinates aren't crucial).

Checking If your App has permission

Activity.checkSelfPermission() checks if you have been granted a particular permission. To check if you have the Manifest.permission.ACCESS_FINE_LOCATION permission, you would do:

ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION

Let's use that to build a function that checks for the two permissions we need.

Add the following to the activity.

(Code snippets are presented first in Kotlin and then in Java)

private fun isLocationPermissionGranted(activity: Activity): Boolean {
    return ActivityCompat.checkSelfPermission(
        activity,
        Manifest.permission.ACCESS_FINE_LOCATION
    ) == PackageManager.PERMISSION_GRANTED ||
            ActivityCompat.checkSelfPermission(
                activity,
                Manifest.permission.ACCESS_COARSE_LOCATION
            ) == PackageManager.PERMISSION_GRANTED
}
private boolean isLocationPermissionGranted(Activity activity) {
    return ActivityCompat.checkSelfPermission(activity, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
            ActivityCompat.checkSelfPermission(activity, android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED;
}

This function checks if your app has the permission to access the device's coarse or fine location.

Requesting Permission at runtime

In the activity where location is needed, you must request permission at runtime. We will use an ActivityResultLauncher to request permission and listen for the response to our request. To indicate the kind of request, we have two options. ActivityResultContracts.RequestMultiplePermissions() or ActivityResultContracts.RequestPermission(). We will use the former because we have two permissions to request.

The next function requests for the needed permissions.

private fun requestLocationPermission(callback: (Boolean) -> Unit) {
    val locationPermissionRequest = registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { permissions ->
        val fineLocationGranted: Boolean =
            permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false)
        val coarseLocationGranted: Boolean =
            permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false)

        if (fineLocationGranted || coarseLocationGranted) {
            callback(true) //permission granted
        } else {
            callback(false) //permission denied
        }

    }

    locationPermissionRequest.launch(
        arrayOf(
            Manifest.permission.ACCESS_FINE_LOCATION,
            Manifest.permission.ACCESS_COARSE_LOCATION
        )
    )
}
private void requestLocationPermission(CallbackListener<Boolean> callbackListener) {

    ActivityResultLauncher<String[]> locationPermissionRequest = registerForActivityResult(
            new ActivityResultContracts.RequestMultiplePermissions(),
            permissions -> {
                Boolean fineLocationGranted = permissions.getOrDefault(android.Manifest.permission.ACCESS_FINE_LOCATION, false);
                Boolean coarseLocationGranted = permissions.getOrDefault(android.Manifest.permission.ACCESS_COARSE_LOCATION, false);
                if (
                        fineLocationGranted != null && fineLocationGranted ||
                                coarseLocationGranted != null && coarseLocationGranted
                ) {
                    // Permission granted
                    callbackListener.onCallback(true);
                } else {
                    // No location access granted.
                    callbackListener.onCallback(false);
                }
            });

    locationPermissionRequest.launch(new String[]{
            android.Manifest.permission.ACCESS_FINE_LOCATION,
            android.Manifest.permission.ACCESS_COARSE_LOCATION
    });
}

The result, permissions is a Map containing the permissions requested and whether they were granted. We look at the map for the two permissions requested. If either of them were granted, we're good. Else, we cannot use the Location API. You may present the user with a warning to let them know that some features may be unavailable or may not function properly since they have not granted the app the permission to access their device location.

Check Phone's Location Settings

Let's say the user granted the permission to access the device's location, we also need to check if the device's settings satisfy our requirements. For example, the device's location may be off. In this case, we cannot access the location.

First, we build a location request. This is our way of specifying our requirements.

Add the following field to your Activity

private val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000)
    .build()
private final LocationRequest locationRequest = new LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000)
        .build();

We use SettingsClient.checkLocationSettings() to check if our requirement is met. If the requirement is met, we can proceed to the next step. If it isn't met, we need to ask the user to update their setting.

For that, we need an ActivityResultLauncher to launch the request and listen for the user's action.

Add this field to your Activity.

private val locationSettingsResult =
    registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
        when (result.resultCode) {
            Activity.RESULT_OK -> {
                //User has updated the device's setting
            }

            Activity.RESULT_CANCELED -> {
                //Warn user that location is required for some features
            }
        }
    }
private ActivityResultLauncher<IntentSenderRequest> locationSettingsResult;

public void onCreate(@Nullable Bundle savedInstanceState) {
    locationSettingsResult = registerForActivityResult(new ActivityResultContracts.StartIntentSenderForResult(), result -> {
            if (result.getResultCode() == Activity.RESULT_OK) {
                //User has updated the device's setting
            } else {
                //Warn user that location is required for some features
            }
        });
}

This ActivityResultContracts must be registered before the Activity starts, or the app will crash.

Putting it all together we have this function to check the device's location setting and ask the user to update their location settings if there is a need:

private fun checkPhoneLocationSettings(
    activity: Activity,
    locationSettingsResult: ActivityResultLauncher<IntentSenderRequest>,
    callback: (Boolean) -> Unit
) {
    val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest)
    val client = LocationServices.getSettingsClient(activity)
    val task = client.checkLocationSettings(builder.build())

    task.addOnSuccessListener { callback(true) }

    task.addOnFailureListener { exception ->
        if (exception is ResolvableApiException) {
            try {
                locationSettingsResult.launch(
                    IntentSenderRequest.Builder(exception.resolution).build()
                )
            } catch (sendEx: IntentSender.SendIntentException) {
                // Ignore the error.
            }
        }
    }
}
private void checkPhoneLocationSettings(
        Activity activity,
        ActivityResultLauncher<IntentSenderRequest> locationSettingsResult,
        CallbackListener<Boolean> callbackListener) {

    LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder()
            .addLocationRequest(locationRequest);
    SettingsClient client = LocationServices.getSettingsClient(activity);

    Task<LocationSettingsResponse> task = client.checkLocationSettings(builder.build());

    task.addOnSuccessListener(activity, locationSettingsResponse -> {
        //Location settings are fine. You can start listening for location
        callbackListener.onCallback(true);
    });

    task.addOnFailureListener(activity, e -> {
        if (e instanceof ResolvableApiException) {
            locationSettingsResult.launch(new IntentSenderRequest.Builder(((ResolvableApiException) e).getResolution()).build());
        }
    });
}

At this point, we have what we need to check if we have location permission. If we don't we can request the permissions. After which we can check if the settings meet our location requirements. Let's package all of that into a function.

private fun setUpLocationComponentsAndGetLocation(activity: Activity) {
    if (isLocationPermissionGranted(activity)) {
        checkPhoneLocationSettings(activity) { isLocationSettingsOk ->
            if (isLocationSettingsOk) {
                //You can get last known location or request location update here because you have permission
                //and the device settings meet your requirement.
            } else {
                //warn user
            }
        }
    } else {
        requestLocationPermission { permissionGranted ->
            if (permissionGranted) {
                //simply a recursive call
                setUpLocationComponentsAndGetLocation(activity)
            } else {
                //warn user
            }
        }
    }
}
private void setUpLocationComponentsAndGetLocation(Activity activity) {
        if (isLocationPermissionGranted(activity)) {
        checkPhoneLocationSettings(activity, locationSettingsResult, isLocationSettingsOk -> {
            if (isLocationSettingsOk) {
                //You can get last known location or request location update here because you have permission
                //and the device settings meet your requirement.
            } else {
                //You may decide to warn users that some functions may not be available without permission

            }
        });
    } else {
        requestLocationPermission(permissionGranted -> {
            if (permissionGranted) {
                //simply a recursive call
                setUpLocationComponentsAndGetLocation(activity);
            } else {
                //Warn user
            }
        });
    }
}

Accessing Location

At this point, we can now access the device's location. FusedLocationProviderClient is the main entry point for interacting with the Fused Location Provider - the Location API by Google for accessing user location. To begin, add this field to your Activity:

private val mFusedLocationProviderClient: FusedLocationProviderClient by lazy {
    LocationServices.getFusedLocationProviderClient(this)
}
private FusedLocationProviderClient fusedLocationProviderClient;
@Override
    protected void onStart() {
        super.onStart();
        fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this);
    }

There are two methods of concern to us in the FusedLocationProviderClient class:

FusedLocationProviderClient.getLastLocation()retrieves the last known location while FusedLocationProviderClient.requestLocationUpdates() listens for Location updates.

For the former, think of Weather apps that get your current city and display the weather in that area. For the latter, think of Bolt or Uber which needs to get updates on the driver and rider's location.

Retrieving the last known location

To retrieve the last known location of the device,

@SuppressLint("MissingPermission")
private fun getLastLocation() {
    mFusedLocationProviderClient.lastLocation.addOnSuccessListener { location ->
        if (location != null) {
            // Logic to handle location object
            Log.d("Location", "Long" + location.longitude)
            Log.d("Location", "Lat" + location.latitude)
        }
    }
}
@SuppressLint("MissingPermission")
private void getLastLocation() {
    fusedLocationProviderClient.getLastLocation().addOnSuccessListener(location -> {
        // Got last known location. In some rare situations this can be null.
        if (location != null) {
            // Logic to handle location object
            Log.d("Location", "Long" + location.getLongitude());
            Log.d("Location", "Lat" + location.getLatitude());
        }
    });
}

Notice that you need to check for null because the last location may be null at some point.

Setting up Location Updates for Real-time tracking

This requires some more work compared to retrieving the last known location because we need to set up a callback and manage lifecycle.

For the callback, add this field to your Activity:

private val locationCallBack = object : LocationCallback() {
    override fun onLocationResult(locationResult: LocationResult) {
        super.onLocationResult(locationResult)
        Log.d("Location", "Long" + locationResult.lastLocation?.longitude)
        Log.d("Location", "Lat" + locationResult.lastLocation?.latitude)
    }
}
private final LocationCallback locationCallback = new LocationCallback() {
    @Override
    public void onLocationResult(@NonNull LocationResult locationResult) {
        super.onLocationResult(locationResult);
        if (locationResult.getLastLocation() != null) {
            Log.d("Location", "Long: " + locationResult.getLastLocation().getLongitude());
            Log.d("Location", "Lat: " + locationResult.getLastLocation().getLatitude());
        }
    }
};

Remember we created a LocationRequest object when checking if the device setting meets our requirement. Well, we need the same object now.

Create this function in your Activity

@SuppressLint("MissingPermission")
private fun requestLocationUpdate(activity: Activity) {
    if (isLocationPermissionGranted(activity)) {
        mFusedLocationProviderClient.requestLocationUpdates(
            locationRequest,
            locationCallBack,
            Looper.getMainLooper()
        )
    }
}
@SuppressLint("MissingPermission")
private void requestLocationUpdate(Activity activity) {
    if (isLocationPermissionGranted(activity)) {
        fusedLocationProviderClient.requestLocationUpdates(
                locationRequest,
                locationCallback,
                Looper.getMainLooper()
        );
    }
}

Connecting it all up

Now that we have the function to Get the last known location and request location updates, it is time to update our setUpLocationComponentsAndGetLocation() from before. Add the new lines to the setUpLocationComponentsAndGetLocation() function.

private fun setUpLocationComponentsAndGetLocation(activity: Activity) {
    if (isLocationPermissionGranted(activity)) {
        checkPhoneLocationSettings(activity) { isLocationSettingsOk ->
            if (isLocationSettingsOk) {
                getLastLocation() //Add only this line to get the last location
                requestLocationUpdate(activity) //Add only this line, to request location updates
            } else {
                //warn user
            }
        }
    } else {
        requestLocationPermission { permissionGranted ->
            if (permissionGranted) {
                //simply a recursive call
                setUpLocationComponentsAndGetLocation(activity)
            } else {
                //warn user
            }
        }
    }
}
private void setUpLocationComponentsAndGetLocation(Activity activity) {
        if (isLocationPermissionGranted(activity)) {
        checkPhoneLocationSettings(activity, locationSettingsResult, isLocationSettingsOk -> {
            if (isLocationSettingsOk) {
                getLastLocation(); //Add only this line to get the last location
                requestLocationUpdate(activity); //Add only this line, to request location updates
            } else {
                //You may decide to warn users that some functions may not be available without permission

            }
        });
    } else {
        requestLocationPermission(permissionGranted -> {
            if (permissionGranted) {
                //simply a recursive call
                setUpLocationComponentsAndGetLocation(activity);
            } else {
                //Warn user
            }
        });
    }
}

To get location or request location update, all you need do at this point is to call the setUpLocationComponentsAndGetLocation() from the entry point of the Activity that needs location data. Usually, this is the onStart() method.

override fun onStart() {
    super.onStart()
    setUpLocationComponentsAndGetLocation(this)
}
@Override
protected void onStart() {
    super.onStart();
    setUpLocationComponentsAndGetLocation(this);
}

Cleaning up after yourself

One final and important thing. When you no longer require the location updates, you need to unsubscribe. This most likely happens when the user navigates away from the Activity that requires the location update. It is a best practice to stop location updates by removing the location callback. This way, you can avoid unnecessarily draining the device battery.

override fun onStop() {
    mFusedLocationProviderClient.removeLocationUpdates(locationCallBack)
    super.onStop()
}
@Override
protected void onStop() {
    fusedLocationProviderClient.removeLocationUpdates(locationCallback);
    super.onStop();
}

Conclusion

Integrating location functionality into your Android applications can significantly enhance user experience and provide valuable features for a wide range of applications. By following this step-by-step guide, you now know how to access the user's location, handle permissions, set up real-time location updates, and ensure your app meets the necessary location settings requirements. Implementing these features will allow you to create more dynamic and contextually aware applications.

This guide helped you leverage the Android Location API effectively in your mobile applications. It discussed the process of:

  • Integrating the Location API dependency within your project.

  • Requesting and handling location permissions at runtime.

  • Verifying that the device's location settings meet your app's requirements.

  • Retrieving the user's last known location.

  • Setting up real-time location updates.

  • Stopping location updates to optimize battery usage.

By following the steps in this article, you can add location-based functionalities to enhance your Android apps, create more dynamic and contextually aware applications and deliver a superior user experience.

For a complete example, you can refer to the repository at https://github.com/olubunmialegbeleye/Location.

Happy coding!

0
Subscribe to my newsletter

Read articles from Olubunmi Alegbeleye directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Olubunmi Alegbeleye
Olubunmi Alegbeleye