Press "Enter" to skip to content

Android Location API (w/Kotlin)

0

Adding Location-based support to your app can make it much more attractive to your users. You can for example, show the current address, track your user location and show a notification when entering (or leaving) in a specific location, detect whether user is walking or running and take an appropriate action depends on the result, and much more. Google Play Services provides a set of APIs that make it easy to add location awareness to an app.

The main idea of this article is to show some of the features provided by the Location API. We also created a demo app that implements all the examples demonstrated on this article. You can download it here.

This is what we will discuss throughout this article (and you also find in the demo app):

  • Location Tracking
    • Get the last known location
    • Receive location updates
    • Display current location address (reverse geocoding)
  • Activity Recognition
    • Detect the most probably activity you are doing (e.g.: walking, driving a car, etc)
  • Geofencing
    • Create geofences
    • Monitor geofences by adding a notification when you enter and leave in a geofence

First Steps

In order to call any API from the Google Play Services, we need to install the Google Play services for the Android SDK. We are assuming it is already installed on your system. You can check this link for details.

Then, we need to add Play Services dependencies in the build.gradle. In our case, since we just want to use Location API, we simply add the line below to the grade file. Check the official SDK setup instructions to find the latest version for individual APIs.

To get access to the Google Play Services, we need to use the GoogleApiClient object. GoogleApiClient object is the the main entry point for the Google Play services integration. It can queue requests when offline, and execute them in order when a connection is available. Also, according to the docs, “…GoogleApi objects are cheap to create, so you can instantiate them as needed to access Google services.”. Before any operation is executed, the GoogleApiClient must be connected.

We can start either a connection manually or automatically. The advantage of the auto-managed connection is that GoogleApiClient instance will automatically connect after your activity calls onStart() and disconnect after calling onStop(). It will also automatically display UI to attempt to fix any resolvable connection failures.

There might be cases where we want to manually manage connections. If that is the case, we need to implement two callback interfaces: ConnectionCallbacks and OnConnectionFailedListener. They both will receive callbacks in response to the asynchronous connect() method when the connection to Google Play services succeeds, fails, or becomes suspended. We need also to call the connect() and disconnect() methods manually. Usually we call them in the activity’s onStart() method and disconnect() in your activity’s onStop() method, but it really depends on your app needs.

Note: On the demo app we use the manual managed connection.

In the snippet below we show what we’ve just explained. Since it uses a manual connection, BaseFragment implements both ConnectionCallbacks and OnConnectionFailedListener interfaces. Inside onCreate() method, we build the GoogleApiClient object by passing those interfaces as well as all the service-specific APIs we want to use. Later, inside onStart() we try to connect to the Google Play services client library.

Once it is connected, onConnected() callback method is invoked, then we can start making calls to the service-specific APIs we specified to the GoogleApiClient instance.

If something goes wrong while trying to connect, onConnectionFailed() is called. This error can be either recoverable or not, and hasResolution() method provides such information. In case of a recoverable error (e.g. missing permission, user needs to sign in, etc), we can show a dialog requiring user interaction by calling startResolutionForResult() method. Basically it behaves the same as startActivityForResult() in this situation, and launches an activity appropriate to the context that helps the user to resolve the error (such as an activity that helps the user to select an account).

The result of this interaction will be handled by the onActivityResult() method. If the resultCode is RESULT_OK, the application should try to connect again.

In case of a not recoverable error we can call GoogleApiAvailability.getErrorDialog() method that returns a Dialog provided by Google Play services containing a message that explains the error. You can check a list of all error codes here.

Later, if at any time the connection drops, onConnectionSuspended() is called, and we need to ensure we will not call any API while in this state.

Note that we connect and disconnect to the Google Play Services inside onStart and onStop respectively. This will ensure we will still be able to communicate to the services even when our activity is in background (e.g. while reading a notification). This is not mandatory. Depends on your needs, you may call them in onPause and onResume, or whatever method you want. It is up to you.

Under the Hood

When working with the Google Play Services, there are two important pieces: Google Play Services client library and Google Play Services APK. The first one is a library we add to our projects that contains the interfaces to the individual Google services and allows us to call them on our code. The APK is a service that runs as a background service in the Android OS and contains the individual Google services which we interact with through the client library. Then the service carries out the actions on your behalf.

The Google Play services APK is delivered through the Google Play Store, so updates to the services are not dependent on carrier or OEM system image updates.

The picture below illustrate it better.

Google Play Services and Runtime Permissions

It is important to note that in Android 6.0+ permissions are now requested at runtime instead of before app installation. Additionally, users can choose to deny specific permissions, so we need to be prepared to handle errors in cases a user has denied a permission required for an API your app uses.

Google Play services 8.1+ support runtime permissions (in Android 6.0+). This means that before calling any Google Play services API, we need to ensure we have the required permission. It is out of the scope of this article to explain how it works, but you can find a very detailed explanation on this link.

User-Permissions

