Location on Android: Stop Mocking Me!
tl;dr Scroll to the bottom and grab a sweet Java class that solves all issues of setting up location services and detecting mock locations across all relevant Android versions.
People, it’s 2016. Writing a location-aware app in Android should be simple. Unfortunately, it’s not. If you want to support a decent range of versions (let’s say API level 14 and newer, 97% of Android users) you’re faced with a number of issues. I struggled with these for several days and thought I would share my lessons learned - and the code that came out of it. If you just want to cut to the chase, click here.
Stop mocking me.
A particular requirement in my case was that I needed the user’s real location. In other words I wanted to detect and reliably reject fake locations. As you might know, Android officially allows the installation of so-called ‘mock location’ apps. Most of them are called something like ‘Fake GPS’ and broadcast a location chosen by the user (for example at the north pole) to the system, just like a regular GPS provider. Most apps silently accept mock locations and in fact teleporting yourself to the ends of the earth can be incredibly handy when testing app features or previewing your next vacation destination. However, it can also be a real pain, especially when your app depends on the authenticity of location information, e.g. for check-ins, location-based rewards etc.
In general, I wanted to do four things whenever my app was started/resumed:
- Request location updates with a certain accuracy and at fixed update intervals.
- If necessary (on newer Android systems), ask the user to grant permission to retrieve location info.
- If disabled, ask the user to enable location providers, ideally without leaving the app.
- Detect and reject mock locations and ask the user to switch off mock location apps.
Getting the location permission is pretty straightforward, as there is a nice tutorial on how to request permissions at run time on systems with API level ≥ 23 (Marshmallow and above). Getting updates from the location providers can be a bit tricky, because there are two ways to do it. You can subscribe directly to the different available location providers (GPS, Network, Bluetooth) via the LocationManager on the device. For a long time, this used to be the main method of obtaining location info and you had to basically write your own logic to fuse the location info from different providers.
Location Settings on Android Marshmallow
Not so long ago, Google introduced the Play Services Location API as an alternative that simplifies a lot of the manual effort and abstracts a number of issues. It is actually quite nice and - unless you need really fine-grained control of the location sources - it’s definitely the way to go. The utility class I’m introducing below is built on top of the Google Play Services API. You can read Google’s intro ‘Making Your App Location-Aware’ if you want to learn more about it.
The following diagram shows the exact order of steps that I have found necessary to reliably obtain location updates on the aforementioned API levels.
The 6 steps to location Nirvana.
The orange boxes denote situations in which a user action is required. Let me walk you through the steps:
Check Permission: On older Android versions, this step is irrelevant, as the user has to grant permission at install time. Starting with Marshmallow, the permission mechanism became more refined (similar to the way it works on iOS). The user must now grant permission at run time and you must do a better job at explaining why your app needs location permission. Depending on how obvious your app’s need for location information is, you could first show a small explanation and then request the permission or directly request it. Either way, if the user declines, you should show a more detailed explanation. Once permission is granted, you may proceed to step 2.
- Request Access: Here, we are asking the Google API to give us access to location information with a certain
(choices are HIGH, BALANCED, LOW, NO_POWER) and update frequency. The API automatically decides which
location providers are necessary to meet your requirements. For example, if you need high accuracy, it is likely that
GPS will be switched on, while for balanced or low accuracy it might be sufficient to just use network location
(WiFi and cell tower).
Requesting access can result in three outcomes:
- Unavailable means that for some reason, it is and won’t be possible to change the location settings with the in-app dialog to meet your requirements. I decided, that the most reasonable step in this case is to direct the user to the system location settings so she/he can figure out what’s going on.
- Resolution means that some providers are currently switched off, but it is possible to switch them from an in-app dialog. This is where you want to interact with the user to possibly explain the situation and then ask her/him to enable the necessary providers.
- Success means all the necessary providers are enabled and you may proceed to step 3.
Request Updates: Having been granted access, we now subscribe to periodic updates in this step. Nothing should go wrong here (unless there are connectivity issues).
Check Availability: The Google API offers a simple check to determine if location info is available. This will include the best “last known position”, so it’s a good idea to check availability up front, even if your GPS is still searching for a fix. If no location info is available after the Google API reports providers were switched on and updates were successfully requested, it’s a good idea to manually check the location providers (via the LocationManager). Oftentimes this is redundant to the Google API check, however, I saw some cases on older Android phones / distros where this fallback was actually necessary.
Check for Mock Locations: Once we acquire a new location reading, this step aims to determine whether the info is real or fake. Read the following section for more info on how this is done.
- We’re good to go and should be receiving location updates.
Implementing steps 1-6 is mostly busywork. However, step 5 - detecting mock locations - can trip you up pretty bad. On older Android systems (< API level 18) mock locations are detected via Android’s Settings.Secure flag. This works reliably and will cause your app to generally reject any kind of location until mock locations are disabled in the system settings.
The trouble starts on systems with API level 18 and up: The new flag Location.isFromMockProvider() is supposed to flag mock locations on a per-location basis. In general this is a good idea, as it provides more fine-grained control and could even be used to simultaneously process location info from fake and real providers.
Let’s go somewhere warm.
So let’s beam ourselves to Brazil for a moment and see what Location.toString() looks like on the console for every new location we receive.
You’re kidding me.
So apparently the Brazil locations from our Fake GPS provider get properly flagged as mock locations. Except for those that do not. I had a major incident of Schnappatmung (you may look that up in a German dictionary).
It can be more or less frequent, but eventually you will see it happen: If you switch on a fake location provider, every now and then a fake location will arrive that is not labeled as a mock. Obviously, this is detrimental if your application is relying on .isFromMockProvider() to reject fake locations. One such false negative is enough to screw up your use case.
It’s funny that the culprit mock location has a much poorer accuracy than the other location readings. I’m pretty sure the fake GPS provider is always sending locations with the same accuracy, which leads me to believe that the incorrectly labeled location is the result of some erroneous fusion logic inside Google’s API.
Since most location readings are correctly labeled, it’s not too difficult to identify and reject the false negatives. I chose the following strategy:
- Remember the most recent location labeled as a mock
- If a new “non-mock” reading is within 1km of the last mock, reject it.
- Only clear the last mock location after 20 consecutive “non-mock” readings.
This simple approach proved to work very well in practice. Theoretically, if the very first reading was a false negative, this logic would still be fooled. However, given my assumption that the reading stems from some API fusion logic, such a reading would only ever occur after a couple of regular (mock) readings. Here’s the rejection strategy in action:
That feels better…
No matter whether you have valiantly plowed through the entire blog post or simply skipped to this section - you have come to the right place. Let me introduce you to … the LocationAssistant. He’s your knight in shining armor and your new best buddy.
I have tried to abstract and hide away all the gory, boring and irrelevant details of setting up and subscribing to location updates. The LocationAssistant provides a Listener interface that contains relevant events, upon some of which you may need to or want to interact with the user. It also provides a few methods that let you switch stuff on and off.
If you want, you can use the LocationAssistant to reject mock locations, but you don’t have to. I believe you will find it useful either way. Now head on over to GitHub and get it. If you like it, please star it, fork it or contribute. If you spot errors or have enhancements feel free to submit a pull request.
Thanks for joining me on today’s tangent and please don’t be shy to comment below.