There are numerous of ways that we periodically can sync local data with a backend server, without opening up the app. Some ways are better than others, but today id like to take some time to talk about Androids preferred method of doing this. Using the sync adapter framework.
The implementation it self can be frightening, and lengthy, making developers look for other ways to do the syncing instead. While people would also suggest using the Android AlarmManager, and though it may work, it is not optimized for this kind of operation.
As Android developers, we must understand that the Android OS has very limited resources, and as mentioned in the google docs
A poorly designed alarm can cause battery drain and put a significant load on servers.
Before we move on i need to mention that, a sync adapter should only be used for operations that will most likely occur when the app is NOT open or when the device is sleeping. Otherwise, feel free to use any other method out there, such as using RxJava, the Alarm Manager, a Handler with a timer and thread, IntentService, or even an Async Task….
Alright i was kidding about the async task part.
So what are sync adapters anyways?
For those of you who have never heard of the Sync Adapters, a sync adapter is a method to sync your local mobile data, with a backend server, while taking advantage of many out of the box features that Android gives us by using the sync adapter framework.
Some advantages are:
- Automated execution based on a variety of criteria, such as data changes, elapsed time, or time of day.
- If for some reason the sync cannot occur at the specified time, it gets added to a queue, and runs as soon as possible
- The sync adapter gives us automatically network detection! This means we do not need to keep on checking for network every single time the sync happens, which means, when network actually IS available, the operation will automatically run. Come on, this is great!
- Sync adapters also give us improved battery performance, by allowing us to centralize all of the apps data transfer tasks in one place, where they can all run synchronously. The data is automatically scheduled in conjunction with other apps in the device. This saves battery by reducing the time the system needs to keep on switching the network back on to perform a task.
- If your app requires user credentials or server login, you can optionally integrate account management and authentication into your data transfer.
Alright then, now that we are all exited about this, lets see some code!
So remember when i said that “The implementation it self can be frightening, and lengthy, making developers look for other ways to do the syncing instead.”? well, if you don’t, scroll up and read it again, pay attention this time, if you do, then well…this is the beginning of it.
In order to create a sync adapter we need to first start with an Authenticator.
The sync adapter framework assumes that your sync adapter transfers data between device storage associated with an account and server storage that requires login access. For this reason, the framework expects you to provide a component called an authenticator as part of your sync adapter. This component plugs into the Android accounts and authentication framework and provides a standard interface for handling user credentials such as login information.
For this tutorial, lets assume that we do not require any authentication. This is what a stubbed out authenticator would look like (purposely added the JavaDocs to understand what each one of these methods do.)
public class Authenticator extends AbstractAccountAuthenticator { public Authenticator(final Context context) { super(context); } /** * Returns a Bundle that contains the Intent of the activity that can be used to edit the * properties. In order to indicate success the activity should call response.setResult() with a * non-null Bundle. * * @param response used to set the result for the request. If the Constants.INTENT_KEY is set * in the bundle then this response field is to be used for sending future * results if and when the Intent is started. * @param accountType the AccountType whose properties are to be edited. * @return a Bundle containing the result or the Intent to start to continue the request. If * this is null then the request is considered to still be active and the result should sent * later using response. */ @Override public Bundle editProperties(final AccountAuthenticatorResponse response, final String accountType) { throw new UnsupportedOperationException(); } /** * Adds an account of the specified accountType. * * @param response to send the result back to the AccountManager, will never be null * @param accountType the type of account to add, will never be null * @param authTokenType the type of auth token to retrieve after adding the account, may be * null * @param requiredFeatures a String array of authenticator-specific features that the added * account must support, may be null * @param options a Bundle of authenticator-specific options, may be null * @return a Bundle result or null if the result is to be returned via the response. The result * will contain either: <ul> <li> {@link AccountManager#KEY_INTENT}, or <li> {@link * AccountManager#KEY_ACCOUNT_NAME} and {@link AccountManager#KEY_ACCOUNT_TYPE} of the account * that was added, or <li> {@link AccountManager#KEY_ERROR_CODE} and {@link * AccountManager#KEY_ERROR_MESSAGE} to indicate an error </ul> * @throws NetworkErrorException if the authenticator could not honor the request due to a * network error */ @Override public Bundle addAccount(final AccountAuthenticatorResponse response, final String accountType, final String authTokenType, final String[] requiredFeatures, final Bundle options) throws NetworkErrorException { return null; } /** * Checks that the user knows the credentials of an account. * * @param response to send the result back to the AccountManager, will never be null * @param account the account whose credentials are to be checked, will never be null * @param options a Bundle of authenticator-specific options, may be null * @return a Bundle result or null if the result is to be returned via the response. The result * will contain either: <ul> <li> {@link AccountManager#KEY_INTENT}, or <li> {@link * AccountManager#KEY_BOOLEAN_RESULT}, true if the check succeeded, false otherwise <li> {@link * AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to indicate an * error </ul> * @throws NetworkErrorException if the authenticator could not honor the request due to a * network error */ @Override public Bundle confirmCredentials(final AccountAuthenticatorResponse response, final Account account, final Bundle options) throws NetworkErrorException { return null; } /** * Gets an authtoken for an account. * <p> * If not {@code null}, the resultant {@link Bundle} will contain different sets of keys * depending on whether a token was successfully issued and, if not, whether one could be issued * via some {@link Activity}. * <p> * If a token cannot be provided without some additional activity, the Bundle should contain * {@link AccountManager#KEY_INTENT} with an associated {@link Intent}. On the other hand, if * there is no such activity, then a Bundle containing {@link AccountManager#KEY_ERROR_CODE} and * {@link AccountManager#KEY_ERROR_MESSAGE} should be returned. * <p> * If a token can be successfully issued, the implementation should return the {@link * AccountManager#KEY_ACCOUNT_NAME} and {@link AccountManager#KEY_ACCOUNT_TYPE} of the account * associated with the token as well as the {@link AccountManager#KEY_AUTHTOKEN}. In addition * {@link AbstractAccountAuthenticator} implementations that declare themselves {@code * android:customTokens=true} may also provide a non-negative {@link #KEY_CUSTOM_TOKEN_EXPIRY} * long value containing the expiration timestamp of the expiration time (in millis since the * unix epoch). * <p> * Implementers should assume that tokens will be cached on the basis of account and * authTokenType. The system may ignore the contents of the supplied options Bundle when * determining to re-use a cached token. Furthermore, implementers should assume a supplied * expiration time will be treated as non-binding advice. * <p> * Finally, note that for android:customTokens=false authenticators, tokens are cached * indefinitely until some client calls {@link AccountManager#invalidateAuthToken(String, * String)}. * * @param response to send the result back to the AccountManager, will never be null * @param account the account whose credentials are to be retrieved, will never be null * @param authTokenType the type of auth token to retrieve, will never be null * @param options a Bundle of authenticator-specific options, may be null * @return a Bundle result or null if the result is to be returned via the response. * @throws NetworkErrorException if the authenticator could not honor the request due to a * network error */ @Override public Bundle getAuthToken(final AccountAuthenticatorResponse response, final Account account, final String authTokenType, final Bundle options) throws NetworkErrorException { throw new UnsupportedOperationException(); } /** * Ask the authenticator for a localized label for the given authTokenType. * * @param authTokenType the authTokenType whose label is to be returned, will never be null * @return the localized label of the auth token type, may be null if the type isn't known */ @Override public String getAuthTokenLabel(final String authTokenType) { throw new UnsupportedOperationException(); } /** * Update the locally stored credentials for an account. * * @param response to send the result back to the AccountManager, will never be null * @param account the account whose credentials are to be updated, will never be null * @param authTokenType the type of auth token to retrieve after updating the credentials, may * be null * @param options a Bundle of authenticator-specific options, may be null * @return a Bundle result or null if the result is to be returned via the response. The result * will contain either: <ul> <li> {@link AccountManager#KEY_INTENT}, or <li> {@link * AccountManager#KEY_ACCOUNT_NAME} and {@link AccountManager#KEY_ACCOUNT_TYPE} of the account * whose credentials were updated, or <li> {@link AccountManager#KEY_ERROR_CODE} and {@link * AccountManager#KEY_ERROR_MESSAGE} to indicate an error </ul> * @throws NetworkErrorException if the authenticator could not honor the request due to a * network error */ @Override public Bundle updateCredentials(final AccountAuthenticatorResponse response, final Account account, final String authTokenType, final Bundle options) throws NetworkErrorException { throw new UnsupportedOperationException(); } /** * Checks if the account supports all the specified authenticator specific features. * * @param response to send the result back to the AccountManager, will never be null * @param account the account to check, will never be null * @param features an array of features to check, will never be null * @return a Bundle result or null if the result is to be returned via the response. The result * will contain either: <ul> <li> {@link AccountManager#KEY_INTENT}, or <li> {@link * AccountManager#KEY_BOOLEAN_RESULT}, true if the account has all the features, false otherwise * <li> {@link AccountManager#KEY_ERROR_CODE} and {@link AccountManager#KEY_ERROR_MESSAGE} to * indicate an error </ul> * @throws NetworkErrorException if the authenticator could not honor the request due to a * network error */ @Override public Bundle hasFeatures(final AccountAuthenticatorResponse response, final Account account, final String[] features) throws NetworkErrorException { throw new UnsupportedOperationException(); } }
Remember, this is a stub, no need to actually add any logic/implementation in any of these. Though, they do seem pretty handy.
Now that we have our authenticator, we need to bind it to a service, so that later on our sync adapter can access the authenticator and do authenticator stuff. This allows an Android binder object to call the authenticator and pass in data between the authenticator and the sync adapter framework.
Since the framework starts this
Service the first time it needs to access the authenticator, you can also use the service to instantiate the authenticator, by calling the authenticator constructor in the
Service.onCreate() method of the service.
Heres what the service looks like
public class AuthenticatorService extends Service { private Authenticator authenticator; @Override public void onCreate() { authenticator = new Authenticator(this); } @Override public IBinder onBind(Intent intent) {
return authenticator.getIBinder();
} }
Note the IBinder method.
Now that we have our authenticator all set, stubbed out, and ready we can add all the metadata that comes along with it. So inside our res/xml directory, we create a new file called authenticator.xml
this file looks something like this
<?xml version="1.0" encoding="utf-8"?> <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType=“example.com”
android:icon="@drawable/ic_launcher" android:smallIcon="@drawable/ic_launcher"
android:label=“@string/app_name”
/>
To figure out what each of these fields are you can go HERE but the 2 important fields are accountType, and label. These 2 fields can be any text, just need to make sure that accountType is in the form of a url as you can see. Also, the url doesnt really have to be a valid one.
Next declare the Authenticator in the manifest.
<service android:name=".AuthenticatorService"> <intent-filter> <action android:name="android.accounts.AccountAuthenticator" /> </intent-filter> <meta-data android:name="android.accounts.AccountAuthenticator" android:resource="@xml/authenticator" /> </service>
The
<intent-filter> element sets up a filter that’s triggered by the intent action
android.accounts.AccountAuthenticator
, which sent by the system to run the authenticator. When the filter is triggered, the system startsAuthenticatorService
, the boundService you have provided to wrap the authenticator.
The
<meta-data> element declares the metadata for the authenticator. The
android:name attribute links the meta-data to the authentication framework. The
android:resource element specifies the name of the authenticator metadata file you created previously.
Another thing that the Sync Adapter requires is a Content Provider even if you do not need/use one.
Well then…i guess we need a content provider.
public class StubContentProvider extends ContentProvider { @Override public boolean onCreate() { return true; } @Nullable @Override public Cursor query(@NonNull final Uri uri, final String[] projection, final String selection, final String[] selectionArgs, final String sortOrder) { return null; } @Nullable @Override public String getType(@NonNull final Uri uri) { return null; } @Nullable @Override public Uri insert(@NonNull final Uri uri, final ContentValues values) { return null; } @Override public int delete(@NonNull final Uri uri, final String selection, final String[] selectionArgs) { return 0; } @Override public int update(@NonNull final Uri uri, final ContentValues values, final String selection, final String[] selectionArgs) { return 0; } }
Thats one hell of a stub there.
Just like any other content provider, we must declare it in the manifest as well. Inside the application tag.
<provider android:name="com.example.android.datasync.provider.StubProvider" android:authorities="com.example.android.datasync.provider" android:exported="false" android:syncable="true"/>
Note the syncable flag. This indicates that the provider allows the sync adapter to make data transfers with it, but only if explicitly done so.
Now, off to the good stuff. The Sync Adapter it self!
The adapter itself is really simple. Like really really simple. Check this out.
class ConfigurationSyncAdapter
extends AbstractThreadedSyncAdapter
{ private static final String TAG = ConfigurationSyncAdapter.class.getSimpleName(); private final SomeManagerIUseToDoStuff manager; ConfigurationSyncAdapter(final Context context, final boolean autoInitialize) { super(context, autoInitialize); manager = new SomeManagerIUseToDoStuff(context); } ConfigurationSyncAdapter(final Context context, final boolean autoInitialize, final boolean allowParallelSyncs) { super(context, autoInitialize, allowParallelSyncs); manager = new ApolloBackendConfigurationManager(context); } /** * Perform a sync for this account. SyncAdapter-specific parameters may be specified in extras, * which is guaranteed to not be null. Invocations of this method are guaranteed to be * serialized. * * @param account the account that should be synced * @param extras SyncAdapter-specific parameters * @param authority the authority of this sync request * @param provider a ContentProviderClient that points to the ContentProvider for this * authority * @param syncResult SyncAdapter-specific parameters */ @Override public void onPerformSync(final Account account, final Bundle extras, final String authority, final ContentProviderClient provider, final SyncResult syncResult) { Log.i(TAG, "onPerformSync() was called"); /* This is where you would put any code you want to run in the background. Such as fetching data from a server! */ manager.fetchDataFromServer(); } }
Your sync adapter must implement the AbstractThreadedSyncAdapter, the actual background operations happen inside the onPerformSync method. This method gets called automatically when the syncing is supposed to occur. If you haven’t figured this out by now, the entire sync adapter runs on a background thread, so there is no need to add additional background processing in here.
The sync adapter does NOT automatically do the data transfers for you. What it does is that it encapsulates your data transfer code, so that the sync adapter can then run the data transfer in the background without any involvement from your application.
Now that we have a way to handle the background operation, we need to give the sync adapter access to our code/information. We do this by creating another service that passes a special Android binder object from the sync adapter to the framework.
The same way we have bound the previous components to the framework, we need to do the same with the sync adapter.
public class ConfigurationSyncAdapterService extends Service { private static ConfigurationSyncAdapter syncAdapter = null; private static final Object syncAdapterLock = new Object(); @Override public void onCreate() { super.onCreate(); /* * Create the sync adapter as a singleton. * Set the sync adapter as syncable * Disallow parallel syncs */ synchronized (syncAdapterLock) { if (syncAdapter == null) { syncAdapter = new ConfigurationSyncAdapter(getApplicationContext(), true); } } } /** * Return an object that allows the system to invoke * the sync adapter. * */ @Override public IBinder onBind(final Intent intent) { /* * Get the object that allows external processes * to call onPerformSync(). The object is created * in the base class code when the SyncAdapter * constructors call super() */ return syncAdapter.getSyncAdapterBinder(); } }
The code is pretty straight forward and self explanatory. The ConfigurationSyncAdapter object is the sync adapter we created previously. The syncAdapterLock is used for thread-safe locking, and the IBinder returns the sync adapters binder to do the actual framework binding mentioned above.
Next, we need to add another file to our res/xml directory called syncadapter.xml
Inside this file we add the following
<?xml version="1.0" encoding="utf-8"?> <sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType=”@string/account_type”
android:allowParallelSyncs="false"
android:contentAuthority=”@string/authority”
android:isAlwaysSyncable="true" android:supportsUploading="false" android:userVisible="false" />
To know what each of these fields do please take a look at THIS link, though i feel like they are self explanatory.
NOTE: The accountType inside the syncadapter.xml file MUST be the same account type provided in the authenticator.xml file. For this reason i moved the account type to the strings file for easy access whenever needed. Also note the contentAuthority, this will come in handy for later use when using the sync adapter. Put it in the strings file for later use. The contentAuthority can be any string as well. In my case i have it as com.packagename.authority.
Next we need to declare the sync adapter in the manifest of course, along with a could of permissions.
<uses-permission android:name=”android.permission.INTERNET” />
<uses-permission android:name="android.permission.CALL_PHONE" /> <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.READ_SMS" /> <uses-permission android:name="android.permission.SEND_SMS" /> <uses-permission android:name="android.permission.RECEIVE_SMS" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<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” />
<application android:allowBackup="false" android:label="@string/app_name" android:supportsRtl="true"> <service android:name=".AuthenticatorService"> <intent-filter> <action android:name="android.accounts.AccountAuthenticator" /> </intent-filter> <meta-data android:name="android.accounts.AccountAuthenticator" android:resource="@xml/authenticator" /> </service> <provider android:name=".StubContentProvider" android:authorities="@string/authority" android:exported="false" android:syncable="true" />
<service android:name=”.ConfigurationSyncAdapterService” android:exported=”true” android:process=”:sync”> <intent-filter> <action android:name=”android.content.SyncAdapter” /> </intent-filter> <meta-data android:name=”android.content.SyncAdapter” android:resource=”@xml/syncadapter” /> </service>
</application>
The bold text is what we just added.
Note: The attribute
android:process=”:sync” tells the system to run the
Service in a global shared process named sync
. If you have multiple sync adapters in your app they can share this process, which reduces overhead.
And thats it! We are now done with our setup. Now the moment we’ve all been waiting for….running the sync adapter!
You can find additional information about different methods to use for running the sync adapter HERE but i will show you how to run the adapter periodically, and on demand. Keep in mind that you can automatically run a sync adapter when When server data changes, when device data changes, at regular intervals, or on demand.
So for my personal need, in my latest project, i did this by creating a SyncAdapterManager which handles all of the syncing operation for me. Inside my manager, i have a beginPeriodicSync method that runs the sync adapter periodically, when ever i tell it to do so
@SuppressWarnings ("MissingPermission") void beginPeriodicSync(final long updateConfigInterval) { Log.d(TAG, "beginPeriodicSync() called with: updateConfigInterval = [" + updateConfigInterval + "]"); final AccountManager accountManager = (AccountManager) context .getSystemService(ACCOUNT_SERVICE); if (!accountManager.addAccountExplicitly(account, null, null)) { account = accountManager.getAccountsByType(type)[0]; } setAccountSyncable(); ContentResolver.addPeriodicSync(account, context.getString(R.string.authority), Bundle.EMPTY, updateConfigInterval); ContentResolver.setSyncAutomatically(account, authority, true); }
Here is the setAccountSyncable method
private void setAccountSyncable() { if (ContentResolver.getIsSyncable(account, authority) == 0) { ContentResolver.setIsSyncable(account, authority, 1); } }
I also have a syncImmediately method as well which can be used to respond to a button click or something
void syncImmediately() { Bundle settingsBundle = new Bundle(); settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); ContentResolver.requestSync(account, authority, settingsBundle); }
SYNC_EXTRAS_MANUAL
Forces a manual sync. The sync adapter framework ignores the existing settings, such as the flag set by
setSyncAutomatically().
SYNC_EXTRAS_EXPEDITED
Forces the sync to start immediately. If you don’t set this, the system may wait several seconds before running the sync request, because it tries to optimize battery use by scheduling many requests in a short period of time.
To give you a better idea of the full picture, here is my SyncAdapterManager’s full code.
class SyncAdapterManager { private static final String TAG = SyncAdapterManager.class.getSimpleName(); private final String authority; private final String type; private Account account; private Context context; SyncAdapterManager(final Context context) { this.context = context; type = context.getString(R.string.account_type); authority = context.getString(R.string.authority); account = new Account(context.getString(R.string.app_name), type); } @SuppressWarnings ("MissingPermission") void beginPeriodicSync(final long updateConfigInterval) { Log.d(TAG, "beginPeriodicSync() called with: updateConfigInterval = [" + updateConfigInterval + "]"); final AccountManager accountManager = (AccountManager) context .getSystemService(ACCOUNT_SERVICE); if (!accountManager.addAccountExplicitly(account, null, null)) { account = accountManager.getAccountsByType(type)[0]; } setAccountSyncable(); ContentResolver.addPeriodicSync(account, context.getString(R.string.authority), Bundle.EMPTY, updateConfigInterval); ContentResolver.setSyncAutomatically(account, authority, true); } void syncImmediately() { Bundle settingsBundle = new Bundle(); settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); ContentResolver.requestSync(account, authority, settingsBundle); } private void setAccountSyncable() { if (ContentResolver.getIsSyncable(account, authority) == 0) { ContentResolver.setIsSyncable(account, authority, 1); } } }
Notice the type, and authority? These are the strings that we used before for accountType, and contentAuthority inside of our authenticator.xml and syncadapter.xml files.
The Account object takes a name and a type. The name should be the same name as specified in our authenticator’s label tag, and the type is the accountType.
Once you run these methods, the Sync adapters onPerformSync method will run, and off you go!
NOTE: When debugging this, if you have any logs inside of your SyncAdapter class make sure that you remove any log filters from Android Studio’s Android monitor to see the logs of your Sync Adapter. If you don’t, you will not see your logs and think your adapter is not running.
The reason for this is that Sync Adapter runs on a Bound Service which is not in the same process, so your logs don’t show in the LogCat under your app main process but in the process that the Sync Adapter is using.
You can use break points to help you debug if you don’t want to use logs.
Another thing you can do is add a filter for the application running the sync adapter by clicking the filter drop down and selecting “Edit filter configuration” option.
This brings up a dialog where you can add specific log filters. Just click the “add/plus” button on the upper left hand corner, and add the apps package name in the Package Name field. This will show the the sync adapter’s log that belong to the application.
The full source code can be found in this link.
Thank you for reading. Until next time!
Thank you for this article. There is not too much information on the topic. I use a SyncAdapter myself in one of my projects. Check it out if you are interested. You can find it here: https://bitbucket.org/tbsprs/altglas