The Fused Location Provider

At the 2013 Google I|O conference, Google announced an update to Google Play Services that offers a “fused location provider”, one that seamlessly uses all available location data to give you as accurate of a location as possible, as quickly as possible, with as little power consumption as possible. This serves as an adjunct to the traditional LocationManager approach for finding one’s position. The fused location provider has a different API, though one that is similar in some respects to the LocationManager API. However, this provider is part of the Play Services SDK, not part of Android itself.

In this chapter, we will examine how to use the fused location provider, in its latest incarnation, sporting a new API that debuted in 2017.

Prerequisites

This chapter assumes that you have read the preceding chapter on location-based services, along with that chapter’s prerequisites.

Why Use the Fused Location Provider?

The traditional recipes for using location providers are a bit complicated, if you want to maximize results. Simply asking for a GPS fix is not that hard, but:

The fused location provider is designed to address these sorts of concerns. Its implementation will blend data from GPS, cell tower triangulation, and WiFi hotspot proximity to determine the device’s location, without your having to manually set all of that up. The fused location provider will also take advantage of sensor data, so it does not try to update your location as frequently if the accelerometer indicates that you are not moving.

The net result is better location data, delivered more quickly, with (reportedly) less power consumption.

Why Not Use the Fused Location Provider?

The fused location provider is part of Google Play Services. Google Play Services is available on hundreds of millions of Android devices. However:

If you are aiming to distribute your app solely through the Play Store, relying upon the Play Services framework is reasonable. If, however, you are distributing through other channels, you will either need to conditionally use the fused location provider on devices that offer it, or avoid the fused location provider entirely, falling back to the traditional LocationManager solution.

Finding Our Location, Once

This section will review the Location/LocationServices sample application, which is akin to the Location/Classic sample application from the previous chapter, revised to use the fused location provider to get a one-off weather forecast.

Adding Dependencies

Currently, the Play Services SDK is distributed through Google’s Maven repository, the same as with the Support Library. Most likely, your Android Studio project is already set up to use that, so all you need is to add an entry in your dependencies closure that pulls in the com.google.android.gms:play-services-location artifact:

apply plugin: 'com.android.application'

dependencies {
    implementation 'com.google.android.gms:play-services-location:11.8.0'
    implementation 'com.android.support:support-v4:27.1.0'
    implementation 'com.squareup.picasso:picasso:2.5.2'
    implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
    implementation 'com.squareup.okhttp3:okhttp:3.9.1'
}

