Page authors

  • Vladimir Vasiltsov
    November 15, 2011

Concept of SyncAdapter (android.content.AbstractThreadedSyncAdapter)

Google I/O 2010 - Android REST client applications

Implementing SyncAdapter might get a little bit challenging, as soon as it is not that much examples and documentation explaining how concept works. Even when it is recommended for use by this guy he explains a little.

Still he inspired me to use that concept in my current project and I decided to find out how it work and implement one.

First of all I was not able to find any graphical schema to visualize what is involved in the process and how do the entities interact to each other. That does not seem very unusual for android platform and to all java world. javadoc should be enough, right?

Well I decided that it is not after I read few articles I found about it. You can read them for yourself if you wish. Here they are:

Those articles more like an example which is good and helped me a lot, still you wont get the whole picture of the concept, and few things are missing here and there.








The BIG picture.

On the right side you can click the diagram which shows what is involved in the process how do those 'whats' interact to each other and what do you have to implement to be able to utilize SyncAdapter in your application.

First of all everything starts with Account. Your application must use some account to interact to an external souce of information. You can use built-in accounts like one provided by google or third party like one facebook use, or you can create your own.

To create your own account you must implement 3 entities:
  1. AuthenticatorService
  2. Authenticator
  3. and AccountAuthenticatorActivity which does all the interaction to a user.
You have to implement all 3 at once and make sure they don't throw exceptions to the outer world or you might find that your device reboots. Anyway I'd recommend use emulator when you create SyncAdapter and as soon as google's emulator is slow as hell you can try use this one: http://code.google.com/p/android-x86/ where you can use VirtualBox and eeepc ISO-images.

Here is what we have to do in general
  1. Create an account (if we use a custom one)
  2. Create sync adapter
  3. Create DataProvider
  4. Connect that decoupled system to work as a one piece.

Create Account

AuthenticatorService


The main purpose for AuthenticatorService to create Authenticator and that's what it does in onCreate. 

    public void onCreate() {
        if(mAuthenticator == null){
                mAuthenticator = new Authenticator(this);
        }
    }
The next big thing is to return the binder, which is

    @Override
    public IBinder onBind(Intent intent) {
        return mAuthenticator.getIBinder();
    }

Now make sure it is registered in your AndroidManifest.xml and marked as exported

/manifest/application
    <service android:name="AccountAuthenticatorService" android:exported="true">
    </service>

Authenticator


Authenticator used to work with account. Create, update, verify etc. also it is used to allow a user to get involved in the process so it will instantiate your custom activity displayed to somebody creating an account of your custom type. 

@Override
public Bundle addAccount(AccountAuthenticatorResponse response, 
                                 String accountType, 
                                 String authTokenType, 
                                 String[] requiredFeatures, 
                                 Bundle options) throws NetworkErrorException {

    final Intent intent = new Intent(mContext, MyLoginActivity.class);
    intent.putExtra(MyLoginActivity.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;
}
What we do here is simply preparing intent to start MyLoginActivity (which is implementation of AccountAuthenticatorActivity). Some external activity will execute it. It might be one from Settings or Dev Tools, or even our own.

addAccount is a must, others are not the very minimum to get everything started and we still have Abstract class to be implemented so we can just share the implementation of addAccount for the rest excluding hasFeatures. Well we do not do any fancy features here so we just reject everything. 

@Override
public Bundle hasFeatures(AccountAuthenticatorResponse arg0, 
                              Account arg1,
                                    String[] arg2) throws NetworkErrorException {

        final Bundle result = new Bundle();
        result.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);

        return result;
}


Register our authenticator in manifest:

/manifest/application/service[@android:name='AccountAuthenticatorService']

    <intent-filter>
        <action android:name="android.accounts.AccountAuthenticator"></action>
    </intent-filter>
    <meta-data android:name="android.accounts.AccountAuthenticator" android:resource="@xml/my_authenticator" />

res/xml/my_authenticator
    <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
        android:accountType="com.my.custom.account"
        android:icon="@drawable/icon"
        android:smallIcon="@drawable/icon"
        android:label="@string/app_name"
    />

AccountAuthenticatorActivity


Now we have the back-end ready, but we still have to let the user provide us credentials. So we instantiated MyLoginActivity earlier and it was not implemented yet. Well it should extend android.accounts.AccountAuthenticatorActivity
besides that it is a regular activity you implement it as usual just make sure it has some way to create/update/verify account upon request. In the very basic situation it needs only an ability to create account, well we use android.accounts.AccountManager and android.accounts.Account to do so. 

    Account newAccount = new Account(userName, "com.my.custom.account");
    AccountManager accountManager = AccountManager.get(this);
    accountManager.addAccountExplicitly(newAccount,password, null);

Make sure we return intent with appropriate extras, to the calling code.

    Intent i = new Intent();
    i.putExtra(AccountManager.KEY_ACCOUNT_NAME, userName);
    i.putExtra(AccountManager.KEY_ACCOUNT_TYPE, "com.my.custom.account");
    i.putExtra(AccountManager.KEY_AUTHTOKEN,"com.my.custom.account");
    i.putExtra(AccountManager.KEY_PASSWORD, password);
    this.setAccountAuthenticatorResult(i.getExtras());
    this.setResult(RESULT_OK, i);

Register MyLoginActivity in manifest and declare it exported

    <activity android:name=".MyLoginActivity" android:excludeFromRecents="true" android:exported="true"/>

Use Dev Tools to make sure it works, you should be able to add an account of your custom type (the account name is @string/app_name). DevTools -> Account tester is the tool you need.



Create SyncAdapter


SyncService


Sync service needed to create an instance of SyncAdapter and to host it returning Binder upon request. So it is pretty standard

    public class MySyncService extends android.app.Service

implement onCreate 

    if (syncAdapter == null) {
      syncAdapter = new SmsSyncAdapter(getApplicationContext(), true);
    }

and onBind

return syncAdapter.getSyncAdapterBinder();

make sure it is in your AndroidManifest.xml and exported

/manifest/application
    <service android:name=".MySyncService" android:exported="true">

SyncAdapter


That is the guy we started everything for. The only method we need here is onPerformSync and implementation is totally up to us. The importaint thing is you recieve account on every call and you can extract saved password from AccountManager if you stored it earlier with .getPassword(account).

    public class MySyncAdapter extends AbstractThreadedSyncAdapter {
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
            // ... your interaction to an external service starts here
        }
    }

make sure sync adapter registered in AndroidManifest.xml

/manifest/application/service[@android:name='.MySyncService']

    <intent-filter>
        <action android:name="android.content.SyncAdapter"></action>
    </intent-filter>
    <meta-data android:resource="@xml/my_syncadapter" android:name="android.content.SyncAdapter"></meta-data>

res/xml/my_syncadapter
    <?xml version="1.0" encoding="utf-8"?>
    <sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
        android:accountType="com.my.custom.account"
        android:contentAuthority="com.android.contacts"
        android:supportsUploading="false" />

Now we have account and sync adapter ready. We connected them with accountType and the only thing missing is our SyncAdapter does not get activated by the system. Well we must tell it to start our syncAdapter. We can start it for a single time, make the system start it for us from time to time and adjust retrials. One good place to assign all the sync settings for sync adapter is MyLoginActivity right where the account created.

setIsSyncable was not mandatory until android 3.1 or 3.2 now it is.

    ContentResolver.setIsSyncable(account,"com.android.contacts",1);

Configure it to start every 150 seconds 

    Bundle params = new Bundle();
    params.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, false);
    params.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, false);
    params.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
    ContentResolver.addPeriodicSync(account, "com.android.contacts", params, 150);
    ContentResolver.setSyncAutomatically(account, "com.android.contacts", true);

And make it start right now

    ContentResolver.requestSync(account,"com.android.contacts",params);

            
Are we done yet? Nope.


DataProvider

We have multi-threading in our application right when we start using SyncAdapter, so we have to provide thread safe way to store the data SyncAdapter extracts from an external service and we also must let the UI to know when the data is updated and provide it with a way to safely retrieve the data from an internal storage.

Internal abstract storage


    public class MyDataProvider extends ContentProvider

implement methods delete, getType, insert, onCreate, query and update. In a very basic case you might want them to return nulls and zeroes and do nothing, and you still need a DataProvider or nothing will work, better if you implement them for your own data types.

change the AndroidManifest.xml

    <provider android:syncable="true" android:label="MyDataProvider" android:name="MyDataProvider" android:authorities="com.my.custom"></provider>

Notify UI when the data get changed


Ok. Now our SyncAdapter recieved a portion of the data we stored in in the DataProvider with set of insert/updates let's notify UI to re-read the data and rebind it to the UI controls
    
    context.getContentResolver().notifyChange()

on the UI side subscribe for the notification with 

    getContentResolver().registerContentObserver()

Make sure Uri for resource in notifyChange and registerContentObserver are the same also SyncCompleteContentObserver should be executed in UI thread. Please keep that in mind.


That is it


Now we have an Account with SyncAdapter which gets activated by the system. The SyncAdapter gets the data from an external service and publishes it to the DataProvider, which notifies everybody interested about the changes, and the interested in changes part is our UI in application. As soon as our application's Activity get change notification it forces the UI to get refreshed and display new data.

Č
ą
Vladimir Vasiltsov,
Nov 15, 2011, 9:12 AM
ą
Vladimir Vasiltsov,
Nov 15, 2011, 8:32 AM
ą
Vladimir Vasiltsov,
Nov 15, 2011, 8:34 AM
Comments