Introduction
Most of the applications that are interesting in some way are connected to live data on the Internet. A common scenario is then how to keep local and remote content synchronized. Android does provide an easy and straightforward mechanism to achieve this synchronization: the SyncAdapter.
What is a SyncAdapter?
Stop anything you are doing right now and watch this excellent video by Virgil Dobjanschi. If you are in a hurry forward to 44' when Virgil introduces the third pattern.
To sum up Virgil's words: a SyncAdapter is just a service that is started by the sync manager, which in turn maintains a queue of SyncAdapters. You delegate the responsibility for choosing when to sync to the system, who knows better than you what's going on and which other applications are synchronizing data. The sync manager does also schedule resyncs according to the errors reported by the SyncAdapter, making your code cleaner and eliminating the need of alarms.
How does this work?
The basic idea is to mirror the remote data in a local database and access it through a ContentProvider. You then access this local ContentProvider from you app. You fetch data from it and impact any changes on it.
The SyncAdapter will be in charge of making the remote content match your local data. It will fetch new data and push the local changes to the server.
Writing a ContentProvider
Writing a ContentProvider is outside the scope of this entry, so go read it anywhere else, like here, here or here. There are some other players that we need to take care of.
Adding an account
You need to add an account in order to use a SyncAdapter. This account will appear in the Accounts & sync section of the settings application. From this section the user is able to add, modify or remove his account, as well as choosing what content to synchronize. Your application will also play fairly when the user enables automatic synchronization.
The AccountManager is in charge of managing user credentials. The user enters his credentials just once and all the applications that have the USE_CREDENTIALS permission can query the manager to obtain an authentication token.
In order to add an account to your application you have to extend the AbstractAccountAuthenticator class. It is okay to return null values from the methods you are not going to use. The important ones are addAcount and getAuthToken.
@OverrideYou have probably noticed that there is a reference to an activity named AuthenticatorActivity. This is just an activity that asks for the user credentials. It will be prompted when the user adds a new account from settings. You can of course use the same activity than when your application is launched.
public Bundle addAccount(AccountAuthenticatorResponse response,
String accountType, String authTokenType,
String[] requiredFeatures, Bundle options)
throws NetworkErrorException {
final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
intent.putExtra(AuthenticatorActivity.PARAM_AUTHTOKEN_TYPE,
authTokenType);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,
response);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
The other important method to override is getAuthToken. You will call this method from your SyncAdapter if you need to do any authenticated requests to the server. It will return the same token until it is explicitly invalidated in the account manager. Here is where the logic to get an auth token from the server will be placed.
@OverrideAndroid provides a handy class called AccountAuthenticatorActivity that has a method to set the authentication result back to the account authenticator. Here is the relevant code of our AuthenticatorActivity that adds the account and sets the result.
public Bundle getAuthToken(AccountAuthenticatorResponse response,
Account account, String authTokenType, Bundle options)
throws NetworkErrorException {
if (!authTokenType.equals(AuthenticatorActivity.PARAM_AUTHTOKEN_TYPE)) {
final Bundle result = new Bundle();
result.putString(AccountManager.KEY_ERROR_MESSAGE,
"invalid authTokenType");
return result;
}
final AccountManager am = AccountManager.get(mContext);
final String password = am.getPassword(account);
if (password != null) {
boolean verified = callSomeLoginServiceThatReturnsTrueIfValid(
account.name, password);
if (verified) {
final Bundle result = new Bundle();
result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
result.putString(AccountManager.KEY_ACCOUNT_TYPE,
AuthenticatorActivity.PARAM_ACCOUNT_TYPE);
result.putString(AccountManager.KEY_AUTHTOKEN,
hereGoesTheReceivedToken);
return result;
}
}
// Password is missing or incorrect. Start the activity to add the missing data.
final Intent intent = new Intent(mContext, AuthenticatorActivity.class);
// ...
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE,
response);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
private void finishLogin(String token) {
final Account account = new Account(mUsername, ACCOUNT_TYPE);
if (mRequestNewAccount) {
mAccountManager.addAccountExplicitly(account, mPassword, null);
// Extension point. Here we will set up the auto sync for different
// services
// Example: ContentResolver.setSyncAutomatically(account,
// ContactsContract.AUTHORITY, true);
} else {
mAccountManager.setPassword(account, mPassword);
}
final Intent intent = new Intent();
intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, mUsername);
intent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, ACCOUNT_TYPE);
if (token != null && token.equals(AUTHTOKEN_TYPE)) {
intent.putExtra(AccountManager.KEY_AUTHTOKEN, token);
}
setAccountAuthenticatorResult(intent.getExtras());
setResult(RESULT_OK, intent);
finish();
}
There is still one step remaining. Android does not call our implementation of the AbstractAccountAuthenticator directly. It must be wrapped in a service that returns a subclass of AbstractAccountAuthenticator from its onBind method.
public class AuthenticationService extends Service {
private static final String TAG = "AuthenticationService";
private Authenticator mAuthenticator;
@Override
public void onCreate() {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Authentication Service started.");
}
mAuthenticator = new Authenticator(this);
}
@Override
public void onDestroy() {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Authentication Service stopped.");
}
}
@Override
public IBinder onBind(Intent intent) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "getBinder() ... returning AccountAuthenticator binder");
}
return mAuthenticator.getIBinder();
}
}
Finally, register the service in the AndroidManifest.xml file and filter the action named android.accounts.AccountAuthenticator. The permissions required for account management are GET_ACCOUNTS, USE_CREDENTIALS, MANAGE_ACCOUNTS and AUTHENTICATE_ACCOUNTS.
Writing the SyncAdapter
First of all throw a file in the res/xml folder that describes the SyncAdapter and what kind of content it is going to synchronize.
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:contentAuthority="com.nasatrainedmonkeys.app.providers.SomeContentProvider"
android:accountType="com.nasatrainedmonkeys.account" />
In order to write a SyncAdapter you have to extend the AbstractThreadedSyncAdapter class and override its onPerformSync method. This method will be run in a newly spawned thread when no other sync operation is running. One of the parameters is an instance of the SyncResult class which is used to inform the SyncManager about any errors in the sync process. With this result the SyncManager can decide whether to reschedule the sync in the future or not.
public class SampleSyncAdapter extends AbstractThreadedSyncAdapter {
private AccountManager mAccountManager;
private ContentResolver mContentResolver;
public SampleSyncAdapter(Context context, boolean autoInitialize) {
super(context, autoInitialize);
mAccountManager = AccountManager.get(context);
mContentResolver = context.getContentResolver();
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority,
ContentProviderClient provider, SyncResult syncResult) {
String authtoken = null;
try {
authtoken = mAccountManager.blockingGetAuthToken(account,
AuthenticatorActivity.PARAM_AUTHTOKEN_TYPE, true);
// Dummy sample. Do whatever you want in this method.
List data = fetchData(authtoken);
syncRemoteDeleted(data);
syncFromServerToLocalStorage(data);
syncDirtyToServer(authtoken, getDirtyList(mContentResolver));
} catch (Exception e) {
handleException(authtoken, e, syncResult);
}
}
private void handleException(String authtoken, Exception e,
SyncResult syncResult) {
if (e instanceof AuthenticatorException) {
syncResult.stats.numParseExceptions++;
Log.e(TAG, "AuthenticatorException", e);
} else if (e instanceof OperationCanceledException) {
Log.e(TAG, "OperationCanceledExcepion", e);
} else if (e instanceof IOException) {
Log.e(TAG, "IOException", e);
syncResult.stats.numIoExceptions++;
} else if (e instanceof AuthenticationException) {
mAccountManager.invalidateAuthToken(
AuthenticatorActivity.PARAM_ACCOUNT_TYPE, authtoken);
// The numAuthExceptions require user intervention and are
// considered hard errors.
// We automatically get a new hash, so let's make SyncManager retry
// automatically.
syncResult.stats.numIoExceptions++;
Log.e(TAG, "AuthenticationException", e);
} else if (e instanceof ParseException) {
syncResult.stats.numParseExceptions++;
Log.e(TAG, "ParseException", e);
} else if (e instanceof JsonParseException) {
syncResult.stats.numParseExceptions++;
Log.e(TAG, "JSONException", e);
}
}
// ...
}
The only missing step is to return the sync adapter from the onBind method of a wrapping service and register the service in the AndroidManifest.xml to filter the intents with action android.content.SyncAdapter.
public class SampleSyncService extends Service {Conclusion
private static final Object sSyncAdapterLock = new Object();
private static SampleSyncAdapter sSyncAdapter = null;
@Override
public void onCreate() {
synchronized (sSyncAdapterLock) {
if (sSyncAdapter == null) {
sSyncAdapter = new SampleSyncAdapter(getApplicationContext(), true);
}
}
}
@Override
public IBinder onBind(Intent intent) {
return sSyncAdapter.getSyncAdapterBinder();
}
}
Use SyncAdapters. They take away the pain of manually managing alarms and awkward synchronization retries. Let the system be responsible for choosing the best time to sync your data and play nicely with the rest of the applications.
References
- http://www.c99.org/2010/01/23/writing-an-android-sync-provider-part-2/
- http://ericmiles.wordpress.com/2010/09/22/connecting-the-dots-with-android-syncadapter/
- Android official SyncAdapter sample code: http://developer.android.com/resources/samples/SampleSyncAdapter/index.html
And of course StackOverflow and the great responses of jcwenger on SyncAdapters.