Location / Uncategorized

LocationServices

Many apps offer functionality based upon the location of the device running the app. The tools for obtaining device location have been around since API 1, but they have evolved a little over the years – one big change was moving them in to Play Services. However, this change came with a cost: Establishing a GoogleApiClient instance is a potentially blocking operation which you really do not want to be doing on the main thread. This usually requires some kind of asynchronous operation to obtain that instance and initialise the connection. This requires a chunk of extra code and introduces complexity which can easily introduce bugs. But things have become a whole lot easier, and in this article we’ll look at the new LocationServices APIs to see how to use them, but also check out the performance impact we can expect.

The process of obtaining device location really has been vastly simplified. For those already familiar with the APIs they are pretty much the same as before, only with much of the boilerplate removed. To use the new APIs requires Play Services V11.0.0 or later.

For our example, we’re going to register for location updates from the Fused Location Provider within an Activity. If you can understand this example then hopefully doing the same for other location types (such as Geofencing) will be easy to work out. Also, you may need to approach things slightly differently if you’re doing this within a Service.

I won’t bother covering the Activity class which is mainly concerned with obtaining the necessary location permissions – it is the LocationFragment where all of the interesting stuff is happening (in the context of this article, at least). LocationFragment will not be created until all of the necessary permissions have been obtained, so we do not have any need for permission checks in the Fragment itself.

The majority of LocationFragment is concerned with initialising the UI, and updating the values of the TextView instances in the layout:

public class LocationFragment extends Fragment {
    private static final String FRACTIONAL_FORMAT = "%.4f";
    private static final String ACCURACY_FORMAT = "%.1fm";

    private TextView latitudeValue;
    private TextView longitudeValue;
    private TextView accuracyValue;

    private FusedLocationProviderClient locationProviderClient = null;

    @Override
    public void onStart() {
        super.onStart();
        registerForLocationUpdates();
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_location, container, false);
        latitudeValue = (TextView) view.findViewById(R.id.latitude_value);
        longitudeValue = (TextView) view.findViewById(R.id.longitude_value);
        accuracyValue = (TextView) view.findViewById(R.id.accuracy_value);
        return view;
    }

    @Override
    public void onStop() {
        unregisterForLocationUpdates();
        super.onStop();
    }

    void updatePosition(Location location) {
        String latitudeString = createFractionString(location.getLatitude());
        String longitudeString = createFractionString(location.getLongitude());
        String accuracyString = createAccuracyString(location.getAccuracy());
        latitudeValue.setText(latitudeString);
        longitudeValue.setText(longitudeString);
        accuracyValue.setText(accuracyString);
    }

    private String createFractionString(double fraction) {
        return String.format(Locale.getDefault(), FRACTIONAL_FORMAT, fraction);
    }

    private String createAccuracyString(float accuracy) {
        return String.format(Locale.getDefault(), ACCURACY_FORMAT, accuracy);
    }
    .
    .
    .
}

The bit that is actually concerned with the location is actually pretty small:

public class LocationFragment extends Fragment {
    .
    .
    .
    private FusedLocationProviderClient fusedLocationProviderClient = null;
    .
    .
    .
    @DebugLog
    @SuppressLint("MissingPermission")
    void registerForLocationUpdates() {
        FusedLocationProviderClient locationProviderClient = getFusedLocationProviderClient();
        LocationRequest locationRequest = LocationRequest.create();
        Looper looper = Looper.myLooper();
        locationProviderClient.requestLocationUpdates(locationRequest, locationCallback, looper);
    }

    @DebugLog
    @NonNull
    private FusedLocationProviderClient getFusedLocationProviderClient() {
        if (fusedLocationProviderClient == null) {
            fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(getActivity());
        }
        return fusedLocationProviderClient;
    }

    @DebugLog
    void  unregisterForLocationUpdates() {
        if (fusedLocationProviderClient != null) {
            fusedLocationProviderClient.removeLocationUpdates(locationCallback);
        }
    }

    private LocationCallback locationCallback = new LocationCallback() {
        @DebugLog
        @Override
        public void onLocationResult(LocationResult locationResult) {
            super.onLocationResult(locationResult);
            Location lastLocation = locationResult.getLastLocation();
            updatePosition(lastLocation);
        }
    };
}

