Monday, January 10, 2011

How To: Use the Gyroscope API and Remain Android 1.1 Compatible

Making your app backwards compatible is the process of ensuring it degrades gracefully. 

Every new Android SDK release introduces a variety of new APIs

It takes time for each new OS version to percolate through the ecosystem, so while new functionality is cool, but it introduces a dilemma - use the new hotness or support the majority of users?

The answer of course is to do both! Make use of useful new APIs where they're available, and fall back to an earlier alternative (or disable functionality) when necessary.

Let's look at a practical example
My Nexus S has a gyroscope sensor that I can use to stabilize my artificial horizon / compass app (New Horizons). I'll save most of the implementation details for a separate post, and focus instead on how I can adjust my code to go from supporting only Android 2.3, to supporting everything from Android 1.0 up.
The platform version distribution shows 99.9% of devices are now running at least Android 1.5, and more than 75% are running 2.1+, so in this case achieving 1.0 compatibility is a contrivance that lets me demonstrate a number of useful techniques within a single example.
I'll start by reacting to orientation changes using the gyroscope in 2.3 and then remove hardware and API features until we end up compatible with a factory-fresh HTC G1.

Using the gyroscope sensor 

You'll need code that looks a little like this:

private void hookupSensorListener() {
  SensorManager sm = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
  sm.registerListener(sel,
    sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE),
    SensorManager.SENSOR_DELAY_UI);
}

private final SensorEventListener sel = new SensorEventListener() {
  public void onSensorChanged(SensorEvent event) {
    if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) {
      updateOrientation(event.values[0], event.values[1], event.values[2]);
    }
  }
  
  public void onAccuracyChanged(Sensor sensor, int accuracy) {}
};

private void updateOrientation(float heading, float pitch, float roll) {
  // Update the UI
}


Within the updateOrientation method you react to each orientation change (for a gyro that's going to mean integrating the changes in angular velocity) and eventually extract values you'll use to update the UI.

Case 1: What if there is no gyroscope?

Can we implement the same functionality with different hardware? In this case yes, we can use the accelerometers to determine our orientation.

Start by creating a listener interface that can listen for orientation changes:

public interface OrientationChangeListener {
  public void onUpdate(float heading, float pitch, float roll);
  public void onAccuraryChanged(int accuracy);
}


Then define another interface that can be implemented to handle orientation updates from different sensor hardware:

public interface IOrientationSensorListener {
  public void setOrientationChangeListener(OrientationChangeListener l);
  public void registerListener(SensorManager sensorManager);
  public void unregisterListener(SensorManager sensorManager);
}


Create two implementations of the IOrientationSensorListener class, one that uses the gyro and the other that uses accelerometers:
  • GyroOrientationSensorListener
  • AccOrientationSensorListener
The orientation updates are encapsulated in their hardware-specific implementations, so we need to implement an OrientationChangeListener that will receive their orientation updates and update the UI accordingly:

final OrientationChangeListener ocl = new OrientationChangeListener() {
  public void onUpdate(float heading, float pitch, float roll,) {
    updateOrientation(heading, pitch, roll);
  }

  public void onAccuraryChanged(int accuracy) {
    updateAccuracy(accuracy);
  }
});


Now we determine if the gyro is available:

PackageManager paM = getPackageManager();
boolean gyroExists = paM.hasSystemFeature(PackageManager.FEATURE_SENSOR_GYROSCOPE);


And instantiate the appropriate IOrientationSensorListener implementation:

IOrientationSensorListener mySensorListener;
if (gyroExists)
  mySensorListener = new GyroOrientationSensorListener();
else
  mySensorListener = new AccOrientationSensorListener();


Before hooking up the orientation change listener:

mySensorListener.setOrientationChangeListener(ocl);
For cases where there is no viable alternative hardware, you can either disable that functionality completely, or specify the required hardware as a mandatory feature:

<uses-feature android:name="android.hardware.sensor.gyroscope"/>

Note that this will prevent your app from being visible on the Market for devices without the required hardware.

Case 2: What if the APIs you're using are missing?

While your Android 2.3 app will install and run on any version of the Android OS, it will crash the moment it tries to use a class or method that doesn't exist on that device. To figure out what needs to be fixed, you can modify your project's build target to the lowest OS version you intend to support and see what won't compile.

Looking at my code above, I can see the following constants, methods, and classes were introduced since Android 1.0:
  • API Level 9: PackageManager.FEATURE_SENSOR_GYROSCOPE
  • API Level 6: PackageManager.hasSystemFeature()
  • API Level 3: Sensor, SensorEvent, and SensorEventListener
Dealing with new constants

New constants will work without any changes. The compiler will convert them into the value they represent and the methods into which you pass them should be able to handle with unrecognized values cleanly.

In this instance I pass a string constant into the hasSystemFeature method that assumes any feature it doesn't recognize isn't available and returns false.

We're now compatible with API Level 6 (Android 2.0.1)!

Dealing with missing methods

Missing methods are a little more complicated. In this case the PackageManager class has been extended to include the hasSystemFeature method. If you call this method on a device running Android 1.6 (or below) it will throw an exception and crash.

To avoid this you can:
  • Use reflection at runtime to determine if the method exists.
  • Replace the method with an alternative that works on previous OS versions.
Reflection is expensive, so first we'll check if there's a cheaper alternative that doesn't add significant complexity or cost.

In this case I can use SensorManager.getDefaultSensor to check if there is a default gyroscope sensor available.

boolean gyroExists = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null;

We're now compatible with API Level 3 (Android 1.5), and given the distribution of Android OS versions in the wild, for most cases we'd be done.

We still haven't looked at dealing with missing classes though, so for the sake of completeness let's go one step further and ensure support for Android 1.0.

Dealing with missing classes

The Sensor, SensorEvent, and SensorEventListener classes were all added in Android 1.5 (along with a number of useful SensorManager methods including getDefaultSensor used above).

Let's start by using reflection to see if the new Sensor classes and methods are available. This can be expensive so it's important to perform the check only once per application launch. We'll also assume that the existence of one 1.5+ class implies the existence of the rest.

private static boolean sensorListenerExists = false;
 
private static void checkSensorEventListenerExists() throws NoClassDefFoundError {
  sensorEventListenerExists = SensorEventListener.class != null;
}
 
static {
  try {
    checkSensorEventListenerExists();
  } catch (NoClassDefFoundError e) {
    sensorEventListenerExists = false;
  }
}


To avoid an exception on calling getDefaultSensor we need to move this call into a new class that's only used if the new method is available. Let's expose the functionality using a static method to avoid the need to create any new objects:

public class SensorEventHasGyro {
  public static boolean gyroExists(SensorManager sensorManager) {
    return sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null;
  }
}


Gyroscopes weren't supported prior to Android 1.5, so we can check for a gyroscope sensor using the following code:

boolean gyroExists = sensorListenerExists && 
                     SensorEventHasGyro.gyroExists(sensorManager);

Finally, we create a new implementation of the OrientationSensorListener that uses the old technique for calculating orientation from accelerometers (which is so deprecated it's not worth repeating here), then modify the code used to select and instantiate the correct implementation.

if (gyroExists)
  mySensorListener = new GyroOrientationSensorListener();
else if (sensorListenerExists)
  mySensorListener = new AccOrientationSensorListener();
else 
  mySensorListener = new AccOldOrientationSensorListener();

Summary

While providing support for Android 1.0 as done in this example is a little contrived, the same techniques will work in cases like the Download Manager or SIP stack where the majority of users don't have access to the newer versions of the OS.

Why wait until everyone is using Android 2.3 to make use of the gyroscope or download manager? Using these techniques you can build it in right now, and simply provide an alternative implementation for users who aren't fortunate enough to have the latest hardware / software.

Monday, January 03, 2011

Listomania! My Reading List and Gadget Compendium

tl;dr

Those of you interested can check out these lists of what I've been reading and what gadgets I'm using (and have used).

The Long Version

My Reading List

One of my resolutions for 2010 was to read at least one book every two weeks. Having kept track of everything I read, I figured I might as well share it - hence Reto Meier's Reading List. Some observations from reviewing the list:
  • Favourite Book: Music for Torching by A. M. Holmes.
  • Most Read Author: Philip K Dick accounted for 4 of the 23 books I read in 2010.
  • The Kindle Effect: I read 6 books in 2 months on Kindle versus 17 in the 9 months before.
  • Hardcover versus Paperback: 12 hardcovers, 6 eBooks, 5 paperbacks.
I'll continue to update this with new books as I read them -- hopefully at a rate of one every two weeks.

My Gadget Compendium

I often get asked which gadgets (Android powered or otherwise) I'm currently carrying around. These days the list seems to change every month or so, so for easy reference I've put together Reto Meier's Gadget Compendium.

I'll update it whenever a new gadget gets slightly less top secret, or something gets retired from the active lineup. A couple of observations from looking at my current lineup and the honorably discharged:
  • Rate of Innovation: From 2 phones in 5 years, to 5 phones in two years since the G1 was released in 2008.
  • Size: Everything (except my Netbook) has gotten thinner, lighter, and sleeker. There's also a distinct movement away from physical keys / buttons -- my SE P910i had a qwerty keyboard, dial pad, and a 5 function scroll wheel, my Nexus S and Galaxy Tab have only power and volume keys.
  • Connectivity: The only pieces of old kit with a mobile data connection where the phones. Now the only gadget in my bag without 3G is my camera. Which reminds me, I must pick up an Eye-Fi card.
With a broader variety of Android devices scheduled for release this year it'll be interesting to see if they replace or augment my existing collection of portable gadgets. I'd love to replace the netbook for a Chrome-powered variety, but I'm not sure I can imagine writing a whole book without Word so I'm not sure if that's on the cards.