This codelab covers changes in background location gathering on devices running Android "O".

Before we get coding, let's briefly outline what has changed in "O" - and what hasn't - with regard to location gathering.

Apps can continue to request foreground and background location updates.

Apps with a visible activity or those that define a foreground service can continue receiving location updates as before. Nothing changes with regard to foreground location gathering in Android "O". We won't be targeting this type of foreground location gathering in this codelab.

Apps requesting location in the background, i.e., without a visible app component, can also continue to receive location updates. But there are significant changes to background location gathering in Android "O" to improve system health and a user's device battery life. This codelab helps you understand these changes.

What you will build

You will work through a sample application and learn how to get location updates both while your app is in the foreground and when it is in the background. There are two parts to this codelab:

  1. Shows how location updates work when targeting "N" (or lower) but running on an "O" device
  2. Shows strategies for getting location updates when upgrading targetSdkVersion to "O".

The app has a simple UI:

Prerequisites

Familiarity with Android development.

No prior experience with location services is required.

What you'll learn

What you'll need

Download source code

To get started, open BackgroundLocationUpdates in Android Studio. This is the sample you'll be modifying throughout this codelab.

You MUST use the 3.0. Look for this icon:

To see the finished version, you can peek ahead at BackgroundLocationUpdates_Finished directory.

When completed, this sample allows you to receive location updates both while your app is in the foreground, and after the app has exited.

Understanding the code

Before you start coding, you should familiarize yourself with some of the files in this project (files are located in app/src/main/java/com/google/android/gms/location/sample/backgroundlocationupdates/).

  1. MainActivity.java: The only activity in this app. Contains code to build GoogleApiClient, create a LocationRequest, handle runtime permissions etc. You'll be adding code here to request location updates. The location request is described in greater detail below.
  2. LocationUpdatesIntentService: You'll add code here to process location data sent to your app by Location Services. Used when location is requested using PendingIntent.getService().
  3. LocationUpdatesBroadcastReceiver: You'll add code here to process location data sent to your app by Location Services. Used when location is requested using PendingIntent.getBroadcast().
  4. LocationResultHelper: Utility class for processing location data received by your app (formats location data, creates a notification, etc.)
  5. LocationRequestHelper: Utility class for keeping track of whether you are requesting updates or not. (You won't be modifying this file).

Requesting Location Updates

You'll use the requestLocationUpdates() method provided by the FusedLocationProvider API (one of many APIs provided by Location Services) to request location updates.

There are several versions of requestLocationUpdates(), and you'll be using the one with this signature:

public abstract PendingResult<Status> requestLocationUpdates(
    GoogleApiClient client, 
    LocationRequest request, 
    PendingIntent callbackIntent)

When called successfully, this kicks off a request to location services to start sending location updates to your app.

Here is an explanation of the arguments you pass to requestLocationUpdates():

GoogleApiClient

This is the gateway to Google Play Services and Google APIs. The code for building GoogleApiClient has already been defined:

private void buildGoogleApiClient() {
   if (mGoogleApiClient != null) {
       return;
   }
   mGoogleApiClient = new GoogleApiClient.Builder(this)
           .addConnectionCallbacks(this)
           .enableAutoManage(this, this)
           .addApi(LocationServices.API)
           .build();
   ...
}

LocationRequest

This stores details about the request you are making:

You'll be adding code for the location request in the next step.

.

PendingIntent

Using a PendingIntent makes your app suitable to receive location in both the foreground and background.

You'll be defining the PendingIntent a bit later in the codelab.

The createLocationRequest() method in MainActivity.java is currently empty:

private void createLocationRequest() {
   
}

Modify createLocationRequest() so that it looks like this:

private void createLocationRequest() {
   mLocationRequest = new LocationRequest();

   mLocationRequest.setInterval(UPDATE_INTERVAL);

   // Sets the fastest rate for active location updates. This interval is exact, and your
   // application will never receive updates faster than this value.
   mLocationRequest.setFastestInterval(FASTEST_UPDATE_INTERVAL);
     mLocationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);

   // Sets the maximum time when batched location updates are delivered. Updates may be
   // delivered sooner than this interval.
   mLocationRequest.setMaxWaitTime(MAX_WAIT_TIME);
}

The location request you defined has the following profile:

If you run the app now, you'll see a blank screen, and you won't see any location data. That's because so far, the code for the PendingIntent that you need for requesting location updates doesn't exist. You should fix that now.

Open MainActivity.java in Android Studio and locate the getPendingIntent() method. This method currently returns null, and you will be adding code to this to get location updates.

private PendingIntent getPendingIntent() {
   return null;
}

This getPendingIntent() method is used when requesting location updates (in MainActivity.java):

public void requestLocationUpdates(View view) {
   try {
       ...
       LocationServices.FusedLocationApi.requestLocationUpdates(
               mGoogleApiClient, mLocationRequest, getPendingIntent());
   } catch (SecurityException e) {
      ...
   }
}

It is also used for removing location updates (in MainActivity.java):

