Tuesday, June 28, 2011

A Deep Dive Into Location Part 2: Being Psychic and Staying Smooth

This is part two of A Deep Dive into Location. This post focuses on making your apps psychic and smooth using the Backup Manager, AsyncTask, Intent Services, the Cursor Loader, and Strict Mode.
The code snippets used are available as part of the Android Protips: A Deep Dive Into Location open source project. More pro tips can be found in my Android Pro Tips presentation from Google I/O. 
Being Psychic

You've just had to factory reset your device - never a good day - but yay! You've opted in to "backup my settings" and Android is happily downloading all your previously installed apps. Good times! You open your favourite app and... all your settings are gone.


Backup Shared Preferences to the Cloud using the Backup Manager

If you're not using the Backup Manager to preserve user preference to the cloud I have a question for you: Why do you hate your users? The Backup Manager was added to Android in Froyo and it's about as trivial to implement as I can conceive.

All you need to do is extend the BackupAgentHelper and create a new SharedPreferencesBackupHelper within it's onCreate handler.

As shown in the PlacesBackupAgent, your Shared Preferences Backup Helper instance takes the name of your Shared Preference file, and you can specify the key for each of the preferences you want to backup. This should only be user specified preferences - it's poor practice to backup instance or state variables.

public class PlacesBackupAgent extends BackupAgentHelper {
  @Override
  public void onCreate() {
    SharedPreferencesBackupHelper helper = new
      SharedPreferencesBackupHelper(this, PlacesConstants.SHARED_PREFERENCE_FILE);
    addHelper(PlacesConstants.SP_KEY_FOLLOW_LOCATION_CHANGES, helper);
  }
}


To add your Backup Agent to your application you need to add an android:backupAgent attribute to the Application tag in your manifest.

<application android:icon="@drawable/icon" android:label="@string/app_name"
             android:backupAgent="PlacesBackupAgent">


