Chapter 29

Handling Platform Changes

Android has been rapidly evolving since its initial release, and will continue to do so over the next few years. Perhaps, in time, the rate of change will decline some. However, for the present, you should assume that there will be significant Android releases every 6 to 12 months, and changes to the lineup of possible Android hardware on an ongoing basis. So, while right now the focus of Android is phones and tablets, soon you will see Android netbooks, Android TVs, Android media players, and so on.

Many of these changes will have little impact on your existing code. Some, though, will necessitate at least new rounds of testing for your applications, and perhaps changes to those applications based upon the test results.

This chapter covers several issues that may cause you trouble in the future as Android evolves, and provides some recommendations on how to deal with them.

Things That Make You Go Boom

Android will change, not only in terms of what Google introduces, but also in how device manufacturers tweak Android for their own hardware. This section points out a couple of places where these changes can affect your application if you're not prepared for them.

View Hierarchy

Android is not designed to handle arbitrarily complicated view hierarchies. Here, view hierarchy means containers holding containers holding containers holding widgets. The hierarchyviewer program, described in a later chapter, depicts such view hierarchies well.

Android has always had limits as to how deep the view hierarchy can be. In Android 1.5, though, the limit was reduced, so some applications that worked fine on Android 1.1 would crash with a StackOverflowException in the newer Android. This, of course, was frustrating to developers who never realized there was an issue with view hierarchy depth and then got caught by this change.

The lessons to take from this are as follows:

  • Keep your view hierarchies shallow. Once you drift into double-digit depth, you are increasingly likely to run out of stack space.
  • If you encounter a StackOverflowException and the stack trace looks like it is somewhere in the middle of drawing your widgets, your view hierarchy is probably too complex.

Changing Resources

The core Android team may change resources with an Android upgrade, and those may have unexpected effects in your application. For example, in Android 1.5, the Android team changed the stock Button background, to allow for smaller buttons. However, applications that implicitly relied on the former larger minimum size wound up breaking and needing some UI adjustment.

Similarly, applications can reuse public resources, such as icons, available inside of Android proper. While doing so saves some storage space, many of these resources are public by necessity and are not considered part of the SDK. For example, hardware manufacturers may change the icons to fit some alternative UI look and feel. Relying on the existing ones to always look as they do is a bit dangerous. You are better served by copying those resources out of the Android open source project into your own code base.

Handling API Changes

The core Android team has generally done a good job of keeping APIs stable, and supporting a deprecation model when they do change APIs. In Android, when a feature is deprecated, that does not mean the feature is going away, just that its continued use is discouraged. And, of course, new APIs are released with every new Android update. Changes to the APIs are well documented with each release via an API differences report.

Unfortunately, the Android Market—the primary distribution channel for Android applications—allows you to upload only one Android package (APK) file for each application. Hence, you need that one APK file to deal with as many Android versions as possible. Many times, your code will “just work” and not require changing. Other times, though, you will need to make adjustments, particularly if you want to support new APIs on new versions while not breaking on old versions. Let's examine some techniques for handling these cases.

Minimum, Maximum, Target, and Build Versions

Android goes to great lengths to help you deal with the fact that at any point in time, there will be many Android OS versions out on the market. Unfortunately, the tools supplied by Android have given us a somewhat confusing set of overlapping concepts, such as targets and SDK versions. This section attempts to clarify those concepts.

Targets vs. SDK Versions vs. OS Versions

The concept of targets was introduced toward the beginning of this book. Targets are used when defining AVDs, to determine what sort of device those AVDs support. Targets are also used when creating new projects, primarily to determine what version of the SDK build tools will be used to build your project.

A target combines an API level with an indicator of whether or not the target includes Google APIs (e.g., Google Maps support).

An API level is an integer representing a version of the Android API. Each Android OS release that makes changes to the Android API triggers a new API level. Following are the API levels:

  • 3: Android 1.5r1, 1.5r2, and 1.5r3
  • 4: Android 1.6r1 and 1.6r2
  • 5: Android 2.0
  • 6: Android 2.0.1
  • 7: Android 2.1.x
  • 8: Android 2.2.x
  • 9: Android 2.3, 2.3.1, and 2.3.2
  • 10: Android 2.3.3 and 2.3.4
  • 11: Android 3.0.x
  • 12: Android 3.1.x
  • 13: Android 3.2
  • 14: Android 4.0

Google maintains a web page that outlines which versions of Android are in use today, based on requests made to the Android Market.

Minimum SDK Version

In your AndroidManifest.xml file, you should add a <uses-sdk> element. This element describes how your application relates to the various SDK versions.

The most critical attribute to have in <uses-sdk> is android:minSdkVersion. This indicates what the lowest API level is that your application supports. Devices running Android OS versions associated with lower API levels will not be able to install your application. Your application may not even appear to those devices in the Android Market listings, should you elect to publish via that distributor.

