Global data sync in Android apps

I'm currently working on an app that needs to sync both global and user-specific data. In this post I'd like to highlight some poorly documented areas of the process.

Let's start with the list of bits we'll need for a successful sync:

  • ContentProvider. Even if you don't use content providers, you'll need one, or the whole thing will crash on you.

  • Authenticator and a binding service. Sync framework uses authenticators to manage user account tokens. Even if you don't need user account to access global app data, you need an authenticator. Let it be a no-op empty thing.

  • Sync adapter and a binding service. This is the heart of it. Every time the sync is needed, the onPerform method is called.

You may have noticed that:

  • We need binding services for authenticator and sync adapter. The services are the entry points for the sync framework and other apps (if you choose to allow that).

  • We still need an account for an account-independent sync. If you look at other apps, like Shazam that require similar syncs, you'll notice they have a "Sync" account hardwired in Settings -> Accounts -> <App name>

Next, let's look at the steps. There's a great class by Google that you can follow to get your basic setup ready. Here's what you will do:

  • Create a no-op content provider
  • Create a no-op authenticator and its binding service, declare it in the manifest
  • Create an empty sync adapter with its binding service, declare it in the manifest

A couple of things that puzzled me at this point:

  • Account type (as seen in xml/authenticator.xml, xml/syncadapter.xml and MainActivity) is the identifier of the sync account. You tie together sync adapter with authenticator and account with it.

  • Authority (as seen in xml/syncadapter.xml and provider definition in manifest). This is a link between the content provider and the rest of the world. At the very least, every sync adapter is linked to a specific content provider through the authority identifier.

Creating user account

In Google class it is shown how the sync account is added through the AccountManager. What's missing is the explanation of how to deal with the edge cases.

  • During every app start, when you setup sync account, you need to check if the account with your account type is already present. If it is, you just use that account, and not even attempt adding a new one. Here's the bit of code:
AccountManager accountManager = AccountManager.get(this);  
Account[] accounts = accountManager.getAccountsByType(ACCOUNT_TYPE_GLOBAL);  
if (accounts.length > 0) {  
    globalSyncAccount = accounts[0];
} else {
    Account newAccount = new Account(ACCOUNT_GLOBAL, ACCOUNT_TYPE_GLOBAL);
    ...
    // add new account and init (see below)
}
  • When you are adding an account, you may want to configure it on the spot. Sync framework operates on per-account basis, which means that you need to configure an account if you want to enable periodic updates etc. Here's how it's done:
if (accountManager.addAccountExplicitly(newAccount, null, null)) {  
    // registered new account, save it
    globalSyncAccount = newAccount;

    // Configure automatic account updates
    ContentResolver.setIsSyncable(globalSyncAccount, AUTHORITY_GLOBAL, 1);
    ContentResolver.setSyncAutomatically(globalSyncAccount, AUTHORITY_GLOBAL, true);
    ContentResolver.addPeriodicSync(globalSyncAccount, AUTHORITY_GLOBAL, Bundle.EMPTY, 60 * 60);
} else {
    // failed to register
    globalSyncAccount = null;
}

Forcing sync on app start

Basically, you don't do this. Following the traditional approach deeply ingrained in my mind, I tried to make it work and succeeded, but somewhere along the way I've got an understanding that in most cases this is completely unnecessary. The reason for that is periodic updates. You can configure your global data account to be updated (roughly) once an hour or once a day (depending on how frequently your data changes) and the system will perform the updates even when the app isn't running. Which means that you literally have the most recent data whenever your app starts.

To get laser-precise with updates, you can use GCM to get an invisible to user notifications when the backend changes data. Your app will be able to seamlessly update itself in the background in the most energy-friendly fashion (sync framework keeps an eye on when it's the best time to run syncs, batches them and does the rest to save energy).

If you still absolutely need to update on launch, go ahead and send a manual sync request. Just note that in some cases, you may encounter that your syncs run twice in a row. The exact nature of this effect is still unclear, but it looks like happening if you request a sync from the Application.onCreate() method. Moving it to main activity's onCreate() solved the problem (watch out for orientation changes which trigger this method -- you don't want to send a sync request every time the user turns their device).

What's next?

Next I'll be working on integrating user-specific syncs. This will get much more curious since it's a two-way street.

comments powered by Disqus