When working with location, there are some permissions we need to add to the project. According to the docs, Android offers two location permissions:

  • ACCESS_COARSE_LOCATION – Allows the API to use WiFi or mobile cell data (or both) to determine the device’s location. The API returns the location with an accuracy approximately equivalent to a city block.
  • ACCESS_FINE_LOCATION – Allows the API to determine as precise a location as possible from the available location providers, including the Global Positioning System (GPS) as well as WiFi and mobile cell data.

The permission you choose determines the accuracy of the location returned by the API. You only need to request one of the Android location permissions, depending on the level of accuracy you need.

Location Tracking

Now that we have a basic understanding of Location API, let’s see what we provide on the demo app. The first feature is location track.

Once connected to the Play Services, if we want to get location updates, we need first to create a LocationRequest object and set the level of accuracy for location requests, by providing:

Then, we need to check whether all the location settings are satisfied. To do that, we first build a LocationSettingsRequest object and call checkLocationSettings() method from the SettingsApi.

In case of location settings are not satisfied, we can check whether this can be fix or not, and if so, show the dialog by calling startResolutionForResult() and check the result in onActivityResult(). If this cannot be fix, we could show a warning to the user and prevent accessing any API.

In case of all location settings are satisfied (and we hope so!), we can start listening for location updates by calling requestLocationUpdates() method. When we call requestLocationUpdates(), whenever location changes, the fuse location provider invokes the LocationCallback.onLocationChanged() callback that contains a Location object (that contains values for the current latitude and longitude). With this location object, we can, for example, request to the geocoder (if available) the address based on the lat/long, or maybe add a marker on a map based on the location’s lat/long, so end-users can visualise where they are.

Note that in order to receive location update, you must implement LocationListener interface. It is also a good practice to stop listening location updates when we no longer need them. Where you will do that depends on your needs, but chances are you use onPause or onStop lifecycles’methods.

Note: I recommend check this link, since depends on how you configure these location settings, you may encounter problems with UI flicker or data overflow.

The snippet below wrap up all these steps.

Geocoder

By following the previous session, you should be able to get the last known location as well as receive updates whenever location changes. At this point you should have a Location object that contains latitude and longitude coordinates. In certain cases, lat/long are enough (e.g. when you need it to provide to another service, or just to save it in a database), but there are cases you would need a more human information. Let’s imagine your app wants to show a certain location to the user. If you present him/her an information like 40.712775/-74.005973, that will probably mean nothing to most users. But, on the other hand, if you show the user an information like this: “New York City”, that will be a much more readable information. In order to achieve it, we need to use Geocoder class provided by the Location API.

Geocoder basically converts an address to the corresponding geographic coordinates as well as a geographic location to an address. The former is called geocoding and the latter reverse geocoding. When need to convert lat/long to an address, we need to call getFromLocation() method from Geocoder class.

As mentioned here, “…getFromLocation() method is synchronous and may take a long time to do its work, so you should not call it from the main, user interface (UI) thread of your app…”.

To follow the docs suggestion, we created an IntentService (FetchAddressIntentService), and when onLocationChanged() method is invoked, we call that service. Then, we can call Geocoder.getFromLocation() method that will return an array of addresses. It can contain 0 or more items (we can also control it by passing the maximum number of addresses we want when calling getFromLocation).

If something goes wrong (i.e. you do not receive any address), this could be due to several reasons. They are:

  • No location data provided –> This is the case when you did not provide a Location object in the intent extras. In this case you will get an exception (IllegalArgumentException).
  • Invalid latitude or longitude – The latitude and/or longitude values you provided are invalid. Same as before (i.e.: IllegalArgumentException exception)
  • No geocoder available – The background geocoding service is not available due to a network error or IO exception. In this case, you will get an exception (IOException).
  • Address not found – The geocoder can’t find an address for the given latitude/longitude. For this case you will get an empty array of addresses.

The snippet below show how to use reverse geocoding.

Activity Recognition

Activity Recognition is a very nice feature provided by the Play Services. We could, for example, prevent users to take certain actions while they are driving, or setup a running training and alert user if him/her stops running before a certain amount of time, since the training has not finished yet.

Basically, all we need to do is to create an IntentService that will receive the callbacks. Then,  request to the ActivityRecognitionApi API to start or stop checking users activities and send the updates to the PendingIntent. We need also to inform the interval for how often we want to receive the updates. Simple like that!

See how it works in the snippet below. We created two classes: ActivityRecognitionFragment and ActivityRecognitionIntentService.

The fragment class first creates a BroadcastReceiver so that it can receive user activity updates. Then, it creates a PendingIntent that contains our IntentService and that will be used later by the ActivityRecognitionApi to send the updates.

Once the fragment resumed, it registers the broadcast and requests to the ActivityRecognitionApi to start checking user activities every 2 seconds. This is done by calling ActivityRecognitionApi.requestActivityUpdates() method.

