Thursday, December 12, 2013

Android's SyncAdapter pattern

This is an article I wrote for MobiTV,Inc. While looking to understand this Google framework, I found dozens and dozens of articles on the internet, but it was still difficult to comprehend. The best articles talks about how we would implement the entire pattern, but I really only wanted to use the SyncAdapter, without utilizing the ContentProvider or the Authenticator piece. I don't yet consider myself an expert in Android development, so comments and corrections are more than welcomed.

The grand idea:

Wouldn’t it be great if your application could startup instantly without any network latency? Well, that’s the idea behind SyncAdapter. It allows Android to synchronize data between a remote resource and your local copy on a daily basis, or more frequently if required, so that when your application starts, it would already have all the data it requires locally. Theoretically, it could allow your application to should run even when the device is disconnected from the network. 

How it's envisioned

When the application is first launched, it ask the user to create or login to an account. That account information is saved in Android's Account settings. During the night, the phone is left idle with full battery and charger attached with good wifi network. The Android "Sync Manager" retrieves all the Accounts in the phone, and serially it starts its SyncAdapter service. Your app's SyncAdapter service starts and begin to download the next day's guide program and newly added TV shows information. Those information are saved by the ContentProvider backed by an SQLite database. Your service is stopped once everything is done. The user wakes up the next day, takes her phone on the train and starts the app. The app first ask the ContentProvider (via ContentResolver) for cached data. It retrieves all the data from the local database and returns them to the app. Your user is happy because the app started quickly.
On the next night, the same thing happens, but this time there's a network outage for several hours. Android's "Sync Manager" realizes the network issue and exponentially back off retries until it establishes good connection. By the following morning, one of the retry worked and our user takes her phone on the train again. And again the app starts immediately because all the data has been cached. Your user is again happy, not knowing the drama that unfolded during the night.

The components:

The SyncAdapter pattern requires three pieces to be present, but only the SyncAdapter needs to have useful code. It needs the SyncAdapter, a ContentProvider, and a Authenticator. Lets take a look at each of these components.


SyncAdapter

Your SyncAdapter is a class that extends AbstractThreadedSyncAdapter and implements the onPerformSync function. The function is responsible for calling any methods that you wish to perform the fetching and persisting of remote resources. In a way, it's like a Runnable that you would implement, except it's a Runnable that Android's "Sync Manager" understands. This method will be called at the proper time when the OS deems appropriate. This typically means every 24 hours since the SyncAdapter was initially setup. But it can also be delayed if another application is busy doing their synchronization. It may also be delayed if network is spotty or if it's roaming etc.   

Service

A SyncAdapter class needs a service host. Any Service will do.  There are two things this service needs in order to work with the SyncAdapter
1/ Implements the onBind method to return the SyncAdapter's binder


2/ Declare the service as receiving SyncAdapter intent

notice that the meta-data element references res/xml/syncadapter.xml file which we will go over in the Account section.

ContentProvider

A ContentProvider is typically a thin layer on top of an SQLite database. This is where your application may store the remote resources and provide quick access to data. Here at MobiTV, it’s unlikely that we would encourage other applications to use our unprotected guide data, user’s personalized recommendation, etc, so our  ContentProvider maybe just a stub.
ContentProvider.java:
“A content provider is only required if you need to share data between multiple applications. For example, the contacts data is used by multiple applications and must be stored in a content provider. If you don't need to share data amongst multiple applications you can use a database directly via android.database.sqlite.SQLiteDatabase.”
While we may not make use of a ContentProvider, it is still required by the SyncAdapter. So we create a StubContentProvider which extends ContentProvider and implement all methods to return etiher false, 0, or null.

Account

