Backward Compatibility for Applications

A variety of Android-powered devices are now available to consumers from carriers in geographies around the world. Across those devices, a range of Android platform versions are in use, some running the latest version of the platform, others running older versions. As a developer, you need to consider the approach to backward compatibility that you will take in your application — do you want to allow your application to run on all devices, or just those running the latest software? In some cases it will be useful to employ the newer APIs on devices that support them, while continuing to support older devices.

Setting the minSdkVersion

If the use of a new API is integral to the application — perhaps you need to record video using an API introduced in Android 1.5 (API Level 3) — you should add a <android:minSdkVersion> to the application's manifest, to ensure your app won't be installed on older devices. For example, if your application depends on an API introduced in API Level 3, you would specify "3" as the value of the minimum SDK version:

  <manifest>
   ...
   <uses-sdk android:minSdkVersion="3" />
   ...
  </manifest>

However, if you want to add a useful but non-essential feature, such as popping up an on-screen keyboard even when a hardware keyboard is available, you can write your program in a way that allows it to use the newer features without failing on older devices.

Using reflection

Suppose there's a simple new call you want to use, like android.os.Debug.dumpHprofData(String filename). The Debug class has existed since Android 1.0, but the method is new in Anroid 1.5 (API Level 3). If you try to call it directly, your app will fail to run on devices running Android 1.1 or earlier.

The simplest way to call the method is through reflection. This requires doing a one-time lookup and caching the result in a Method object. Using the method is a matter of calling Method.invoke and un-boxing the result. Consider the following:

public class Reflect {
   private static Method mDebug_dumpHprofData;

   static {
       initCompatibility();
   };

   private static void initCompatibility() {
       try {
           mDebug_dumpHprofData = Debug.class.getMethod(
                   "dumpHprofData", new Class[] { String.class } );
           /* success, this is a newer device */
       } catch (NoSuchMethodException nsme) {
           /* failure, must be older device */
       }
   }

   private static void dumpHprofData(String fileName) throws IOException {
       try {
           mDebug_dumpHprofData.invoke(null, fileName);
       } catch (InvocationTargetException ite) {
           /* unpack original exception when possible */
           Throwable cause = ite.getCause();
           if (cause instanceof IOException) {
               throw (IOException) cause;
           } else if (cause instanceof RuntimeException) {
               throw (RuntimeException) cause;
           } else if (cause instanceof Error) {
               throw (Error) cause;
           } else {
               /* unexpected checked exception; wrap and re-throw */
               throw new RuntimeException(ite);
           }
       } catch (IllegalAccessException ie) {
           System.err.println("unexpected " + ie);
       }
   }

   public void fiddle() {
       if (mDebug_dumpHprofData != null) {
           /* feature is supported */
           try {
               dumpHprofData("/sdcard/dump.hprof");
           } catch (IOException ie) {
               System.err.println("dump failed!");
           }
       } else {
           /* feature not supported, do something else */
           System.out.println("dump not supported");
       }
   }
}

This uses a static initializer to call initCompatibility, which does the method lookup. If that succeeds, it uses a private method with the same semantics as the original (arguments, return value, checked exceptions) to do the call. The return value (if it had one) and exception are unpacked and returned in a way that mimics the original. The fiddle method demonstrates how the application logic would choose to call the new API or do something different based on the presence of the new method.

For each additional method you want to call, you would add an additional private Method field, field initializer, and call wrapper to the class.

This approach becomes a bit more complex when the method is declared in a previously undefined class. It's also much slower to call Method.invoke() than it is to call the method directly. These issues can be mitigated by using a wrapper class.

Using a wrapper class

The idea is to create a class that wraps all of the new APIs exposed by a new or existing class. Each method in the wrapper class just calls through to the corresponding real method and returns the same result.

If the target class and method exist, you get the same behavior you would get by calling the class directly, with a small amount of overhead from the additional method call. If the target class or method doesn't exist, the initialization of the wrapper class fails, and your application knows that it should avoid using the newer calls.

Suppose this new class were added:

public class NewClass {
   private static int mDiv = 1;

   private int mMult;

   public static void setGlobalDiv(int div) {
       mDiv = div;
   }

   public NewClass(int mult) {
       mMult = mult;
   }

   public int doStuff(int val) {
       return (val * mMult) / mDiv;
   }
}

We would create a wrapper class for it:

class WrapNewClass {
   private NewClass mInstance;

   /* class initialization fails when this throws an exception */
   static {
       try {
           Class.forName("NewClass");
       } catch (Exception ex) {
           throw new RuntimeException(ex);
       }
   }

   /* calling here forces class initialization */
   public static void checkAvailable() {}

   public static void setGlobalDiv(int div) {
       NewClass.setGlobalDiv(div);
   }

   public WrapNewClass(int mult) {
       mInstance = new NewClass(mult);
   }

   public int doStuff(int val) {
       return mInstance.doStuff(val);
   }
}

This has one method for each constructor and method in the original, plus a static initializer that tests for the presence of the new class. If the new class isn't available, initialization of WrapNewClass fails, ensuring that the wrapper class can't be used inadvertently. The checkAvailable method is used as a simple way to force class initialization. We use it like this:

public class MyApp {
   private static boolean mNewClassAvailable;

   /* establish whether the "new" class is available to us */
   static {
       try {
           WrapNewClass.checkAvailable();
           mNewClassAvailable = true;
       } catch (Throwable t) {
           mNewClassAvailable = false;
       }
   }

   public void diddle() {
       if (mNewClassAvailable) {
           WrapNewClass.setGlobalDiv(4);
           WrapNewClass wnc = new WrapNewClass(40);
           System.out.println("newer API is available - " + wnc.doStuff(10));
       } else {
           System.out.println("newer API not available");
       }
   }
}

If the call to checkAvailable succeeds, we know the new class is part of the system. If it fails, we know the class isn't there, and adjust our expectations accordingly. It should be noted that the call to checkAvailable will fail before it even starts if the bytecode verifier decides that it doesn't want to accept a class that has references to a nonexistent class. The way this code is structured, the end result is the same whether the exception comes from the verifier or from the call to Class.forName.

When wrapping an existing class that now has new methods, you only need to put the new methods in the wrapper class. Invoke the old methods directly. The static initializer in WrapNewClass would be augmented to do a one-time check with reflection.

Testing is key

You must test your application on every version of the Android framework that is expected to support it. By definition, the behavior of your application will be different on each. Remember the mantra: if you haven't tried it, it doesn't work.

You can test for backward compatibility by running your application in an emulator that uses an older version of the platform. The Android SDK allows you to do this easily by creating "Android Virtual Devices" with different API levels. Once you create the AVDs, you can test your application with old and new versions of the system, perhaps running them side-by-side to see the differences. More information about emulator AVDs can be found in the AVD documentation and from emulator -help-virtual-device.