Location (GPS) and Automated Testing on Android

So I’ve been contributing to the NPR Android App, specifically by building a test suite and tests for outstanding bugs. While my specific interest is in the audio playback, I took on some sticky issues around the location lookup and found that testing location services isn’t as straightforward as I thought. So here’s a quick primer with some notes to get you started.

Getting the device’s location

Android devices can get location from the (cellular) network or the GPS chip (if they have them). Some devices may support other providers in the future. The emulator only has a GPS provider (emulated of course) registered on start-up.

The quickest way to get the location of the device is to call LocationManager#getLastKnownLocation.

LocationManager lm =(LocationManager) getSystemService(Context.LOCATION_SERVICE);
Location location = null;

List providers = lm.getAllProviders();
for (String provider : providers) {
  Location loc = lm.getLastKnownLocation(provider);
  if (loc != null) {
    location = loc;
    break;
  }
}

Note this call is synchronous but in order to not block the thread during a lookup, it just looks for a saved location from the given provider. Therefore, it does not actually do a network or GPS location lookup.

For rapid response, this is what you want, and most of the time that will work well in your apps. Some other application (like Google Maps) has probably already found the location recently. However, when testing the application, it’s very likely the emulator will not have a location and this call will return null (which, according to the SDK docs, means there is no provider but it can also mean the provider hasn’t located the device yet).

To locate the device, you’ll have to set up a listener. There’s a small likelihood that your app is the first (location-based) app your customer started after turning on the phone. In that case it would be the same problem — no location. So what you probably want to do is have your initial activity launch a listener on one or more providers to record a location. This is asynchronous so it doesn’t block your app. Here’s an example of how to do that.

  private static final int MSG_CANCEL_LOCATION_LISTENERS = 2;
  private Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      switch (msg.what) {
      case MSG_CANCEL_LOCATION_LISTENERS:
        cancelLocationListeners();
        break;
      }
    }
  };

  // This is public so that we can inspect if for testing
  public List locationListeners =  new ArrayList();

 /**
   * On start up, launch a location listener for each service. We need to do
   * this in order to ensure that getLastKnownLocation
   * will always find a value.
   */
  private void lauchLocationListeners() {
    LocationManager lm =
        (LocationManager) getSystemService(Context.LOCATION_SERVICE);
    List providers = lm.getAllProviders();
    for (String provider : providers) {
      LocationListener listener = new LocationListener() {

        @Override
        public void onLocationChanged(Location location) {
          handler.sendEmptyMessage(MSG_CANCEL_LOCATION_LISTENERS);
        }

        @Override
        public void onProviderDisabled(String provider) {
        }

        @Override
        public void onProviderEnabled(String provider) {
        }

        @Override
        public void onStatusChanged(String provider, int status, Bundle extras) {
        }

      };
      lm.requestLocationUpdates(provider, 60000, 0, listener);
      locationListeners.add(listener);
    }
  }

  /**
   * Remove all listeners.
   */
  private void cancelLocationListeners() {
    LocationManager lm =
        (LocationManager) getSystemService(Context.LOCATION_SERVICE);
    // Synchronized because there may be multiple listeners running and
    // we don't want them to both try to alter the listeners collection
    // at the same time.
    synchronized (locationListeners) {
      for (LocationListener listener : locationListeners) {
        lm.removeUpdates(listener);
        locationListeners.remove(listener);
      }
    }
  }

Testing the location

The SDK provides methods for mocking location providers and location details for your testing. (You can also change the geo fix with DDMS in Eclipse or through the console, but I’m interested in automated testing here.) It wasn’t very clear from the documentation on how to use them so it took me a while to figure out that all the ‘test’ calls are not necessarily related.

addTestProvider

This method places a new provider (with the name you gave it) in the collection of providers returned by getAllProviders() (and other calls). You cannot add a mock provider with the same name as a provider that already exists, such as “gps”. This will cause an error. The mock provider doesn’t allow you to pass in a class, so it’s not very useful for inspecting actions called on it (which is why I usually use mocks). Instead it just lets you create a provider that will return in response to specific criteria.

The corresponding removeTestProvider removes this entry from the list. You should remove the provider as you clean up your test because otherwise your next test won’t be able to create it again. (The provider is retained in the emulator instance, even if the activity is destroyed and recreated between tests.) Note that you can actually remove any provider, even the built in “gps” one with this call, so be careful. If you do that you’ll have to restart your emulator to get the emulated GPS interface back.

setTestProviderLocation

I think this method should have been called “setTestLocation” because it is not directly related to a “test provider” made in the previous call. You can set a test location, with this call, for any provider. In fact, if all you want to do is test that your application can find a particular location, then set a location on the GPS provider and ignore the ‘addTestProvider’ method completely.

      LocationManager lm = (LocationManager) activity.getSystemService(Context.LOCATION_SERVICE);
      Location location = new Location(providerName);
      location.setLatitude(latitude);
      location.setLongitude(longitude);
      location.setTime(System.currentTimeMillis());
      lm.setTestProviderLocation(providerName, location);

