Location (GPS) and Automated Testing on Android

Posted on September 23, 2010

8


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
Posted in: Coding