Stoner Physics

Android engineering

Testing a location-dependent Android app

There are four places a chosen position can enter an app under test: a framework test provider, the fused provider’s mock mode, the emulator’s simulated GPS, or a fake behind your own interface. Each covers a different part of the problem. This page gives the exact calls, the settings that gate them, and what each one leaves untested.

The gate: one app op, not a permission

A mock location is a position injected into the Android location service by an app, rather than computed by the receiver. An app op is a per-app switch the system keeps outside the normal permission list.

The switch that allows injection is the app op android:mock_location — AppOpsManager.OPSTR_MOCK_LOCATION, added in API level 23 and documented as “inject mock location into the system”. Settings → Developer options → Select mock location app sets that op for one package, and its picker lists only apps whose manifest requests android.permission.ACCESS_MOCK_LOCATION. That permission is signature-level, so a third-party app is never granted it; requesting it is what earns the app a place in the list. On a test rig you set it without touching the screen:

adb shell appops set com.example.app android:mock_location allow

If the op is not allowed, LocationManager.addTestProvider() and setTestProviderLocation() throw SecurityException. That is the documented failure, and it is the first thing to check when a rig that worked yesterday stops accepting positions: the op is stored against the installed package and deleted when that package is uninstalled, so a clean reinstall or a wiped device loses it. An update install keeps it.

The framework calls

Three overloads of addTestProvider exist. The original takes ten arguments (name, then requiresNetwork, requiresSatellite, requiresCell, hasMonetaryCost, supportsAltitude, supportsSpeed, supportsBearing, a power-usage constant and an accuracy constant). API level 31 added addTestProvider(String, ProviderProperties) and a third form that also takes a set of attribution tags. After that: setTestProviderEnabled, then setTestProviderLocation for each fix, then removeTestProvider when the test ends.

Use the provider name the app actually listens to. Registering one called test and wondering why nothing arrives is the usual first mistake: if the app requests LocationManager.GPS_PROVIDER, the test provider has to be named gps.

Why the framework rejects your Location object

setTestProviderLocation throws IllegalArgumentException if the named provider is not a test provider, and again “if the location is null or incomplete”. Completeness is defined in AOSP’s Location.java (public as isComplete() from API level 33), and the whole test is one line:

return mProvider != null && hasAccuracy() && mTimeMs != 0 && mElapsedRealtimeNs != 0;

So four things must be set before the object will be accepted: a provider name, a horizontal accuracy, a Unix epoch time, and an elapsed-realtime timestamp in nanoseconds since boot. The last one is the one people miss. Elapsed realtime is a clock that counts from boot and cannot be moved by the user or by a time sync, which makes it the trustworthy timestamp and the wall clock the convenient one. Set it from SystemClock.elapsedRealtimeNanos() at the moment you build the fix.

Accuracy is not a free number either. Android defines it as “the estimated horizontal accuracy radius in meters of this location at the 68th percentile confidence level” — a circle of that radius has a 68 % chance of containing the true position. If your test always passes 1.0, code that discards fixes above a threshold never runs.

Every fix delivered this way is flagged. Location.isMock() (API level 31; before that isFromMockProvider(), now deprecated) returns true for anything from a test provider, and the documentation is explicit that such a location “may not be related to the actual location of the device”. A debug build that logs that flag alongside each fix will tell you, after the fact, exactly which part of a test run was synthetic.

The fused provider has a second switch

Many apps never talk to LocationManager directly. They use the fused location provider in Google Play services, which in Google’s words “intelligently combines different signals”, GPS and Wi-Fi among them, behind one API. It has its own pair of calls: setMockMode(boolean) and setMockLocation(Location) on FusedLocationProviderClient.

Three things in that API’s documentation matter for test design. Entering mock mode clears the provider’s cached locations, so the first thing your app sees afterwards is yours and not a leftover. The app has to request android.permission.ACCESS_MOCK_LOCATION and still be the selected mock location app. And mock mode applies to every client of the fused provider on the device, including other processes and derived APIs such as geofencing — which is why the documentation tells clients to always set it back to false when finished. A test that leaves it on leaves every app on that handset that uses the fused provider receiving only the positions your test sets.

Emulator or real device

The emulator carries a simulated GPS driven from its console. Android’s emulator console documentation gives the syntax, and it is worth memorising because the argument order is the reverse of Android’s own APIs:

geo fix <longitude> <latitude> [altitude] [satellites] [velocity]

Longitude comes first. Altitude is in metres, satellites is a count from 1 to 12, velocity is in knots. The console listens on ports 5554–5585, localhost only, and needs auth with the token from ~/.emulator_console_auth_token; from a script, adb emu geo fix … does the same without the telnet session. geo nmea pushes a raw NMEA 0183 sentence — the printable-ASCII sentence format GPS receivers emit — with $GPGGA and $GPRMC supported. The Extended controls window adds saved points, multi-stop routes with playback, repeat and speed multipliers, and import of GPX or KML tracks.

The system image decides what exists at all. Per Android Studio’s virtual-device documentation, images labelled Google APIs include Google Play services; plain AOSP images do not, so there is no Play-services fused provider and no Geofencing API on them. Images with the Play Store are signed with a release key, so adb root is refused on them; the documentation’s route to elevated privileges is an AOSP image, which has no Play services. A test that needs root and Play-services code paths together cannot run on a Play Store image. Those facts decide most emulator test matrices.

EmulatorFree, scriptable, reproducible. No radio front end, so nothing exercises signal acquisition, multipath in an urban canyon, or the receiver’s own filtering. Play-services behaviour depends on the image.
Test provider on a deviceReal OS, real power management, real permission dialogs. Reaches apps that read LocationManager directly.
Fused mock mode on a deviceReaches apps that use the fused provider, and the geofencing built on it. Affects every app on the handset while it is on.
Fake behind your own interfaceRuns in a JVM unit test in milliseconds, with no device. Tests your logic and nothing about Android’s delivery of fixes.

What none of these approaches covers

The coarse path. An app granted only ACCESS_COARSE_LOCATION does not get your mock at full precision — it gets an approximate location. Android’s documentation puts it at “accurate to within about 3 square kilometers (about 1.2 square miles)” when it comes from the system location service or the fused provider. Code that assumes metres will fail on a device where the user chose approximate location, and only a test run with the coarse grant will find it.

The permission sequence. For an app targeting Android 11 (API level 30) or higher, requesting a foreground location permission and ACCESS_BACKGROUND_LOCATION in the same call means the system “ignores the request and doesn’t grant your app either permission”. On Android 11 and higher, background access has no button in the dialog at all; the user has to pick Allow all the time on a settings page. The foreground dialog also offers Only this time. Three separate states to test, and no location simulator produces any of them.

Delivery rate. Android’s background location limits documentation states that since Android 8.0 a backgrounded app receives location updates “only a few times each hour”, however often it asked, while geofencing is handled separately and responds “every couple of minutes or so”. Injecting fixes at 1 Hz does not change what the delivery path does with them, so measure arrival times in your own logs instead of assuming the app sees your rate.

Doze. On Android 6.0 and higher a device left unplugged and stationary with the screen off enters Doze, and the documented restrictions are: network access suspended, wake locks ignored, standard AlarmManager alarms deferred to the next maintenance window, no Wi-Fi scans, no sync adapters, no JobScheduler. A location feature that works on a screen-on device can still fail overnight. Force the state with adb shell dumpsys deviceidle force-idle rather than waiting for it, and release it with adb shell dumpsys deviceidle unforce.

The tool we sell for this

GPS Spoofer is our Android app for driving a real handset by hand: pick a point on a 3D globe, search for a city or type in coordinates, and the device reports that position through the platform’s mock-location path — test providers named gps, network and fused, plus the fused provider’s mock mode. It adds a joystick overlay for moving in real time and a route builder with speed control, fills in speed and bearing from the movement, and reports an accuracy that starts near 19 m and settles to a few metres, with the odd brief degradation, instead of one fixed number — so threshold code gets exercised. Both editions do all of this. The Full edition is sold direct from this site as a one-time purchase; the Google Play edition, without the Full edition’s mock-hiding features, is not yet released. It suits manual QA on a device in your hand, where scripting the emulator is the wrong shape of work.

Questions about test setups: [email protected].