While much of the data you wish to synchronize are public data, some will be specific to your user. In that case, the SyncAdapter would need to access the user’s account and password. Secondly, the Account screen is where Android user expect to find the sync settings for their personal information.
For example, this is what you see if you install the Evernote application.
Of course you may not want to always fetch protected data. Perhaps all we want is to make sure that we update EPG program data nightly. These data are publicly accessible and doesn't require username and password. In that case we can programmatically create an account and hide the account from the user. You can do that by setting the android:userVisible flag to false in the syncadapter.xml file; I've found from experience that it may also require that you don't put any android:accountPreferences, icon, or label in the authenticator.xml file. If you don't already have a xml folder under the res directory, you need to create the folder and add this xml file. You can call it whatever you want, as long as it matches what is declared in the meta-data field of your SyncAdapter Service. This xml file needs just one root element <sync-adapter> and there you can specify

AccountManager

To create an account for the user, whether from within your application or via the Accounts screen in Android's Setting, you will need to write a few lines of code to let The AccountManager know what kind of account you want to create.

Note that authToken here is passed in as a String. You will have to write an Activity to allow the user to provide their user name and password, then use that information to retrieve the auth token in your own code.

To retrieve the account later, you simply call getAccountsByType and pass in the account type string that you created the account with previously

Authenticator

Your Authenticator is a class that extends AbstractAccountAuthenticator. This is where you will connect your code to retrieve auth token etc. Of course if you're only interested in publicly accessible data, then you can just implement empty functions that return null. You will probably still need to pass in a dummy username when you create an Account in the AccountManager.
However, If you actually want to implement real authentication, there are several methods that you need to implement, but the most important ones are addAccount and getAuthToken
Example:

Service

The host for the Authenticator is similar to the host SyncAdapter. It has to be a Service and the following requirements
1/ Implements the onBind method to return the Authenticator's binder

2/ Declare the service as receiving AccountAuthenticator intent


Connections

The SyncAdapter/Service, Authenticator/service, and ContentProviders are connected together by using the same contentAuthority and accountType. Here are some notes about them.
android:contentAuthority
A provider usually has a single authority, which serves as its Android-internal name. To avoid conflicts with other providers, you should use Internet domain ownership (in reverse) as the basis of your provider authority. Because this recommendation is also true for Android package names, you can define your provider authority as an extension of the name of the package containing the provider. For example, if your Android package name iscom.example.<appname>, you should give your provider the authority com.example.<appname>.provider.
The String must match in the following places:
  • AccountManager
    • addAccountExplicitly
    • getAccountsByType
    • etc
  • syncadapter.xml
  • AndroidManifest.xml
android:accountType
accountType is used to identify the type of account data to sync. The String must match in the following places:
  • AccountManager
    • addAccountExplicitly
    • getAccountsByType
    • etc
  • syncadapter.xml
  • authenticator.xml
    • Authenticator class will need to use the accountType in when sending KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, but the type will be an attribute in the Account being passed in.
    • Authenticator class will also utilize accountType when AccountManager.getAuthToken is called.

Recipe

  1. 1 Service class + 1 SynAdapter class
  2. 1 Service class + 1 Authenticator class
  3. 1 Content Provider class
  4. XML resources
    1. 1 syncadapter.xml
    2. 1 authenticator.xml
    3. 1 account_preferences.xml (optional)
  5. 1 AndroidManifest.xml

Simplified AndriodManifest.xml








Design considerations

Worst case scenario
What Happens the first time the application is launched? The SyncAdapter never had a chance to start so there are no persisted data the app can rely on. Furthermore, if you only get data from the SyncAdapter, you have to wait for the Service to start, which slows down your application boot time. You'll need a mechanism for both the app to get data on demand as well as allowing the SyncAdapter to use those code to retrieve and persist those data.
Diamonds are forever, Services are not
Services are not supposed to stay alive forever. In the case of SyncAdapter Service, it should startup on its own by the "Sync Manager", perform its sync, and then the Service would be stopped. So it's important that the SyncAdapter doesn't rely on configuration setup that isn't known to the SyncAdapter.

References




1 comment: