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.
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)
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 findLastLocationTask = new AsyncTask() {
@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 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();
}
}
}