If you skip this attribute, Android assumes your application works on all Android API versions. That may be true, but it is rather dangerous to assume if you have not tested it. Hence, set android:minSdkVersion to the lowest level you are testing and are willing to support.

Target SDK Version

Another <uses-sdk> attribute is android:targetSdkVersion. This represents the version of the Android API that you are primarily developing for. Any Android device running a newer version of the OS may elect to apply some compatibility settings that will help apps such as yours, targeting an older API, to run on the newer version.

Most of the time, you should set this to be the current Android API version, as of the time you are publishing your application.

In particular, with Ice Cream Sandwich, you need to specify a target of 14 or 15 to get the new look and feel.

Maximum SDK Version

The third <uses-sdk> attribute is android:maxSdkVersion. Any Android device running a newer Android OS than is indicated by this API level will be prohibited from running your application.

On the plus side, this ensures that your application will not be used on API levels you have not tested, particularly if you set this to be the current Android API version as of your publication date.

However, bear in mind that your application will be filtered out of the Android Market for these newer devices. Over time, this will limit the reach of your application, if you do not release an update with a higher maximum SDK version.

The core Android team recommends that you not use this option and instead rely on Android's intrinsic backward compatibility—particularly leveraging your android:targetSdkVersion value—to allow your application to continue to run on new Android OS versions.

Detecting the Version

If you simply want to take different branches in your code based on version, the easiest thing to do is inspect android.os.Build.VERSION.SDK_INT. This public static integer value will reflect the same API level as you use when creating AVDs and specifying API levels in the manifest. So, you can compare that value to, say, android.os.Build.VERSION_CODES.DONUT to see whether you are running on Android 1.6 or newer.

Wrapping the API

So long as the APIs you try to use exist across all Android versions you are supporting, just branching may be sufficient. Where things get troublesome is when the APIs change, such as when there are new parameters to methods, new methods, or even new classes. You need code that will work regardless of Android version, while also letting you take advantage of new APIs where available.

The challenge is that if you try loading into the virtual machine code that refers to classes, methods, and such that do not exist in the version of Android that the device is running on, your application will crash with a VerifyError. You need to compile against the version of Android that contains the latest APIs you are trying to use—you just cannot load that code into an older Android device.

Note that the key phrase here is “load that code.” You don't necessarily have a problem just because a class exists in your application that uses a newer-than-available API. It is only if you execute code that triggers Android to load that class into your running process that you will encounter the VerifyError.

With that in mind, there are three primary tricks to deal with this situation, outlined in the following sections.

Detecting Classes

Perhaps all you need to do is disable some features in your app that lead to things that are not possible on a given device. For example, suppose you have an activity that uses the fragments feature. You cannot successfully start that activity on a pre-3.0 device. Stopping that activity may just be a matter of disabling a menu choice or Button or something.

To see if a certain class (say, ListFragment) is available to you, you can call Class.forName(). This will either return a Class object representing the requested class or throw an Exception if it is not available. You can use the exception handler as the spot to disable the UI paths that would cause your application to try to start an activity that uses the unavailable class.

Reflection

If you need limited access to a class that will not exist on older versions of Android, you can use a bit of reflection.

For example, in the chapter on rotation, we used a series of sample applications that allowed the user to pick a contact. That relied on an ACTION_PICKIntent, using a specific Uri for the contact's content provider. In those samples, we specifically used ContactsContract, the revised contacts API offered in Android 2.0 and beyond. That means those projects will not work on older versions of Android.

However, all we really need is this magic Uri value. If we can devise a way to get the right Uri for older versions of Android, as well as the right Uri for newer versions of Android, without causing problems, we can be more backward compatible.

Fortunately, this is fairly easy to do with some reflection:

static {
  intsdk=new Integer(Build.VERSION.SDK).intValue();

  if (sdk>=5) {
    try {
      Class clazz=Class.forName("android.provider.ContactsContract$Contacts");

      CONTENT_URI=(Uri)clazz.getField("CONTENT_URI").get(clazz);
    }
    catch (Throwable t) {
      Log.e("PickDemo", "Exception when determining CONTENT_URI", t);
    }
  }
  else {
    CONTENT_URI=android.provider.Contacts.People.CONTENT_URI;
  }
}

