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.
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.
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.
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.
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 Creating and Managing Virtual Devices and
from emulator -help-virtual-device
.