android {
    compileSdkVersion 27

    defaultConfig {
        minSdkVersion 15
        targetSdkVersion 27
        applicationId 'com.commonsware.android.weather2.fused'
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}
(from Location/LocationServices/app/build.gradle)

Deal With Runtime Permissions

As with the Location/Classic sample, we need to handle runtime permissions. And, as with the Location/Classic sample, we use an AbstractPermissionActivity to handle all of the permission-related bits, so we can focus on the task at hand.

Confirm Locations Are Available

It is entirely possible that the user has disabled location access on the device. If so, we are doomed.

Hence, it would be nice to know if we are doomed. Even better would be to ask the user if they would enable location access, so our app can be moderately less doomed.

This is possible, but it works a bit like how runtime permissions works, which means that it is a pain.

As part of our work in onReady() — after we have obtained the location runtime permission from the user — we create a LocationSettingsRequest via a Builder:

      LocationSettingsRequest request=new LocationSettingsRequest.Builder()
        .addLocationRequest(LocationRequest.create())
        .build();
      LocationServices.getSettingsClient(this)
        .checkLocationSettings(request)
        .addOnCompleteListener(this::handleSettingsResponse);
(from Location/LocationServices/app/src/main/java/com/commonsware/android/weather2/WeatherDemo.java)

A LocationSettingsRequest makes a request of Play Services to find out if the location settings on the device matches the requirements of a supplied LocationRequest. As it turns, we do not have a LocationRequest elsewhere — we will see that coming up later in this chapter, when we start requesting periodic location updates. So, we create a fairly basic LocationRequest via LocationRequest.create() and supply that to the Builder as part of building the LocationSettingsRequest.

Then, we call LocationServices.getSettingsClient() and ask it to checkLocationSettings(). We arrange to have a handleSettingsResponse() method on our activity be called when the settings request has completed, via addOnCompleteListener() and a Java 8 method reference.

The job of the handleSettingsResponse() method is to either proceed with requesting the location or asking the user to enable locations:

  private void handleSettingsResponse(Task<LocationSettingsResponse> task) {
    try {
      LocationSettingsResponse response=task.getResult(ApiException.class);
      LocationSettingsStates states=response.getLocationSettingsStates();

      if (states.isLocationPresent() && states.isLocationUsable()) {
        findLocation();
      }
      else {
        unavailable();
      }
    }
    catch (ApiException e) {
      copeWithFailure(e);
    }
  }
(from Location/LocationServices/app/src/main/java/com/commonsware/android/weather2/WeatherDemo.java)

The somewhat strange API that Play Services offers here has us call getResult() on the supplied Task object, and from there call getLocationSettingsStates() to get a LocationSettingsStates object. There are three basic possibilities at this point.

First, we could find out that locations seem to be usable, via checks of isLocationPresent() and isLocationUsable(). In that case, we can request our location, which is handled by a findLocation() method that we will examine shortly.

Second, we could find out that locations are not usable at this point. In that case, we call an unavailable() method, which just shows a Toast and exits the activity:

  private void unavailable() {
    Toast.makeText(this, R.string.msg_not_avail, Toast.LENGTH_LONG)
      .show();
    finish();
  }
(from Location/LocationServices/app/src/main/java/com/commonsware/android/weather2/WeatherDemo.java)

The third possibility is that our call to getResult() throws an ApiException. In that case, we call copeWithFailure() to examine that exception:

  private void copeWithFailure(Exception e) {
    if (e instanceof ResolvableApiException) {
      try {
        ((ResolvableApiException)e).startResolutionForResult(this, REQUEST_RESOLUTION);
        return;
      }
      catch (IntentSender.SendIntentException e1) {
        e=e1;
      }
    }

    Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
    Log.e(getClass().getSimpleName(), "Exception getting location", e);
    finish();
  }
(from Location/LocationServices/app/src/main/java/com/commonsware/android/weather2/WeatherDemo.java)

If the exception is a ResolvableApiException, then while there is a problem, it is something that the user can address. Typically, this will be if locations are not enabled in Settings, as the user can fix that. If that is what we receive, we call startResolutionForResult() on the ResolvableApiException. Under the covers, this calls startActivityForResult() to display a Play Services-supplied activity, typically to ask the user to enable location settings:

Location Settings Dialog
Figure 830: Location Settings Dialog

If the exception is not a ResolvableApiException, or we run into a problem calling startResolutionForResult(), we show a Toast and bail out of the activity.

So, that this point, if our activity is still around, either we can findLocation() or we will be called with onActivityResult(), to find out the results of the problem-resolution process that we triggered with startResolutionForResult().

onActivityResult() looks at the result code and calls findLocation() or unavailable() depending on what we got:

  @Override
  protected void onActivityResult(int requestCode, int resultCode,
                                  Intent data) {
    if (requestCode==REQUEST_RESOLUTION) {
      isInResolution=false;

      if (resultCode==RESULT_OK) {
        findLocation();
      }
      else {
        unavailable();
      }
    }
    else {
      super.onActivityResult(requestCode, resultCode, data);
    }
  }
(from Location/LocationServices/app/src/main/java/com/commonsware/android/weather2/WeatherDemo.java)

You will notice, though, that we set an isInResolution field to false. startResolutionForResult() behaves much like requestPermissions() in the runtime permission system: it displays a dialog-themed activity from another app. If we undergo a configuration change while that activity is in the foreground, since our activity is still visible, our activity will be destroyed and recreated. In that case, we do not want to start this whole process again, as we will wind up with a second dialog.

So, we use a similar recipe to the one that AbstractPermissionActivity uses for dealing with requesting permissions when the activity starts up:

  @Override
  protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

    outState.putBoolean(STATE_IN_RESOLUTION, isInResolution);
  }
(from Location/LocationServices/app/src/main/java/com/commonsware/android/weather2/WeatherDemo.java)

  @Override
  protected void onReady(Bundle state) {
    if (state!=null) {
      isInResolution=state.getBoolean(STATE_IN_RESOLUTION, false);
    }

    fragment=
      (WeatherFragment)getSupportFragmentManager().findFragmentById(android.R.id.content);

    if (fragment==null) {
      fragment=new WeatherFragment();
      getSupportFragmentManager().beginTransaction()
        .add(android.R.id.content, fragment).commit();
    }

    if (!isInResolution) {
      isInResolution=true;
      LocationSettingsRequest request=new LocationSettingsRequest.Builder()
        .addLocationRequest(LocationRequest.create())
        .build();
      LocationServices.getSettingsClient(this)
        .checkLocationSettings(request)
        .addOnCompleteListener(this::handleSettingsResponse);
    }
  }
(from Location/LocationServices/app/src/main/java/com/commonsware/android/weather2/WeatherDemo.java)

And, after all of that, either we will call findLocation() or our activity will be finished.

Request the Location

So, eventually, we wind up at findLocation():

  private void findLocation() {
    FusedLocationProviderClient client=
      LocationServices.getFusedLocationProviderClient(this);

    client.getLastLocation()
      .addOnCompleteListener(this, this::useResult)
      .addOnFailureListener(this, this::copeWithFailure);
  }
(from Location/LocationServices/app/src/main/java/com/commonsware/android/weather2/WeatherDemo.java)

The Play Services SDK offers a LocationServices class, which has a getFusedLocationProviderClient() method. This gives us a FusedLocationProviderClient instance that we can use to find out the current location.

However, calling getLastLocation() on the FusedLocationProviderClient does not hand over the current location. The Play Services SDK talks to a separate Play Services Framework app, which in turn handles all of the Play Services work. That app may or may not be running, and even if it is, there may or may not be a current location. So, getLastLocation() returns a Task object instead.

The two big things that you can do with a Task is call addOnCompletionListener() and addOnFailureListener(). The former is called if your request succeeds, and in this case it gives us a Location via getResult() on the passed-in Task that we can hand over to the fragment. The latter is called if there is some unrecoverable problem, where we are handed an Exception to use to inform the user about what went wrong. In this case, we are using lambda expressions to replace the OnCompletionListener and OnFailureListener interfaces.

You will notice that we did not use addOnFailureListener() with the previous Task usage: the checkLocationSettings() call. There, we just used addOnCompleteListener(). There is nothing stopping you from adding a failure listener there. However, due to what may be bugs in Play Services, for a recoverable problem (e.g., locations not enabled), both the completion listener and the failure listener get called. You might accidentally wind up calling startResolutionForResult() in both cases, which will give you two dialogs. Since we have to catch the ApiException in the completion listener anyway, in the checkLocationSettings() scenario we only use the completion listener.

Both addOnCompletionListener() and addOnFailureListener() have a few different flavors; in this case, we are using ones that take an Activity as the first parameter. This causes the Play Services SDK to pay attention to the activity lifecycle, and if the activity is stopped before the request is completed, the Play Services SDK will abandon the request.

Using the Location

WeatherFragment has a subset of the original sample’s logic, mostly focused on fetchForecast() and the work to display the weather forecast. WeatherFragment itself no longer has any logic to get the location itself – it relies on the activity to push over the location when it is ready.

Similarly, WeatherFragment is no longer retained. On a configuration change, the activity and fragment start over from scratch. That is simple but not the most efficient option. A production app probably has both the location retrieval and the forecast Web service call be managed by some sort of “repository” object that is independent of the activity/fragment lifecycle. Coverage of the repository pattern can be found in the companion volume, Android’s Architecture Components.

Getting Periodic Locations

Getting periodic location data is only incrementally more complex than is getting a single update. While this particular sample app does not need periodic updates, we can still use it to experiment with the API. The Location/LocationPeriodic sample application is a clone of the Location/LocationServices sample, modified to request “periodic” updates… though we will only use the first one.

Defining a Location Request

