In my last post, I showed how to implement an Android Account Manager, which utilizes the internal Android’s system for managing user accounts. Once you’ve implemented that, you are ready to take your app to the next level with Android Sync Adapter (SyncAdapter
).
What is Android Sync Adapter?
A SyncAdapter
is a plug-in provided by Android to help out with most of the scenarios regarding the syncing abilities of your app. This service is managed by the platform, which is in charge of running it when requested or scheduled.
You can roll out a similar service of your own by writing custom Service
classes and calling them as and when required. But there are many advantages of using the android sync adapter like :-
- Battery Efficiency – The system schedules your syncs as and when it is convenient for the phone, thus optimizing to efficiently use battery, and other scarce resources.
- Error Handling – The Android Sync Adapter knows what screens to show when the server responds with an authentication error, or how to retry with exponential back offs if something goes wrong.
- Disable Background Sync – The user gets to disable sync in background via settings, which is difficult to create in your own implementation. This also plays nice with other apps your users use to save battery/data in their phones.
- Easier Code Management – Everyone will organize their code in their own style, leading to problems of getting productive or understanding code structure. Android Sync Adapter will have a similar interface for most of the projects, thus helping your developers to know where to look for things they need. This also why frameworks work better for teams.
All these advantages come at a price though, you need to learn it! Google has tried hard to make it easier for you to learn by providing a training chapter and a sample implementation (by the name SampleSyncAdapter
), but it is still not simple to understand. There are many unanswered questions about how some properties work, and how does it react under certain circumstances.
Building our Sync Adapter
Making the SyncAdapter class
Our SyncAdapter
is a subclass of AbstractThreadedSyncAdapter
which implements its abstract method onPerformSync
.
public class SyncAdapter extends AbstractThreadedSyncAdapter { private AccountManager mAccountManager; public SyncAdapter(Context context, boolean autoInitialize) { super(context, autoInitialize); mAccountManager = AccountManager.get(context); } public SyncAdapter(Context context, boolean autoInitialize, boolean allowParallelSyncs) { super(context, autoInitialize, allowParallelSyncs); mAccountManager = AccountManager.get(context); } @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { try { String authToken = mAccountManager.blockingGetAuthToken(account, AccountConstants.AUTH_TOKEN_TYPE, true); // Use the authToken and write your sync logic. Skip the previous call if authToken is not required } catch (OperationCanceledException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (AuthenticatorException e) { e.printStackTrace(); } } } |
This is a sample implementation, but it is not enough in itself because you’ll have to register your Sync Adapter with Android’s OS. Also note that the adapter doesn’t care what code it runs, its sole responsibility is to run that code. So you’ll have to write your syncing logic in the little comment I’ve left above.
Sync Adapter’s thread can make synchronous network calls as it runs in its very own background thread. This is why we make a blockingGetAuthToken
call in the onPerformSync
method.
Making the SyncService
The sync adapter also needs a Service to run in. We create this service from within the app so it has access to all the resources that our app has access to while running. The Service is a simple one, where we initialize our adapter when the service starts, and bind it to our adapter.
/** * Define a Service that returns an IBinder for the * sync adapter class, allowing the sync adapter framework to call * onPerformSync(). */ public class SyncService extends Service { // Storage for an instance of the sync adapter private static SyncAdapter sSyncAdapter = null; // Object to use as a thread-safe lock private static final Object sSyncAdapterLock = new Object(); /* * Instantiate the sync adapter object. */ @Override public void onCreate() { /* * Create the sync adapter as a singleton. * Set the sync adapter as syncable * Disallow parallel syncs */ synchronized (sSyncAdapterLock) { if (sSyncAdapter == null) { sSyncAdapter = new SyncAdapter(getApplicationContext(), true); } } } /** * Return an object that allows the system to invoke * the sync adapter. */ @Override public IBinder onBind(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 sSyncAdapter.getSyncAdapterBinder(); } } |
Of course, when you create a service, you need to declare it in the Manifest file.
The manifest file looks something like this :
<service android:name="com.pilanites.streaks.SyncService" 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> |
It looks like your run of the mill service, but there are some important differences.
android:process=":sync"
is a required attribute. This tells the system that this service is a part of the sync process.
The intent-filter
provided let’s the system know when to run this service (like when a specific intent is provided).
Then, in the meta data, you have the name, same as intent-filter and something called android:resource
. This is a vital piece of information which describes our Android Sync Adapter.
Just like how we created a authenticator.xml
file when we used the Android Account Manager, we’ll create a syncadapter.xml
file here.
Creating the syncadapter.xml file
Logically, next we have to create a syncadapter.xml
file.
<?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="true" android:contentAuthority="@string/content_authority" android:isAlwaysSyncable="true" android:supportsUploading="true" android:userVisible="false" /> |
The two things I would like to explain in this small snippet are accountType
and contentAuthority
.
accountType
is the value that you’d have provided in your authenticator.xml
file. This values is used to associate between your account and it’s sync.
contentAuthority
is used to associate with a content provider. I use activeandroid as my database ORM, and it provides me with a content provider out of the box (if you need to create your own content provider, this is a good guide), so I simply declare it in my Manifest file as :-
<provider android:name="com.activeandroid.content.ContentProvider" android:authorities="@string/content_authority" android:exported="false" /> |
As you see, I have the same value for authorities here, as I had in syncadapter.xml
.
(I have used com.pilanites.streaks.provider
as my content authority, but you can use anything, just try not to use something common, in case it clashes.)
Adding relevant permissions
You’ll need to add three permissions to your manifest file to be able to read, write sync status and settings.
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.READ_SYNC_STATS" /> |
Running a Sync
Phew, after going through 1000 words, we should have properly created and configured the Sync mechanism. All that is left is to run it at the right time.
There are multiple ways to run sync, which you can go through in this wonderful guide by udinic. I’ll be focusing on running a sync manually via code.
This means the app will run the sync only when requested by us. Common scenarios will be to run it when a sync button is clicked, or you detect that there were some changes in your local data set and now it needs to be synced.
public void sync() { Account account = UserAccountUtil.getAccount(this); Bundle settingsBundle = new Bundle(); settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true); /* * Request the sync for the default account, authority, and * manual sync settings */ ContentResolver.requestSync(account, getResources().getString(R.string.content_authority), settingsBundle); } |
Just call this sync()
method whenever you need to sync your data, and it’ll run the service and perform the onPerformSync
method. The UserAccountUtil
‘s getAccount
method can be seen implemented in the gist.
And… that’s it!
You can find all the relevant code samples in this gist.
Hopefully this helps in implementing your own Android Sync Adapter. If you liked this guide and want more of these, or have a question, I urge you to leave a comment here if you have any questions or tweet them to me, @shobhitic.
It’ll be great if you can get on my mailing list as well.
Thank you, this tutorial is great.
Hi. As I do if I want to synchronize more than one table. Could you give me an example?
You can do that one by one. In the onPerformSync, you can simply sync all the tables one after the other in a synchronous fashion.
Hi,
Can you please write the code for UserAccountUtil class here?
My bad! Added it to the gist – https://gist.github.com/shobhitic/3eae4ee77d1eaf14296163b03b981ef8#file-useraccountutil-java
How to get the sync disabled action, when user disables from settings > account page?
Call it manually like I’ve shown here, and it’ll work even if it is disabled. Periodic sync will be disabled if user disables from settings.
How to make requests to the Sync Adapter run in the order of a call after connecting to the network? I mean that if u call sync() while device is offline sync adapter put in his “queue” and then after connection perform all requests in random order, which may cause an error on the dependent data. So i need to run them in the order of call.