Android - Sync Manager Framework (SyncAdapter)

> Android

1 - About

Introduced in Android Level 5. SyncAdapters are meant to keep local data on the device (a cache) in sync with data on the web with the goal of:

  • fast load times,
  • offline functionality
  • and not necessitating a network call every time we load an activity.

The Sync Manager will try to reduce the usage of battery by implementing the big cookie model. The transfer needed for all applications will happen at once. Sync Adapter will maintain a balance between:

  • keeping the local data cache up to date
  • and saving the battery

by being smart about when to place network calls. See Android - Data Transfer.

The Sync Manager:

  • will schedule fewer simultaneous sync by schedules network jobs across all apps.
  • has a logic to retry a request (Checking for network connectivity before initiating transfer. Stop transfer when the connectivity is dropped, …)

3 - Prerequisites

An account is required by the framework.

4 - Implementation

4.1 - SyncAdapter

An SyncAdapter (such as mySyncAdapter) must extends AbstractThreadedSyncAdapter. Note the following methods:

  • onPerformSync - This is the important method which is overridden when you subclass AbstractThreadedSyncAdapter. This is what happens when a sync occurs (network code, …)
public final String LOG_TAG = MySyncAdapter.class.getSimpleName();
 
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
    Log.d(LOG_TAG, "Starting sync");
    // Do your sync...         
}
  • syncImmediately() - An helper method that tells the system to perform a sync immediately.
/**
 * Helper method to have the sync adapter sync immediately
 * @param context The context used to access the account service
 */
public static void syncImmediately(Context context) {
	Bundle bundle = new Bundle();
	bundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
	bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
	ContentResolver.requestSync(getSyncAccount(context),
			context.getString(R.string.content_authority), bundle);
}
  • configurePeriodicSync: Helper method to schedule the sync adapter periodic execution
public static void configurePeriodicSync(Context context, int syncInterval, int flexTime) {
	Account account = getSyncAccount(context);
	String authority = context.getString(R.string.content_authority);
	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
		// we can enable inexact timers in our periodic sync
		SyncRequest request = new SyncRequest.Builder().
				syncPeriodic(syncInterval, flexTime).
				setSyncAdapter(account, authority).
				setExtras(new Bundle()).build();
		ContentResolver.requestSync(request);
	} else {
		ContentResolver.addPeriodicSync(account,
				authority, new Bundle(), syncInterval);
	}
}
  • getSyncAccount() - An helper method to get an account. Whenever you request a sync, you need a sync account to login.
/**
 * Helper method to get the fake account to be used with SyncAdapter, or make a new one
 * if the fake account doesn't exist yet.  If we make a new account, we call the
 * onAccountCreated method so we can initialize things.
 *
 * @param context The context used to access the account service
 * @return a fake account.
 */