Core to both finding out whether we can use the fused location provider, and later getting location fixes, will be to define a LocationRequest object. This is a pure POJO, without any ties to any Context or other existing Play Services SDK objects:

  private LocationRequest buildLocationRequest() {
    return new LocationRequest()
      .setNumUpdates(1)
      .setExpirationDuration(60000)
      .setInterval(5000)
      .setPriority(LocationRequest.PRIORITY_LOW_POWER);
  }
(from Location/LocationPeriodic/app/src/main/java/com/commonsware/android/weather2/WeatherDemo.java)

Here, we indicate that the LocationRequest:

The setInterval() call may seem odd, given that we are only seeking one fix. Leaving this out, though, means that you may never get a fix, for unclear reasons.

Also, while we are requesting PRIORITY_LOW_POWER, and we do not need a particularly accurate fix just to get a weather forecast, we still request ACCESS_FINE_LOCATION in the manifest. Without this, once again we seem to never get a fix.

Another issue comes with the expiration value. setExpirationDuration() calculates the expiration time based on when the LocationRequest object is created, not when it is used. If we declared this as a constant with a static initializer, our LocationRequest would be created when the WeatherDemo class is loaded. However, if we do not hold the runtime permissions, we cannot request location updates without user interaction, and that may take some time, which eats into our requested duration. So, instead, we wait to create the LocationRequest until we can use it, as we will see shortly.

Requesting Location Updates

We use buildLocationRequest() in the new findLocation() method:

  private void findLocation() {
    request=buildLocationRequest();

    client.requestLocationUpdates(request, cb, Looper.getMainLooper())
      .addOnFailureListener(this, e -> {
        Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
        Log.e(getClass().getSimpleName(), "Exception getting location", e);
      });
  }
(from Location/LocationPeriodic/app/src/main/java/com/commonsware/android/weather2/WeatherDemo.java)

We use the same FusedLocationProviderClient object as before. This time, we call a requestLocationUpdates() method on the FusedLocationProviderClient. There are two flavors of this method, both taking the LocationRequest as the first parameter. The flavor that we are using here takes a LocationCallback and a Looper as the other parameters. The other flavor takes a PendingIntent and is designed for background use, where your process might not be around at the time the location request is ready and timely.

The LocationCallback is defined here as a cb field:

  private final LocationCallback cb=new LocationCallback() {
    @Override
    public void onLocationResult(LocationResult locationResult) {
      if (fragment!=null && locationResult.getLastLocation()!=null) {
        fragment.fetchForecast(locationResult.getLastLocation());
      }
    }

    @Override
    public void onLocationAvailability(LocationAvailability avail) {
      super.onLocationAvailability(avail);

      // unused
    }
  };
(from Location/LocationPeriodic/app/src/main/java/com/commonsware/android/weather2/WeatherDemo.java)

In onLocationResult(), we get a LocationResult, rather than a simple Location object as we did before. Regardless, we can get that Location via getLastLocation(). If we have our fragment and we have our location, we tell the fragment to fetch the forecast for that location. The onLocationAvailability() method would let us know if our ability to obtain locations changes (e.g., user disables GPS); for this sample, we ignore that.

The Looper indicates the thread on which we want to receive the onLocationResult() call. In this case, we want to get that call on the main application thread, so we use Looper.getMainLooper() to get the Looper tied to that thread. Otherwise, we could fork a separate HandlerThread and use its Looper to get calls delivered on that particular thread.

If you call requestLocationUpdates(), you also need to call removeLocationUpdates() passing in the same LocationCallback object (or an equivalent PendingIntent to the original one, if you used that flavor of requestLocationUpdates()). In this case, we do that in onStop(), and we re-request location updates in onStart() if we have our FusedLocationProviderClient object already:

  @Override
  protected void onStart() {
    super.onStart();

    if (client!=null && request==null) {
      findLocation();
    }
  }

  @Override
  protected void onStop() {
    client.removeLocationUpdates(cb);
    request=null;

    super.onStop();
  }
(from Location/LocationPeriodic/app/src/main/java/com/commonsware/android/weather2/WeatherDemo.java)

We use the request object as a flag to indicate whether or not we need to request the updates. For example, if onReady() is triggered as a part of onCreate(), we will have requested location updates already by the time onStart() is called.

As before, this sample app is less-than-optimal in its handling of configuration changes. Everything works, but we wind up re-requesting location updates on each configuration change.