You also need to specify an API key (which you can obtain from here: http://code.google.com/android/backup/signup.html)

<meta-data android:name="com.google.android.backup.api_key"
           android:value="Your Key Goes Here" />


To trigger a backup you just tell the Backup Manager that the data being backed up has changed. I do this within the SharedPreferenceSaver classes, starting with the FroyoSharedPreferenceSaver.

public void savePreferences(Editor editor, boolean backup) {
  editor.commit();
  backupManager.dataChanged();
}


Being Smooth: Make everything asynchronous. No exceptions.

Android makes it easy for us to write apps that do nothing on the main thread but update the UI.


Using AsyncTask

In this example, taken from PlaceActivity, I'm creating and executing an AsyncTask class to lookup the best previous known location. This isn't an operation that should be particularly expensive - but I don't care. It isn't directly updating the UI, so it has no business on the main application thread.

AsyncTask<void, void, void> findLastLocationTask = new AsyncTask<void, void, void>() {
  @Override
  protected Void doInBackground(Void... params) {
    Location lastKnownLocation =
      lastLocationFinder.getLastBestLocation(PlacesConstants.MAX_DISTANCE,
      System.currentTimeMillis()-PlacesConstants.MAX_TIME);

    updatePlaces(lastKnownLocation, PlacesConstants.DEFAULT_RADIUS, false);
    return null;
  }
};
findLastLocationTask.execute();


You'll note that I'm not touching the UI during the operation or at its completion, so in this instance I could have used normal Thread operations to background it rather than use AsyncTask.

Using the IntentService

Intent Services implement a queued asynchronous worker Service. Intent Services encapsulate all the best practices for writing services; they're short lived, perform a single task, default to Start Not Sticky (where supported), and run asynchronously.

To add a new task to the queue you call startService passing in an Intent that contains the data to act on. The Service will then run, executing onHandleIntent on each Intent in series until the queue is empty, at which point the Service kills itself.

I extended Intent Service for all my Service classes, PlacesUpdateService, PlaceDetailsUpdateService, PlaceCheckinService, and CheckinNotificationService.

Each implementation follows the same pattern, as shown in the PlacesUpdateService extract below.

@Override
protected void onHandleIntent(Intent intent) {
  String reference = intent.getStringExtra(PlacesConstants.EXTRA_KEY_REFERENCE);
  String id = intent.getStringExtra(PlacesConstants.EXTRA_KEY_ID);

  boolean forceCache = intent.getBooleanExtra(PlacesConstants.EXTRA_KEY_FORCEREFRESH, false);
  boolean doUpdate = id == null || forceCache;

  if (!doUpdate) {
    Uri uri = Uri.withAppendedPath(PlaceDetailsContentProvider.CONTENT_URI, id);
    Cursor cursor = contentResolver.query(uri, projection, null, null, null);

    try {
      doUpdate = true;
      if (cursor.moveToFirst()) {
        if (cursor.getLong( cursor.getColumnIndex(           PlaceDetailsContentProvider.KEY_LAST_UPDATE_TIME)) >
          System.currentTimeMillis()-PlacesConstants.MAX_DETAILS_UPDATE_LATENCY)
            doUpdate = false;
      }
    }
    finally {
      cursor.close();
    }
  }

  if (doUpdate)
    refreshPlaceDetails(reference, forceCache);
}


Note that the queue is processed on a background thread, so I can query the Content Provider without having to spawn another background thread.

CursorLoaders are awesome. Use them.

Loaders are awesome; and thanks to the compatibility library, they're supported on every platform back to Android 1.6 - that’s about 98% of the current Android device install base.

Using CursorLoaders is a no-brainer. They take a difficult common task - obtaining a Cursor of results from a Content Provider - and implement, encapsulate, and hide all the bits that are easy to get wrong.

I've already fragmented and encapsulated my UI elements by creating three Fragments -- PlaceListFragment, PlaceDetailFragment, and CheckinFragment. Each of these Fragments access a Content Provider to obtain the data they display.

The list of nearby places is handled within the PlaceListFragment, the relevant parts of which are shown below.

Note that it's entirely self contained; because the Fragment extends ListFragment the UI is already defined. Within onActivityCreated I define a Simple Cursor Adapter that specifies which Content Provider columns I want to display in my list (place name and my distance from it), and assign that Adapter to the underlying List View.

The final line initiates the Loader Manager.

public void onActivityCreated(Bundle savedInstanceState) {
  super.onActivityCreated(savedInstanceState);
  activity = (PlaceActivity)getActivity();

  adapter = new SimpleCursorAdapter(activity,
    android.R.layout.two_line_list_item,
    cursor,
    new String[]
      {PlacesContentProvider.KEY_NAME, PlacesContentProvider.KEY_DISTANCE},
    new int[] {android.R.id.text1, android.R.id.text2}, 0);

  // Allocate the adapter to the List displayed within this fragment.
  setListAdapter(adapter);

  // Populate the adapter / list using a Cursor Loader.
  getLoaderManager().initLoader(0, null, this);
}


When the Loader is initiated we specify the parameters we would normally pass in to the Content Resolver when making a Content Provider query. Instead, we pass those parameters in to a new CursorLoader.

public Loader<cursor> onCreateLoader(int id, Bundle args) {
  String[] projection = new String[]
    {PlacesContentProvider.KEY_ID,
    PlacesContentProvider.KEY_NAME,
    PlacesContentProvider.KEY_DISTANCE,
    PlacesContentProvider.KEY_REFERENCE};

  return new CursorLoader(activity, PlacesContentProvider.CONTENT_URI,
    projection, null, null, null);
}


The following callbacks are triggered when the Loader Manager is initiated, completed, and reset respectively. When the Cursor has been returned, all we need to do is apply it to the Adapter we assigned to the List View and our UI will automatically update.

The Cursor Loader will trigger onLoadFinished whenever the underlying Cursor changes, so there's no need to register a separate Cursor Observer or manage the Cursor lifecycle yourself.

public void onLoadFinished(Loader loader, Cursor data) {
  adapter.swapCursor(data);
}

public void onLoaderReset(Loader loader) {
  adapter.swapCursor(null);
}


The PlaceDetailFragment is a little different; in this case we don't have an Adapter backed ListView to handle our UI updates. We initiate the Loader and define the Cursor parameters as we did in the Place List Fragment, but when the Loader has finished we need to extract the data and update the UI accordingly.

Note that onLoadFinished is not synchronized to the main application thread, so I'm extracting the Cursor values on the same thread as the Cursor was loaded, before posting a new Runnable to the UI thread that assigns those new values to the UI elements - in this case a series of Text Views.

public void onLoadFinished(Loader loader, Cursor data) {
  if (data.moveToFirst()) {
    final String name = data.getString(
      data.getColumnIndex(PlaceDetailsContentProvider.KEY_NAME));
    final String phone = data.getString(
      data.getColumnIndex(PlaceDetailsContentProvider.KEY_PHONE));
    final String address = data.getString(
      data.getColumnIndex(PlaceDetailsContentProvider.KEY_ADDRESS));
    final String rating = data.getString(
      data.getColumnIndex(PlaceDetailsContentProvider.KEY_RATING));
    final String url = data.getString(
      data.getColumnIndex(PlaceDetailsContentProvider.KEY_URL));

    if (placeReference == null) {
      placeReference = data.getString(
        data.getColumnIndex(PlaceDetailsContentProvider.KEY_REFERENCE));
      updatePlace(placeReference, placeId, true);
    }

    handler.post(new Runnable () {
      public void run() {
        nameTextView.setText(name);
        phoneTextView.setText(phone);
        addressTextView.setText(address);
        ratingTextView.setText(rating);
        urlTextView.setText(url);
      }
    });
  }
}


Using Strict Mode will prevent you from feeling stupid

Strict Mode is how you know you've successfully moved everything off the main thread. Strict Mode was introduced in Gingerbread but some additional options were added in Honeycomb. I defined an IStrictMode Interface that includes an enableStrictMode method that lets me use whichever options are available for a given platform.

Below is the enableStrictMode implementation within the LegacyStrictMode class for Gingerbread devices.

public void enableStrictMode() {
  StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
    .detectDiskReads()
    .detectDiskWrites()
    .detectNetwork()
    .penaltyDialog()
    .build());
}


