This seemed like a useful pattern so I thought I’d document it here, as it seems quite common. The scenario is that you have an application that provides some location-based service or information, but needs to gracefully handle the situation when the location cannot be obtained.
Ther are various reason why the location might not be obtainable. The most common is that the user disabled one or all location services requested by your app. Just because you request location permissions and the user accepts doesn’t mean that they actually have those services enabled. And worse, when you request a location in this situation, you don’t get any sort of indicator that it has been disabled. Your location changed callback is just never called.
My application grabs earthquake data from USGS and provides information to the user with respect to their proximity to the quake. If the app can’t get the user’s location, it should functionally normally, minuses providing distances to quake epicenters. For reference, the app is called QuakeAlert! and you can find it on the market, and the source on Google code.
QuakeAlert! has a service that runs periodically and alerts the user of any new earthquakes that match their criteria. When the service runs, it should get the location if possible, process the data from USGS, and potentially alert the user. The problem is in how Android location callbacks operate. The service can request a location update, but there’s no guarantee it will ever be called (and it won’t if the user has disabled the location service). Here is the solution,
UpdateService- this is an intent service that does the real work, whatever that may be. In the case of QuakeAlert! it fetches the data from USGS and processes it, sends notifications, and updates the user interface (if it’s running).
LocationService- this is a regular (non-intent) service that does the following,
- Registers for location updates
- Schedules a TimerTask for execution (say a few minutes in the future)
If the service gets an “on location changed” event before the time is up, it cancels the timer. If the timer runs first, it cancels the “on location changed” registration. In both cases, it calls the UpdateService to do the work.
UpdateService gets the location by calling getLastKnownLocation(). If there was a location obtained through the running of LocationService then it will get that location and use it. The last known location may have also been obtained by a different application at some earlier time getting location updates. That’s okay to, we know we’ve tried our best to get the most accurate location we can at the time.
The implementation of LocationService in QuakeAlert! is reusable. Just pass it a timeout (in milliseconds) and a broadcast intent to send when either the location in obtained or when the timer executes. For example,
// create broadcast intent that will ultimately start your
// intent service
Intent broadcastIntent = new Intent(...);
Intent locationIntent = new Intent(context, LocationService.class);
locationIntent.putExtra("timeout", 1000 * 60 * 2); // 2 minutes
Why pass a broadcast intent, instead of an intent that would start the update service directly? Since UpdateService is an intent service, we must obtain a wake lock before it is run, and the pattern for doing that is to receive an intent (in a receiver), grab the lock there, start the service from the reciever’s handler, then release the lock in the intent service when the work is done.
Note that this same pattern applied to a a foreground activity is much more straighforward: 1) register for location updates 2) open a cancelable progress dialog. If we get an “on location changed” event, dismiss the dialog, and start the UpdateService. If the user cancels, dismiss the dialog, and start the UpdateService. In both cases remember to remove thelocation listener.