Jul 19, 2010

Supporting multiple Android versions in Your application

Often when developing apps for the Android platform you come across the problem of supporting multiple Android versions. There are two flavors of this problem. The first is when the API you need does not exist in the older version and thus you can only use it in the new version. An example is the AccountManager which is available since API level 5 (Android 2.0). The second flavor is when the API changed between versions. In this case You can (if you are lucky) find a common ground between the versions and use it with both versions. An example of this problem is the Contacts API which changed drastically between versions 1.6 and 2.0. There are several solutions to this problems. A few of them are mentioned in this Google I/O video (at about minute 36).

The first flavor
As I mentioned earlier this is the flavor where the API did not exist in the older version. I will elaborate on an example with the AccountManager. The base of the solution is to isolate the behavior that causes the problem (i.e. the AccountManager stuff) in a separate component. The first step is to design an interface through which the component will communicate with the rest of the world. In this case I will use the following interface:

public interface IGoogleAuth {
  List<String> getGoogleAccounts();
  ...
}   

Next I will implement it:

class GoogleAuth implements IGoogleAuth {
  private static final String ACCOUNT_TYPE = "com.google";

  @Override
  public List<String> getGoogleAccounts(){
    AccountManager accountManager =                         AccountManager.get(App.getContext());
    Account[] accounts = accountManager.getAccountsByType(ACCOUNT_TYPE);
    ArrayList
<String> accList = new ArrayList();
    if (accounts != null && accounts.length > 0) {
      for(Account a: accounts) {
       
accList.add(a.name);
      }
    }
    return
accList;
  }
  ...
}

The code is pretty straightforward. First an AccountManager instance is requested from the application and then the instance is queried for Google Accounts (they have the type "com.google"). If the account manager returns any accounts, their name (email) is added to the list that is returned to the caller. Note that the GoogleAuth class is package private since it should not be used outside the enclosing package. One important point to note is that the interface must not use any classes that are defined in the new API because then the application would throw an exception probably causing a Force Close dialog when run on a device with an older Android version. In other words, the interface must only use classes that all involved versions can process. For example, the interface must not use the Account class in the following way:

public interface IGoogleAuth {
  List<Account> getGoogleAccounts();
  ...
}   

since it would crash on an older Android version. Now that the implementation is finished we can implement the factory class:

public enum GoogleAuthFactory {
  ;
  private static IGoogleAuth auth = null;
  private static boolean isSet = false; 

  public static boolean hasGoogleAuth() {
    int sdkVersion = Integer.parseInt(Build.VERSION.SDK);
    return sdkVersion >= Build.VERSION_CODES.ECLAIR;
  }  

  public static IGoogleAuth get() {
    if (!isSet) {
      if (hasGoogleAuth()) {
          auth = new GoogleAuth();
      }
      isSet = true;
    }
    return auth;
  }
}

All that remains is to use the code. It can be used in the following way:

  if(GoogleAuthFactory.hasGoogleAuth()) {
    //This device has a version that implements the api
    IGoogleAuth auth = GoogleAuthFactory.get(); 
    List<Account> accountList = auth.getGoogleAccounts();
    // Display the account list
  } else {
    //This device has an older version. Do something else. 
  }

The code above will work on both versions without problems. Now let's get on to the second flavor.

The second flavor
Now let's examine the second type of problem that includes an API that both versions support but has changed drastically between versions. This problem is actually very similar to the first one but it has its own set of tips and tricks. To elaborate this problem I will use the Contacts API that has changed a lot between versions 1.6 and 2.0. Let's say that we want to start the default activity for adding a new contact to our contacts. To start the activity we need an intent. Such an intent exists, of course, but it looks like this in version 1.6:

Intent i = new Intent(Contacts.Intents.Insert.ACTION, Contacts.People.CONTENT_URI);

and it looks like this in version 2.0:

Intent i = new Intent(Intent.ACTION_INSERT, ContactsContract.Contacts.CONTENT_URI);

It should be noted that in this example starting the activity with the version 1.6 intent on the version 2.0 device would probably work but I am using it here just to demonstrate a technique and to keep the code snippet size at bay. But with this technique You could also implement a method that returns a list of contacts that have an email address which is a lot less trivial task to implement on both versions (that's another story). The base of the solution is the same as with the first kind of problem. We need to isolate the different API's in separate components. First we will design the interface to the world. It looks like this:

public interface IContact {
  Intent getAddContactIntent();
}

Next we implement the interfaces in package private classes. Once for the version 1.6 API:

class ContactVersion4 implements IContact { 
  @Override
  public Intent getAddContactIntent() {
    return new Intent(Contacts.Intents.Insert.ACTION, Contacts.People.CONTENT_URI);
  }
}

And the second time for the version 2.0 API:

class ContactVersion5 implements IContact { 
  @Override
  public Intent getAddContactIntent() {
    return new Intent(Intent.ACTION_INSERT, ContactsContract.Contacts.CONTENT_URI);
  }
}

Everything gets connected in the factory class:

public enum ContactFactory {
  ;
  private static IContact contact = null;

  public static IContact get() {
    if (contact == null) {
      int sdkVersion = Integer.parseInt(Build.VERSION.SDK);
      if (sdkVersion <= Build.VERSION_CODES.DONUT) {
        contact = new ContactProcesorStari();
      } else {
        contact = new ContactProcesorNovi();
      }
    }
    return contact;
  }
}

The get method determines the Android version on the first invocation and creates the appropriate IContact implementation according to the version. The reference is  then cached for future invocations. The only thing that remains to do is to use the factory. This is how it is done:

  IContact contact = ContactFactory.get();
  startActivityForResult(contact.getAddContactIntent(), 1);

This code is safe to use on both Android version 1.6 and version 2.0. A variant of the factory implementation involves using reflection but I do not like to use reflection unless I absolutely have to. I hope this will help someone. Forgive my beginner's mistakes since this is the first blog I have written.

References
Nick's blog
Working with Android contacts

No comments:

Post a Comment