public void removeLocationUpdates(View view) {
   Log.i(TAG, "Removing location updates");
   LocationRequestHelper.setRequesting(this, false);
   LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient,
           getPendingIntent());
}

Modify getPendingIntent() so it looks like this:

private PendingIntent getPendingIntent() {
   Intent intent = new Intent(this, LocationUpdatesIntentService.class);
   intent.setAction(LocationUpdatesIntentService.ACTION_PROCESS_UPDATES);
   return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}

This code hooks you up to receive location updates inside the onHandleIntent() method of LocationUpdatesIntentService.

Open LocationUpdatesIntentService.java. You'll now add code to the onHandleIntent() method to process updates sent to your app. For now, the onHandleIntent() method is empty:

protected void onHandleIntent(Intent intent) {
  
}

Modify onHandleIntent() so it looks like this:

@Override
protected void onHandleIntent(Intent intent) {
   if (intent != null) {
       final String action = intent.getAction();
       if (ACTION_PROCESS_UPDATES.equals(action)) {
           LocationResult result = LocationResult.extractResult(intent);
           if (result != null) {
               List<Location> locations = result.getLocations();
               LocationResultHelper locationResultHelper = new LocationResultHelper(this,
                       locations);
               // Save the location data to SharedPreferences.
               locationResultHelper.saveResults();
               // Show notification with the location data.
               locationResultHelper.showNotification();
               Log.i(TAG, LocationResultHelper.getSavedLocationResult(this));
           }
       }
   }
}

The code inside onHandleIntent() runs when location services sends location results to your app. The code does the following:

  1. It extracts the location result.
  2. It saves data about the location SharedPreferences. MainActivity.java implements an OnSharedPreferenceChangeListener, gets notified about the new location, and updates the UI.
  3. It creates a notification with information about the location received. That way, if location data is received while the app is not in the foreground, the notification shows the user the new data. This is not a persistent notification, and you can dismiss it if you like.

Running the sample

Using Android Studio, run the sample, and press the "Request Location Updates" button to kick off location updates.

After a short period, you should see location updates reported on the screen. You should also see a notification with the location result.

Going into the background

Now, close down your app by minimizing or swiping away the main activity.

You should still see the notification, which should show periodically refreshed location data. Except, you will notice that while in the background, the updates don't seem to happen very frequently. This is where the limits on background location gathering in "O" kick in.

While your app is in the foreground, you should receive location updates as frequently as you requested. When your app goes in the background, your app will receive location updates only a few times each hour (the location update interval may be adjusted in the future based on system impact and feedback from developers).

In this sample, you've requested location updates every 30 seconds (collected every 10 seconds and delivered batched). See the UPDATE_INTERVAL and MAX_WAIT_TIME values in MainActivity.java, which specifies the desired interval for location updates:

private static final long UPDATE_INTERVAL = 10 * 1000;
private static final long MAX_WAIT_TIME = UPDATE_INTERVAL * 3;

When your app is in the foreground, it should receive updates approximately at the frequency specified. The "O" background location limits only kick in when your app is no longer in the foreground.

In the previous step, you got the app up and running and received both foreground and background location updates.

In this step, you'll upgrade the targetSdkVersion to "O", see how this changes you app's ability to receive location updates, and you'll reimplement your location strategy to use a BroadcastReceiver.

So far, we've set our targetSdkVersion to N. Let's change this. Open app/build.gradle and update your compileSdkVersion and targetSdkVersion as shown below:

android {
   compileSdkVersion "android-O"
   defaultConfig {
       ...
       targetSdkVersion "O"
       ...
   }
   ...
}

Run the app

Sync and run your app.

Your app should run the same as before while in the foreground. I.e., you should receive location updates as you specified in the UPDATE_INTERVAL value. And you should still see a notification with location data.

However, the notification will likely not appear. This is unrelated to location gathering; rather, Android "O" required that all notifications go through a channel. You will need to slightly modify the notification code in LocationResultHelper.java to get "O" notifications.

Using a Notification Channel

Open LocationResultHelper.java and add the following import:

import android.app.NotificationChannel;

The LocationResultHelper constructor currently looks like this:

LocationResultHelper(Context context, List<Location> locations) {
   mContext = context;
   mLocations = locations;
}

Add the code related to creating a NotificationChannel to the constructor:

LocationResultHelper(Context context, List<Location> locations) {
   mContext = context;
   mLocations = locations;

   NotificationChannel channel = new NotificationChannel(PRIMARY_CHANNEL,
           context.getString(R.string.default_channel), NotificationManager.IMPORTANCE_DEFAULT);
   channel.setLightColor(Color.GREEN);
   channel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
   getNotificationManager().createNotificationChannel(channel);
}

Finally, still in LocationResultHelper.java, locate the showNotificationMethod() method. You'll be modifying the code for creating a Notification.Builder, which looks like this:

void showNotification() {
   ...

   Notification.Builder notificationBuilder = new Notification.Builder(mContext)
           .setContentTitle(getLocationResultTitle())
           .setContentText(getLocationResultText())
           .setSmallIcon(R.mipmap.ic_launcher)
           .setAutoCancel(true)
           .setContentIntent(notificationPendingIntent);


   ...
}