Here, we examine the API level of the device by looking at Build.VERSION.SDK (we could use Build.VERSION.SDK_INT, but that wasn't added until Android 1.6—the code shown here works on Android 1.5 as well). If we are at Android 2.0 (API level 5) or higher, we use Class.forName() to get at the new ContactsContract.Contacts class, and then use reflection to get at the CONTENT_URI static data member on that class. If we are on an older version of Android, we simply use the Uri published by the older Contacts.People class.

Since we are not directly referencing ContactsContract.Contacts in our code, we can safely execute this, even on older versions of Android.

Conditional Class Loading

Reflection works but is a pain for anything complex. Also, it is slower than calling code directly.

The most powerful technique, therefore, is simply to organize your code such that you have regular classes using newer APIs, but you do not load those classes on older devices. We will examine this technique later in this book.

Patterns for Ice Cream Sandwich and Honeycomb

With the advent of Honeycomb (Android 3.0), and now Ice Cream Sandwich (Android 4.0), supporting multiple Android versions is now a significant challenge. The UI changes required to support differing UIs will, in many cases, require you to take steps to make sure that your application still works successfully on older versions of Android. This section outlines some patterns for dealing with this area of backward compatibility.

The Action Bar

As noted in Chapter 27, many of the action bar's basic features will work in a backward-compatible fashion. For example, indicating than an options menu item can be shown in the action bar requires just an attribute in the menu resource XML, an attribute that will be ignored on older versions of Android. Honeycomb-capable devices will put the item in the action bar, while devices running previous Android versions will not.

However, not all of the action bar's features are backward compatible. In the Menus/ActionBar sample application in Chapter 27, we added a custom View to the action bar, to allow people to add words to our list without dealing with menus and dialog boxes. However, this required some code that works only on API level 11 (Android 3.0) and higher. More advanced action bar capabilities—ones beyond the scope of this book—will have similar requirements.

You need to arrange to use those action bar methods only on devices that run API level 11 or higher. Conditional class loading, outlined earlier in this chapter, is one such technique, and is the technique used in the Menus/ActionBarBC sample application. Let's take a look at how this works.

Checking the API Level

Our originalonCreateOptionsMenu() looked like this:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
  new MenuInflater(this).inflate(R.menu.option, menu);

  EditText add=(EditText)menu
                         .findItem(R.id.add)
                         .getActionView()
                         .findViewById(R.id.title);

  add.setOnEditorActionListener(onSearch);

  return(super.onCreateOptionsMenu(menu));
}

This is fine, but it will work only on API level 11 and higher, as getActionView() only exists from that API level onward. Hence, we cannot run this code, or even load this class, on older versions of Android without getting a VerifyError.

The new version of onCreateOptionsMenu() hides the offending code and checks the API level:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
  new MenuInflater(this).inflate(R.menu.option, menu);

  EditText add=null;

  if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.HONEYCOMB) {
    View v=ICSHCHelper.getAddActionView(menu);

    if (v!=null) {
      add=(EditText)v.findViewById(R.id.title);
    }
  }

  if (add!=null) {
    add.setOnEditorActionListener(onSearch);
  }

  return(super.onCreateOptionsMenu(menu));
}

We hide only the code that retrieves the View that we theoretically have put in the action bar. If we are on an older version of Android, the HONEYCOMB check will fail, and we will wind up with a nullView, so we skip adding the OnEditorActionListener to the EditText inside of that View.

This has another benefit: it works if the Android device runs API level 11 or higher but does not have room for our custom View. Android tablets will have an action bar and sufficient room, but future Honeycomb-capable phones might have an action bar but lack sufficient room. In that case, the phone would leave the Add options menu item in place, and we still would wind up with a nullView. This code handles that scenario; the original code did not.

Isolating the Ice Cream Sandwich/Honeycomb Code

Our Honeycomb-specific code is held in a separate ICSHCHelper class (ICS for Ice Cream Sandwich, HC for Honeycomb), one that will only be used on API level 11 (or higher) devices:

packagecom.commonsware.android.inflation;

importandroid.view.Menu;
importandroid.view.View;

classICSHCHelper {
  static View getAddActionView(Menu menu) {
    return(menu.findItem(R.id.add).getActionView());
  }
}

ICSHCHelper has a single getAddActionView() static method that finds the View for the Add action bar entry, if there is one.

Since we do not try to execute any code on this class except for inside the HONEYCOMB check, it is safe to have this class on older versions of Android. The Menus/ActionBarHC app works on Android 1.6 and newer.

Writing Tablet-Only Apps

Ideally, your Android applications work on all form factors: phones, tablets, and so forth. However, you may want to create an app that simply would be unusable on phones. Ideally, you would want to keep your app off of small-screen devices, so that users are not disappointed.

To do this, you can take advantage of the fact that Android will scale apps up but will not scale apps down. In other words, if you specify that your application does not support some larger screen sizes (e.g., android:xlargeScreens="false" appears in your <supports-screens> element in your AndroidManifest.xml file), Android still allows your app to run on such screens and takes steps to help your app run with the additional screen space. However, if you specify that your application does not support some smaller screen sizes (e.g., android:smallScreens="false" appears in your <supports-screens> element), Android will not run your app, and you will be filtered out of the Android Market for such devices.

Hence, if your application will work well only on larger-screen devices, use a <supports-screens> element like this:

<supports-screens android:xlargeScreens="true"
                 android:largeScreens="true"
                 android:normalScreens="false"
                 android:smallScreens="false"
                 android:anyDensity="true"/>
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset