Jim Butcher

Mobile applications, enterprise Java, and web consultant

Backwards compatibility in Android

Each new release of the Android platform provides a bevy of new features. For each project, you now have to weigh supporting older devices against using newer features. From time to time you can have a bit of both, with the caveat that it will add complexity to your codebase. Using Java reflection, you can check for the API compatibility of the device, and optionally use the newer functionality. The downside is that you lose the ability for the compiler to validate method signatures of the new methods you’re calling. Also, if you’re using a newly introduced Class, you have to build against a newer version of the API than your deploy target. This means that with each Android API method you call, you will have to verify whether or not it is part of the build-time api, or the deployment baseline. All of these hurdles are manageable, however, and the ability to support older devices while using the latest-and-greatest features is a great thing to have.

Lets take two examples from interacting with the Android WebView. In the first, we’ll enable DOM storage for devices using 2.1 and later. In the second, we’ll enable WebSQL storage for devices using 2.0 and above. Both these will assume a project baseline of Android 1.6.

To enable DOM storage in the Android WebView, a new method setDomStorageEnabled(boolean) was added to the existing WebSettings class. We can use reflection on the WebSettings object to check for the presence of the method, and invoke it on clients which support it. Since we’re using the Method class to make the call, we don’t directly link to the newer API, meaning we can still build our code against the 1.6 API libraries.

// somewhere in your initialization...
WebSettings = webView.getSettings();
try
{
    // enable Dom Storage (API lvl 7)
    Class[] setDomStorageEnabledSignature = new Class[] { boolean.class };
    Method setDomStorageEnabled = webSettings.getClass()
        .getMethod("setDomStorageEnabled", setDomStorageEnabledSignature);
    setDomStorageEnabled.invoke(
        webSettings, new Object[] { Boolean.valueOf(true) });
}
catch (NoSuchMethodException ex)
{
    Log.i("YourLogTag", "Dom storage not available.");
}
catch (InvocationTargetException ex)
{
    // oops something went wrong. Handle that here
}
catch (IllegalAccessException ex)
{
    // oops something went wrong. Handle that here
}

Now devices running Android 2.1 and above will have HTML5 DOM storage enabled in the WebView that is hosted in this application, but the application will still run on Android 1.6 devices, as well.

Let’s examine what is required to enable WebSQL storage. Firstly, you must enable it in the WebSettings similarly to enabling DOM storage as above. Secondly, you must configure where the system will store the database files. These, too, are new method calls on the WebSettings class, so we can use the same mechanism. However, when a WebSQL database is created, we immediately recieve an onExceededDatabase callback on the WebChromeClient class. (The initial quota is 0 bytes, and the default implementation of WebChromeClient will not automatically increase it.) This callback has as a parameter the newly introduced WebStorage.QuotaUpdater class. This means, for our custom WebChromeClient class to compile, it must be built against the newer API. Also, if an older client attempts to use the class, it will throw an Exception when it references the class. What we can do is to initialize the new class in a try block during static initialization, and set a variable to tell us whether or not it is safe to use.

First the custom WebChromeClient which handles the quota callback:

package yourpackage;

public class CustomChromeClient extends WebChromeClient
{
    // [snip...]
    @Override
    public void onExceededDatabaseQuota(String url, String databaseIdentifier,
            long currentQuota, long estimatedSize, long totalUsedQuota, QuotaUpdater quotaUpdater)
    {
        quotaUpdater.updateQuota(newQuotaSize);
    }
    // [snip...]
   
    // calling this method causes the runtime to initialize and verify the class
    // call this in a try block in the referencing class's static initializer and
    // catch any exceptions to determine whether the class passed verification
    public static void verifyMe() {}
   
    static
    {
        // determine whether or not the class from the new API
        // is available to us. This will cause the class to throw an
        // exception in event the runtime let it pass verification.
        try
        {
            Class.forName("android.webkit.WebStorage.QuotaUpdater");
        }
        catch (Exception ex)
        {
            throw new RuntimeException(ex);
        }
    }
}

Calling the static method “verifyMe” will force the runtime to load and verify the class. The static initializer in this class is an additional check for a more lax runtime which may have skipped the verification step.

In the code of the class which initializes the WebView and uses the CustomWebChromeClient from above:

public Class somethingWithAWebView
{
    // [snip...]
    public void initWebView()
    {
        // [snip...]
        if (has_2_0_features)
        {
            webView.setWebChromeClient(new CustomWebChromeClient());
        }
    }

    private static boolean has_2_0_features;

    // our static initializer attempts to load the custom web chrome client
    // and uses the success or failure to determine whether or not
    // the device supports the API for WebSQL  
    static
    {
        try
        {
            CustomChromeClient.verifyMe();
            has_2_0_features = true;
        }
        catch (Throwable t)
        {
            has_2_0_features = false;
        }
    }
}

The static initializer for this class calls the verifyMe method on the CustomWebChromeClient class to force it’s initialization and verification. It stores the result of the check in a static member variable for the code the check against. If you had a more complex CustomWebChromeClient, you could use a 1.6 compatible version if the check fails, and the more feature rich one when it succeeds.

Now to come back to the pitfalls. Using the first method, you’ll notice that the method lookup uses a string for the name of the method, and that I have to build the parameter list myself. This means the compiler can’t verify that I didn’t misspell the method name, or that I gave the incorrect signature until runtime. This can be nefarious because those problems will cause the same behavior as using an older version of the API. Careful testing after each addition of Reflection is a good practice to adopt.

Using the second method, I now have to update my project to build against the newer API. This means when I add new functionality to my project from the API, I have to check the documentation closely to verify what version the new methods/classes that I’m using became available. If you do use something unavailable to your minimum sdk requirements, the compiler will happily build the project, but it will begin crashing on older devices. Backwards compatibility is never fun, but with careful testing the added complexity is manageable.

Leave A Comment