There’s not actually that much to explain here, but we’ll go through it in detail nonetheless. The major change in the API is what’s done in registerForLocationUpdates(). Previously we would have had to obtain a GoogleApiClient instance which would have involved implementing a callback interface to handle the asynchronous connection to Play Services. That is a thing of the past, as the new mechanism is just much cleaner and easier to implement.

First we need to obtain a FusedLocationProviderClient instance which we do using LocationServices.getFusedLocationProviderClient(getActivity()). We can then register for location updates from this instance.

The big difference here is that previously we would have to establish a connection to Play Services before we could register for updates. Now we can get a FusedLocationProviderClient instance quite cheaply, without having to first connect to Play Services. When we register for updates, fusedLocationProviderClient will make the connection to Play Services in the background, and we’ll start receiving location updates once that connection has been established, and we’ll start receiving callbacks to onLocationResult() once that has all been done.

So that certainly makes life easier for us as developers, but what is the cost? To benchmark things I have used Hugo, Jake Wharton’s method call logging library. By wrapping all of the calls in simple methods, and annotating those methods with @DebugLog we can see the time take to execute each:

06-17 10:57:49.677 17027-17027/com.stylingandroid.location.services V/LocationFragment: ⇢ registerForLocationUpdates()
06-17 10:57:49.677 17027-17027/com.stylingandroid.location.services V/LocationFragment: ⇢ getFusedLocationProviderClient()
06-17 10:57:49.702 17027-17027/com.stylingandroid.location.services V/LocationFragment: ⇠ getFusedLocationProviderClient [23ms] = com.google.android.gms.location.FusedLocationProviderClient@3738105
06-17 10:57:49.708 17027-17027/com.stylingandroid.location.services V/LocationFragment: ⇠ registerForLocationUpdates [30ms]
06-17 10:57:49.992 17027-17027/com.stylingandroid.location.services V/LocationFragment: ⇢ onLocationResult(locationResult=LocationResult[locations: [Location[fused XX.XXXXXX,-X.XXXXXX acc=20 et=+XdXhXXmXXsXXXms]]])
06-17 10:57:49.996 17027-17027/com.stylingandroid.location.services V/LocationFragment: ⇠ onLocationResult [3ms]
06-17 10:59:21.153 17027-17027/com.stylingandroid.location.services V/LocationFragment: ⇢ unregisterForLocationUpdates()
06-17 10:59:21.154 17027-17027/com.stylingandroid.location.services V/LocationFragment: ⇠ unregisterForLocationUpdates [0ms]

So we can see that the getFusedLocationProviderClient is taking 30ms, and registerForLocationUpdates is taking another 23ms. We wouldn’t want to be making these calls during an animation because we would start dropping frames, but doing this on the main thread during Fragment initialisation is giving us no danger of ANRs.

It takes another 288ms before we receive a location update, but we are not waiting on the main thread for this – we get an async callback with the location update, so this is not going to impact us.

For completeness in production code I would add some additional checking to verify that things are all working correctly, and report an error if they’re not, but I’ve omitted that in order to keep the example code clean and easy to understand. To do this I would also implement the onLocationAvailability() method of LocationCallback which will provide updates when the availability of location data changes.

There will be some developers cringing at this example code because they use different location providers for different build variants (for example, for builds which cannot use Play Services). LocationFragment is tightly coupled to Play Services in order to demonstrate these new Play Services API changes. However, a future article will look at ways of abstracting our code so that we can easily plug in different location providers.

The source code for this article is available here.

© 2017, Mark Allison. All rights reserved.

Copyright © 2017 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.

5 Comments

  1. Hey, Mark – great work – can you share your approach on real services, like background services using Location services? There will be huge changes to policy for such a services, so it would be good to share some thoughts on that…

    1. It will be pretty much the same in those cases. For example FusedLocationProviderClient has two variants of the getFusedProviderClient() method. The first takes an Activity as an argument, but the second takes a Context. Just use the second form in your Service implementation (Service extends Context). Then use it in exactly the same way.

  2. Hey, Mark. I have implemented Location service using new Location APIs. It seems that there is no method that takes LocationListener, so I use LocationCallback (like in your example). But it only triggers 2 times after launching. I have even tried using PendingIntent approach, but get the same.

  3. fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(getActivity());
    I have called this in onCreate(), but it returns null? Is there anything I need to call before this?
    Or some other code is required if this returns null?

    1. Are you sure that it isn’t getActivity() which is returning null? In my code I do this in onStart() because the Fragment is fully attached to the Activity by that point in its lifecycle.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.