When the fragment is no longer visible (i.e. onPause), we request to the ActivityRecognitionApi to stop checking for user activities by simply calling removeAcitivtyUpdates() method.

Inside the ActivityRecognitionIntentService class, the code is pretty straightforward: Once it receives an user activity update, it extracts the result from the intent by calling ActivityRecognitionResult.extractResult() method and get both probableActivities and mostProbableActivity attributes from it. Then, it wrap them up in an Intent and send it via LocalBroadcastManager that will be received by the local broadcast we created in the ActivityRecognitionFragment. From there, we can do whatever we want with this information.

Note: In order to use activity recognition features, we need to add  “com.google.android.gms.permission.ACTIVITY_RECOGNITION” permission.

Geofence

According to the Wikipedia, a geofence is a virtual perimeter for a real-world geographic area.

In the Android context, this means we can create points of interesting (up to 100 per device) given a latitude and a longitude, that can trigger events when users enter and/or leave to that point. With it, we can create apps such as child location services so that parents can monitor their children when they leave an specific perimeter, or monitoring field work teams by sending specific alerts/notifications when they enter in an specific location. The possibilities are endless.

In order to do that, Android provides the Geofence API, that can be monitored by geofence service. And when the user crosses the boundary of a geofence, an alert is generated. We can also add circles to a map that represents geofences.

Next sessions will explain a step-by-step on how to do it.

Create geofence objects

Basically we need first to create a Geofence object by using the Geofence.Builder() builder. When creating it, we must provide some information as follow:

  • A request ID of the geofence. This is a string to identify the geofence.
  • A circular region you want for the geofence (latitude, longitude and radius).
  • The transition types of interest. A transition tells the service when alerts should be generated.
  • An expiration duration. Geofence will automatically be removed after this period of time.

 Geofence API provides three types of transition:

  • GEOFENCE_TRANSITION_ENTER Indicates that the user enters the geofence(s).
  • GEOFENCE_TRANSITION_EXIT  indicates that the user exits the geofence(s).
  • GEOFENCE_TRANSITION_DWELL indicates that the user enters and dwells in geofences for a given period of time. This can help reduce “alert spam” resulting from large numbers notifications when a device briefly enters and exits geofences. If GEOFENCE_TRANSITION_ENTER is also specified, this alert will always be sent after the GEOFENCE_TRANSITION_ENTER alert.

Specify geofences and initial triggers

After build a Geofence object, next step is to create a GeofencingRequest by using its nested builder (GeofencingRequest.Builder()). GeofencingRequest specifies the geofences to be monitored and how the geofence notifications should be reported. We can also inform an initial trigger that indicates to the geofencing service it should trigger a initial notification depends on a certain circumstance.

  • INITIAL_TRIGGER_DWELL – geofencing service should trigger GEOFENCE_TRANSITION_DWELL notification at the moment when the geofence is added if the device is already inside that geofence for some time.
  • INITIAL_TRIGGER_ENTER –  geofencing service should trigger GEOFENCE_TRANSITION_ENTER notification at the moment when the geofence is added if the device is already inside that geofence.
  • INITIAL_TRIGGER_EXIT –  geofencing service should trigger GEOFENCE_TRANSITION_EXIT notification at the moment when the geofence is added if the device is already outside that geofence.

Handle geofence transitions

Now we need to define an IntentService that will handle geofencing notifications. Then, we add it to a PendingIntent that is used when registering the geofence.

When Location Services detects that the user has entered or exited a geofence (depends on the transition you use), it sends out the Intent contained in the PendingIntent we included in the request to add geofences (see next step for that). This Intent contains the geofencing event that contains the transition just triggered, so we can do whatever we want, such as send a notification, send a broadcast to an activity, etc.

Add geofences  

Now it is time to request the geofence creation and listen for the result. This is done by the GeofencingApi.addGeofences() method. It accepts the GoogleApiClient, the GeofenceRequest and the PendingIntent. Once the geofence is successful created, we can, for example, add a circle in a map that represents the geofence.

Remove geofences  

When you decide you no longer need a geofence, you can remove it. Actually this should always be considered in order to save battery. All we need to do is to call removeGeofences() method. You can either remove geofences by their IDs (removeGeofences method accepts a list of IDs) or you can remove all geofences associated with a pendingIntent.

Note: if you want to remove a geofence, you need to use FLAG_UPDATE_CURRENT when creating the pending intent. This will ensure you will get the same pending intent you provide. Otherwise will not be able to remove a geofence accordingly. Details here.

See the snippet below that demonstrates how to use Geofence.

Conclusion

Adding location awareness to an app can bring a great experience to our users, and as we have seen throughout this article, with the help of the Location API, this is easier than ever.

Although Location API tries to be as efficient as possible in terms of power consumption, you should always consider enabling location services only when strictly necessary. Google defines some strategies to preserve battery power, and you should definitely take a look on them. Check here and here for some of these strategies.

We hope you have enjoyed the content of this article, and as usual, if you have any questions, please leave a comment below.