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.
This chapter assumes that you have read the preceding chapter on location-based services, along with that chapter’s prerequisites.
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.
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.
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.
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
}
}
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.
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);
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);
}
}
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();
}
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();
}
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:
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);
}
}
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:
isInResolution
field, initially set to false
Bundle
: @Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(STATE_IN_RESOLUTION, isInResolution);
}
Bundle
in onCreate()
, and only make
the LocationSettingsRequest
if we are not already handling one from a
configuration change: @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);
}
}
isInResolution
back to false
when we handle the resolution in
onActivityResult()
And, after all of that, either we will call findLocation()
or our activity
will be finished.
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);
}
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.
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 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.
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);
}
Here, we indicate that the LocationRequest
:
setNumUpdates(1)
)setExpirationDuration(60000)
)setInterval(5000)
)setPriority(LocationRequest.PRIORITY_LOW_POWER)
)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.
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);
});
}
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
}
};
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();
}
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.