All you need to do now is add the PRIMARY_CHANNEL notification channel as an argument to the call to create a builder (this constant was already defined for you):

void showNotification() {
   ...

   Notification.Builder notificationBuilder = new Notification.Builder(mContext, DEFAULT_CHANNEL)
           ...
   ...
}

Run the app now, and you should be able to see the notification.

Running your app in the background

Your app appears to be working in the foreground. Now test how it works in the background.

Swipe away the main activity.

With the activity gone you will no longer receive location updates (this may be a little hard to see, since background location updates happen only a few times an hour. You'll have to trust the author :)).

The updates stop because you are now targeting "O", and your app is further subject to limits on services started in the background.

Here are strategies for targeting "O" and requesting location updates:

Let's reimplement our location strategy to use a PendingIntent.getBroadcast() with a BroadcastReceiver. This BroadcastReceiver has already been defined in AndroidManifest.xml.

First, open MainActivity.java and redefine getPendingIntent() so that it looks like this:

private PendingIntent getPendingIntent() {
   Intent intent = new Intent(this, LocationUpdatesBroadcastReceiver.class);
   intent.setAction(LocationUpdatesBroadcastReceiver.ACTION_PROCESS_UPDATES);
   return PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
}

Instead of using a PendingIntent with a service, you're now using it with a BroadcastReceiver.

Now, open LocationUpdatesBroadcastReceiver.java, which contains an empty onReceive() method:

@Override
public void onReceive(Context context, Intent intent) {
  
}

The onReceive() method runs when the app receives a broadcast, which in our case contains location data. Update this method so it looks like this:

@Override
public void onReceive(Context context, Intent intent) {
   if (intent != null) {
       final String action = intent.getAction();
       if (ACTION_PROCESS_UPDATES.equals(action)) {
           LocationResult result = LocationResult.extractResult(intent);
           if (result != null) {
               List<Location> locations = result.getLocations();
               LocationResultHelper locationResultHelper = new LocationResultHelper(
                       context, locations);
               // Save the location data to SharedPreferences.
               locationResultHelper.saveResults();
               // Show notification with the location data.
               locationResultHelper.showNotification();
               Log.i(TAG, LocationResultHelper.getSavedLocationResult(context));
           }
       }
   }
}

The code should look familiar, and it parallels the logic you implemented in onHandleIntent() in LocationUpdatesIntentService. Once again, you process the locations received, write data to SharedPreferences so that MainActivity's UI gets updated, and you created a notification which contains the location data.

Run the app.

You should receive location updates in the foreground as before. When you swipe away the main activity, your app will continue to receive location updates (albeit slowly, since background location limits are still in place).

Congratulations, you've built an app that that successfully receives location updates in the foreground and background!!

And you built the app first targeting Android "N" and then Android "O".

And you've seen how Android "O" limits how frequently your app can gather location in the background, regardless of the app's target SDK version.

Summary of "O" changes

Here is a summary of the "O" background location limits:

  1. Background apps will receive location updates only a few times each hour (the location update interval may be adjusted in the future based on system impact and feedback from developers).
  2. Foreground apps are not affected by these limits.
  3. These background limits apply to all apps running on an O device, regardless of the target SDK.
  4. Apps targeting O are further subject to limits on services started in the background. For this reason, apps targeting O should not use PendingIntent.getService() when requesting location updates. Instead, they should use PendingIntent.getBroadcast().

Some alternative to consider

So, given these limits, what are good options for developers who want more frequent location updates?

Use foreground updates

Request updates in the foreground. This means requesting and removing updates as part of the activity lifecycle (request in onResume() and remove in onPause(), for example). Apps running in the foreground are not subject to any location limits on O devices.

Use a foreground service

Request updates using a foreground service. This involves displaying a non-dismissable, persistent notification to users. While this may make sense for some use cases, developers should be thoughtful about using foreground services and what the messaging to the user should be. If this doesn't make sense for your use case, consider the other strategies below.

See foreground service sample.

Use Geofencing

Use geofencing to trigger notifications based on the device's location. If your use case relies on the device entering, dwelling, or exiting a particular area of interest, this API provides a performant way to get these notifications. This approach is more efficient than checking location at regular intervals, as the API optimizes based on the user's activity and proximity to the geofence. See the GeofencingEvent#getTriggeringLocation, which gets the location that triggered the geofence transition.

See geofencing sample.

Some tips when requesting location

Here are some tips for optimizing location requests to be battery efficient.

Use batched location updates

Use batched location updates using LocationRequest#setMaxWaitTime. With this API, locations may be provided more frequently than the non-batched API.

Consume passive location updates

While your app is in the background, it may continue to receive location updates passively if another app in the foreground requests location updates. You can receive some of these updates by using LocationRequest#setFastestInterval with a small interval, such as 5 min.

Backport your changes

Finally, consider backporting any changes to your app to pre-O devices. For example, if geofencing provides an equivalent customer experience as a previously fixed background location request, update your approach for all versions of Android, not just O. This simplifies your codebase to a single version and provides system health benefits to users on pre-O devices as well!