public static Account getSyncAccount(Context context) {
	// Get an instance of the Android account manager
	AccountManager accountManager =
			(AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
 
	// Create the account type and default account
	Account newAccount = new Account(
			context.getString(R.string.app_name), context.getString(R.string.sync_account_type));
 
	// If the password doesn't exist, the account doesn't exist
	if ( null == accountManager.getPassword(newAccount) ) {
 
	       /*
	       * Add the account and account type, no password or user data
	       * If successful, return the Account object, otherwise report an error.
	       */
		if (!accountManager.addAccountExplicitly(newAccount, "", null)) {
			return null;
		}
 
		/*
		 * If you don't set android:syncable="true" in
		 * in your <provider> element in the manifest,
		 * then call ContentResolver.setIsSyncable(account, AUTHORITY, 1)
		 * here.
		 */
 
                /* For a periodic synchronization */
		onAccountCreated(newAccount, context);
	}
	return newAccount;
}

Example:

Advertising

4.2 - Authentication

Whenever you request a sync, you need a sync account

Example:

4.3 - Service

Services provides framework access to the syncAdapter (mySyncAdapter) and the authenticator (myAuthenticator)

Example:

4.4 - Resources

4.4.1 - XML folder

Resources to the res/xml folder

  • authenticator.xml - This resource file plugs an authenticator into the sync adapter and account frameworks by providing it with some of the meta data it needs.
<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
    android:accountType="@string/sync_account_type"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:smallIcon="@mipmap/ic_launcher" />
  • syncadapter.xml - This resource file defines the settings associated with the SyncAdapter.
<?xml version="1.0" encoding="utf-8"?>
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
    android:contentAuthority="@string/content_authority"
    android:accountType="@string/sync_account_type"
    android:userVisible="false"
    android:supportsUploading="false"
    android:allowParallelSyncs="false"
    android:isAlwaysSyncable="true" />
<!-- Account type (for sync-adapter authenticator) -->
<!-- The string sync_account_type suggests that the account is specific to the app myApp. -->
<string name="sync_account_type">myApp.example.com</string>
<!-- The content_authority string is the Uri authority for your content provider. -->
<!-- Make sure all references to content_authority match the string you define here. -->
<string name="content_authority">com.example.android.myApp.app</string>
Advertising

4.4.2 - Manifest

In the AndroidManifest

  • SyncAdapter's authentication service
<service android:name=".sync.myAppNameAuthenticatorService">
	<intent-filter>
		<action android:name="android.accounts.AccountAuthenticator" />
	</intent-filter>
	<meta-data
		android:name="android.accounts.AccountAuthenticator"
		android:resource="@xml/authenticator" 
	/>
</service>
<provider
	android:authorities="@string/content_authority"
	android:name=".data.myAppProvider"
	android:exported="false"
	android:syncable="true" />
<!-- Permissions required by the sync adapter -->
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS"/>
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS"/>
  • The SyncAdapter service
<service
	android:name=".sync.myAppSyncService"
	android:exported="true"
	>
	<intent-filter>
		<action android:name="android.content.SyncAdapter" />
	</intent-filter>
	<meta-data
		android:name="android.content.SyncAdapter"
		android:resource="@xml/syncadapter" />
</service>

4.5 - Start a sync

4.5.1 - Manually

In your activity, puts the code below that calls an helper method of the syncAdapter. See SyncAdapter for its implementation

private void updateMyData() {
    mySyncAdapter.syncImmediately(getActivity));
}
Advertising

4.5.2 - Periodically

In the syncAdapter, when a new account is created, call this helper function.

// Interval at which to sync, in seconds.
// 60 seconds (1 minute) * 180 = 3 hours
public static final int SYNC_INTERVAL = 60 * 180;
public static final int SYNC_FLEXTIME = SYNC_INTERVAL/3;
 
private static void onAccountCreated(Account newAccount, Context context) {
 
	mySyncAdapter.configurePeriodicSync(context, SYNC_INTERVAL, SYNC_FLEXTIME);
 
	/*
	 * Without calling setSyncAutomatically, our periodic sync will not be enabled.
	 */
	ContentResolver.setSyncAutomatically(newAccount, context.getString(R.string.content_authority), true);
 
	/*
	 * Finally, let's do a sync to get things started
	 */
	syncImmediately(context);
 
}
  • Initialize the periodic sync with an helper method in the syncAdapter by a call to the getSyncAccount
public static void initializeSyncAdapter(Context context) {
	getSyncAccount(context);
}

and add a call to this function in the main activity at the end of the onCreate method.

@Override
protected void onCreate(Bundle savedInstanceState) {
 
	super.onCreate(savedInstanceState);
 
	//....
 
	mySyncAdapter.initializeSyncAdapter(this);
 
}

The code flow:

  • Your MainActivity is created and the sync adapter is initialized.
  • During initialization, getSyncAccount is called.
  • getSyncAccount will create a new account if no account exists. If this is the case, onAccountCreated will be called.
  • onAccountCreated configures the periodic sync and calls for an immediate sync.

At this point, the app will sync every 3 hours (if the build version is less than KitKat) or everyone 1 hour (if the build version is greater than or equal to KitKat)

4.6 - Test

A new account must be created on the phone even if it's a dummy.

Below is the sunshine account from the sunshine app

5 - Documentation / Reference