Be sure to set the time on your location if you want the provider to think it’s a new update.

Also, all the testing requires that you have permission for mock location requested in your application under test (not in the test project). Add this to your manifest.

<uses-permission android:name="android.permission.ACCESS_MOCK_LOCATION"/>

If you are testing on a device, you’ll need to go into Settings | Applications | Developement | Allow Mock Locations and enable it, which is not set by default. This setting may not exist on all devices (but there’s a workaround).

setTestProviderEnabled

By now you should be getting the picture. This method doesn’t enable your test provider, it sets a value that indicates to your application that a provider is enabled whether it actually is or not.

Hopefully this will help some other folks who were confused about the terms in the SDK.

Testing Challenges

As mentioned above the SDK is not very conducive to dependency injection. What I’d like to do is inject a real mock provider object (one I create in my test) into the provider list for the app and when the app asks the GPS provider for a location I know the app is working. This isn’t possible as it’s currently built so I have to write silly tests with threads to start the activity and check when it collects and removes its listeners. Not very clean (and really testing the wrong thing). In many places in the SDK, I’d like to be able to inject mock objects into the system, but this isn’t thoroughly supported yet.

The second challenge is that you can’t access the location manager without access to the activity because it’s returned by the context of the activity. This makes it impossible to, say, inject a mock provider or location before the activity starts, which you might like to do in the case of, oh, looking up location in the onCreate call.

If anyone has any suggestions on this, I’d be excited to hear them. Perhaps I’m just missing a basic concept here.

About these ads

10 thoughts on “Location (GPS) and Automated Testing on Android

  1. I’m looking for an application for receive and show a GPS signal generated by Spirent6100 in an android 2.2.1 device, the software that I’m using is SimChan 1.06, I don’t know if is posible found an *.apk file to install on the device and can do this test. The same test is working without problems in WinMo 6.1 OS. So, if someone can help me? or at least tell me if it’s posibble in android OS.

  2. Salvador –

    I’m not familiar with the Spirent 6100 device. Android abstracts the hardware from the programming interface for location lookups. As long as there’s a driver in your Android OS that generates a provider on start-up for the GPS signal you should be able to query it from the application layer.

  3. Great Post.

    I had problems with the way this works on an emulator, though. While publishing TestLocations on a real device and the GPS-provider worked just fine, publishing it in the emulator did not work at all. Even though gps was an available provider and mock locations have been enabled it didn’t let me publish new locations.

    I ended up using my own TestProvider and had to do some additional stuff, but it seems to work now.

  4. Thanks icyerasor. I admit I did some emulator testing but probably not against every SDK and variety so it’s possible that different emulators or SDK releases behave differently.

    Can you share a link to the code you used or at least some hints about what your TestProvider does differently to make it work in the emulator?

  5. @icyerasor: can u share your code?

    I need a code to make a mock location…

    This code not work for me:

    public class LocationFeeder
    {
    private static LocationManager mLocMgr;

    public static void init(Context ctx)
    {
    mLocMgr = (LocationManager) ctx.getSystemService(Context.LOCATION_SERVICE);
    mLocMgr.addTestProvider(“gps”, false, true, false, false, true, true, true, android.location.Criteria.POWER_HIGH, android.location.Criteria.ACCURACY_FINE);
    mLocMgr.setTestProviderEnabled(“gps”, true);
    }

    public static void sendFix(double latitude, double longitude)
    {
    Location loc = new Location(“gps”);

    loc.setLatitude(latitude);
    loc.setLongitude(longitude);
    loc.setTime(System.currentTimeMillis());
    mLocMgr.setTestProviderLocation(“gps”, loc);

    }

    public static void printLKLoc()
    {
    Log.i(“–EARL–”, “” + mLocMgr.getLastKnownLocation(“gps”));
    }

    public static void shutdown()
    {
    mLocMgr.removeTestProvider(“gps”);
    }
    }

  6. Hi palban,
    heres the code i used: http://pastebin.com/Nvn1sMfX

    Note, that i wrapped the locMgr.getLastKnownLocation(providername) within a utility class that first checks if a mock-location is present, if so it is used. Otherwise either gps or gsm location is used. In my app for location tests i always use this utitly method (see getLastKnownLocationInApplication in Code).

    There is still a lot of code in there for logging, etc. But all in all this works quite well for location checks in automated emulator checks. Also make sure to publish the location, sleep for a bit and then continue the test.

  7. hi. i want to create an application that gets logitude and lattitude from an external database and displays on Ui as gps location.
    please can u help me with that. i will be eternally grateful

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s