The only thing I hate more than modal dialogs in apps is apps that freeze because a network read or disk write is blocking the UI thread. As a result I've enabled detection of network and disk read/writes and reports using a modal dialog.

I've applied Strict Mode detection to the entire app by extending the Application class to instantiate the appropriate IStrictMode implementation and enable Strict Mode. Note that it is only turned on in developer mode. Be sure to flick that switch in the constants file when you launch.

public class PlacesApplication extends Application {
  @Override
  public final void onCreate() {
    super.onCreate();

    if (PlacesConstants.DEVELOPER_MODE) {
      if (PlacesConstants.SUPPORTS_HONEYCOMB)
        new HoneycombStrictMode().enableStrictMode();
      else if (PlacesConstants.SUPPORTS_GINGERBREAD)
        new LegacyStrictMode().enableStrictMode();
    }
  }
}

Thursday, June 23, 2011

How to Build Location-Based Apps That Don't Suck

If I were forced to choose between a smartphone that could make / receive voice calls, and one with Google Maps - I would choose Maps without blinking.

Here Back in London, getting a reliable 3G connection is a challenge at the best of times - getting one while sat in most venues is about as likely as a South West Trains running a good service. So it doesn't help when I go to view details for, checkin, or review a location and a lack of 3G signal thwarts my efforts.

Whether it's opening a FourSquare app to checkin, or Qype / Zagat / Where to choose where to eat, or the London Cycle Hire Widget to find a Boris Bike - I always feel like a douche standing around with my phone in my hand for half a minute while my phone gets a GPS fix and downloads the nearest locations.

High latency and a lack of offline support in location-based mobile apps is a blight that must be cleansed

Rather than (or indeed: after) shaking my fist at the sky in impudent rage, I wrote an open-source reference app that incorporates all of the tips, tricks, and cheats I know to reduce the time between opening an app and seeing an up-to-date list of nearby venues - as well as providing a reasonable level of offline support.

You can find out more in the associated deep-dive into location on the Android Developer Blog.

Android Protips: Location Best Pratices

It should came as no surprise to learn that I've borrowed heavily from my Android Protips presentation from Google I/O. Including (but not limited to) using Intents to receive location updates, using the Passive Location Provider, using Intents to passively receive location updates when your app isn't active, monitoring device state to vary refresh rate, toggling your manifest Receivers at runtime, and using the CursorLoader.

But Wait There's More!

The post on the Android Developer Blog focusses on freshness - I'll be posting another deep-dive into the code that examines how I've made the app psychic and smooth on this blog early next week. Stay tuned.