Writing an Android Sync Provider: Part 2

One of the great new user-facing features of Android 2.0 is the is the new Facebook app, which brings your Facebook contacts and statuses into your Android contacts database:

So, how exactly does my Nexus One know that Chris is excited about the upcoming launch of his new mobile apps? The answer is a Contacts sync provider in the Facebook app. Read on to learn how to create your own!

Sync Providers

Sync providers are services that allow an Account to synchronize data on the device on a regular basis. Not quite sure how to create an Account? Read part one first! To implement a Contacts sync provider, we’ll need a service, some xml files, and the following permissions added to the AndroidManifest.xml:

AndroidManifest.xml snippet

The Service

Similar to our Account Authenticator service, our Contacts Sync Provider service will return a subclass of AbstractThreadedSyncAdapter from the onBind method.

ContactsSyncAdapterService.java

public class ContactsSyncAdapterService extends Service { private static final String TAG = "ContactsSyncAdapterService"; private static SyncAdapterImpl sSyncAdapter = null; private static ContentResolver mContentResolver = null; public ContactsSyncAdapterService() { super(); } private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter { private Context mContext; public SyncAdapterImpl(Context context) { super(context, true); mContext = context; } @Override public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { try { ContactsSyncAdapterService.performSync(mContext, account, extras, authority, provider, syncResult); } catch (OperationCanceledException e) { } } } @Override public IBinder onBind(Intent intent) { IBinder ret = null; ret = getSyncAdapter().getSyncAdapterBinder(); return ret; } private SyncAdapterImpl getSyncAdapter() { if (sSyncAdapter == null) sSyncAdapter = new SyncAdapterImpl(this); return sSyncAdapter; } private static void performSync(Context context, Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) throws OperationCanceledException { mContentResolver = context.getContentResolver(); Log.i(TAG, "performSync: " + account.toString()); //This is where the magic will happen! } }

The service is defined in AndroidManifest.xml like so:

AndroidManifest.xml snippet

Finally, we need an xml file to let Android know that our sync provider handles Contacts for the Account type we defined in part 1.

sync_contacts.xml

At this point we have a sync provider that doesn’t do anything, but also shouldn’t crash the Android system. Just in case, lets test it in Dev Tools first. Start the Android emulator and launch the “Dev Tools” app, then scroll down to “Sync Tester”.

The drop-down should confirm that com.android.contacts can be synced with the account type you’ve created. Select the entry for your account type and press the “Bind” button to connect to your sync service. If all goes well, Sync Tester will say it’s connected to your service. Now click “Start Sync” and select your account from the popup. Sync Tester will let you know that the sync succeeded (even though we didn’t actually do anything). At this point it should be safe to enable contact syncing from the Accounts & Sync settings screen without Android crashing and rebooting.

Contacts

Lets take a moment to discuss how Contacts on Android work. Each sync account, such as Google or Facebook, creates its own set of RawContacts which the Android system then aggregates into the single list of contacts you see in the Dialer. The RawContacts table contains several fields that Sync Providers can use for whatever they like, and in this implementation we will use the SYNC1 field to store the RawContact’s Last.fm username.

Contact Data

Data, such as name, phone number, email address, etc. is stored in a table that references a RawContact ID. The Data table can contain anything you like, and there are several predefined MIME-types available for phone numbers, email addresses, names, etc. Android will automatically try to combine RawContacts that contain the same name, email address, etc. into a single Contact, and the user can also combine and split Contacts manually from the contact edit screen.

Custom Data Types

A sync provider can store additional data about a RawContact in the Data table, and provide an xml file to tell the Contacts app how to format this row. To create a custom MIME type, we need to add an additional meta-data tag to our service entry in AndroidManifest.xml to reference a new ContactsSource XML file:

AndroidManifest.xml snippet

This ContactsSource xml file will tell the Contacts app how to format our custom MIME-types. In the example below, we specify an icon, and tell the Contacts app to use the DATA2 and DATA3 columns to render the fields. Note that this file’s format doesn’t appear to be documented outside of the source code for the Contacts app.

contacts.xml

Here’s how Facebook’s custom “Facebook Profile” field looks when rendered by the Contacts app:

Creating a RawContact

Now lets put all the above information together to create a RawContact for a Last.fm user. Our contacts will display only a name and a custom field that links to the user’s Last.fm profile. We store the user’s username in the RawContact’s SYNC1 field so we can easily look it up later. We will batch together the creation of the RawContact and the insertion of the Data, as Android will run an aggregation pass after each batch completes.

addContact method

private static void addContact(Account account, String name, String username) { Log.i(TAG, "Adding contact: " + name); ArrayList operationList = new ArrayList(); //Create our RawContact ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI); builder.withValue(RawContacts.ACCOUNT_NAME, account.name); builder.withValue(RawContacts.ACCOUNT_TYPE, account.type); builder.withValue(RawContacts.SYNC1, username); operationList.add(builder.build()); //Create a Data record of common type 'StructuredName' for our RawContact builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI); builder.withValueBackReference(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID, 0); builder.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE); builder.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name); operationList.add(builder.build()); //Create a Data record of custom type "vnd.android.cursor.item/vnd.fm.last.android.profile" to display a link to the Last.fm profile builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI); builder.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0); builder.withValue(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.fm.last.android.profile"); builder.withValue(ContactsContract.Data.DATA1, username); builder.withValue(ContactsContract.Data.DATA2, "Last.fm Profile"); builder.withValue(ContactsContract.Data.DATA3, "View profile"); operationList.add(builder.build()); try { mContentResolver.applyBatch(ContactsContract.AUTHORITY, operationList); } catch (Exception e) { Log.e(TAG, "Something went wrong during creation! " + e); e.printStackTrace(); } }

Social status updates

Android keeps another table for social networking status updates. Inserting a record into this table will replace any previous status if the timestamp of the insert is newer than the previous timestamp, otherwise the previous record will remain and the new insert will be discarded. A status update record is associated with a Data record, in our implementation we will associate it with our Last.fm profile record. Status records contain the status text, a package name where resources are located, an icon resource, and a label resource. Below is a function that will insert a status update, as well as updating our Last.fm Profile Data record to display the last track the user listened to. Note that for efficiency purposes, we will send all the updates in a single batch, so Android will only run a single aggregation pass at the end.

updateContactStatus method

private static void updateContactStatus(ArrayList operationList, long rawContactId, Track track) { Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId); Uri entityUri = Uri.withAppendedPath(rawContactUri, Entity.CONTENT_DIRECTORY); Cursor c = mContentResolver.query(entityUri, new String[] { RawContacts.SOURCE_ID, Entity.DATA_ID, Entity.MIMETYPE, Entity.DATA1 }, null, null, null); try { while (c.moveToNext()) { if (!c.isNull(1)) { String mimeType = c.getString(2); String status = ""; if (track.getNowPlaying() != null && track.getNowPlaying().equals("true")) status = "Listening to " + track.getName() + " by " + track.getArtist(); else status = "Listened to " + track.getName() + " by " + track.getArtist(); if (mimeType.equals("vnd.android.cursor.item/vnd.fm.last.android.profile")) { ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(ContactsContract.StatusUpdates.CONTENT_URI); builder.withValue(ContactsContract.StatusUpdates.DATA_ID, c.getLong(1)); builder.withValue(ContactsContract.StatusUpdates.STATUS, status); builder.withValue(ContactsContract.StatusUpdates.STATUS_RES_PACKAGE, "fm.last.android"); builder.withValue(ContactsContract.StatusUpdates.STATUS_LABEL, R.string.app_name); builder.withValue(ContactsContract.StatusUpdates.STATUS_ICON, R.drawable.icon); if (track.getDate() != null) { long date = Long.parseLong(track.getDate()) * 1000; builder.withValue(ContactsContract.StatusUpdates.STATUS_TIMESTAMP, date); } operationList.add(builder.build()); builder = ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI); builder.withSelection(BaseColumns._ID + " = '" + c.getLong(1) + "'", null); builder.withValue(ContactsContract.Data.DATA3, status); operationList.add(builder.build()); } } } } finally { c.close(); } }

Here’s how the contact record looks after we’ve inserted a status update record and updated our data record:

Putting It All Together

Now that we have our utility functions written, we can fill in the body of our performSync method. We’re going to take a very simple approach to syncing: we’ll grab the list of RawContact IDs currently associated with Last.fm usernames, and we’ll create a RawContact for any Last.fm friend that doesn’t currently have one. If a RawContact already exists, we’ll fetch their most recent track and post a status update. Note that this is a one-way sync, we’re not dealing with local deletions or changes.

performSync method

private static void performSync(Context context, Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) throws OperationCanceledException { HashMap localContacts = new HashMap(); mContentResolver = context.getContentResolver(); Log.i(TAG, "performSync: " + account.toString()); // Load the local Last.fm contacts Uri rawContactUri = RawContacts.CONTENT_URI.buildUpon().appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name).appendQueryParameter( RawContacts.ACCOUNT_TYPE, account.type).build(); Cursor c1 = mContentResolver.query(rawContactUri, new String[] { BaseColumns._ID, RawContacts.SYNC1 }, null, null, null); while (c1.moveToNext()) { localContacts.put(c1.getString(1), c1.getLong(0)); } ArrayList operationList = new ArrayList(); LastFmServer server = AndroidLastFmServerFactory.getServer(); try { Friends friends = server.getFriends(account.name, "", "50"); for (User user : friends.getFriends()) { if (!localContacts.containsKey(user.getName())) { if (user.getRealName().length() > 0) addContact(account, user.getRealName(), user.getName()); else addContact(account, user.getName(), user.getName()); } else { Track[] tracks = server.getUserRecentTracks(user.getName(), "true", 1); if (tracks.length > 0) { updateContactStatus(operationList, localContacts.get(user.getName()), tracks[0]); } } } if(operationList.size() > 0) mContentResolver.applyBatch(ContactsContract.AUTHORITY, operationList); } catch (Exception e1) { // TODO Auto-generated catch block e1.printStackTrace(); } }

Contact visibility

By default, new contacts that you create from your sync provider are not shown by the Contacts app. Instead, it will show only contacts it has aggregated with other visible sources. To enable the display of contacts that only exist in your provider, press the Menu button and select “Display options”, then expand the entry for your account and tap the checkbox next to “All Contacts”. This is a good default, as I don’t really want my address book to be littered with hundreds of twitter contacts!

Final Thoughts

The Android platform is awesome. The lack of documentation, however, is quite disappointing. While it’s great that I can dig through the system source code while troubleshooting and figuring things out, I shouldn’t have to!

Sync providers require Android 2.0, which is currently only available on 2 devices. The Account modifications I made to our login activity will need to be reimplemented using class introspection in order to keep compatibility with Android 1.5.

This sync provider is far from complete — I still need to figure out how to control the sync interval, set my sync provider as read-only since local changes are ignored, I might grab the user’s avatar if they don’t currently have an icon, and add a fancy “Do you want to sync your contacts?” popup to the login process similar to Facebook’s.

Hopefully this will help other developers out there struggling with the lack of documentation, as I’m looking forward to seeing twitter status updates in the contacts app in the future!

The source code for the implementation referenced here is available in my Last.fm github project under the terms of the GNU General Public License. A standalone sample project is also available here under the terms of the Apache License 2.0. Google has also released their own sample sync provider on the Android developer portal that’s a bit more complete than mine.

This Post Has 84 Comments

  1. To control the sync interval:

    ContentResolver.addPeriodicSync(account, authority, extras, 30);

    I do this in the performSync method if there isn’t a periodic sync already set up.

  2. Hi Sam,
    Your sync adapter really helped me a lot. However I’m struggling where ronem got stuck. I’m using the sync provider in the android samples and am able to sync in the contacts. However, I want to launch a new activity on the click of the profile and this is not a success even after I have added the activity to the android manifest and have defined the proper mime type(as in contacts.xml) as described above. Am I missing Something………..Are there any further steps needed?………….Any help would be greatly appreciated.

    [WORDPRESS HASHCASH] The poster sent us ‘0 which is not a hashcash value.

  3. Hi,
    When i am going to add a contact with sample sync account. It is showing only two fields. How i can see all fields like email,organization. I want to sync all fields.

    [WORDPRESS HASHCASH] The poster sent us ‘0 which is not a hashcash value.

  4. I have couple of questions
    1) Can we use syncAdapter to get data of a row of raw_contact table if that row has been affected (i.e added/deleted/edited). I know ContentObserver notify whenever raw_content table changes in onChange() method but we dont know which row has been changed ( to put this in another way if we add how would we know and if deleted we still get it through deleted flag in raw_contact table but how about edited one??).

    2) when we get notification in ContentObserver can we fire requestSync (Account account, String authority, Bundle extras) to start Sync ?? how we can get data of raw_contact affected by contact application using these to parameters .

    thanks again man !! your blog is really nice please share some idea what you think

  5. Sam,

    Who starts the ContactsSyncAdapterService , why did you use android:process=:contact ?. im phone it is not being called by the system, can we start onPerformSync() on demand??

  6. Hi Sam, Thanks for the great article and sample! I’ve created an app which adds user info to contacts, and it can be called from the contacts Quick contact badge. However, I met an issue after uninstalled the app the user info are still left in contacts and the contacts crashes when I click that contact, it said “Couldn’t find authenticator for specific account type”.

    Do you know what’s the problem and how to fix it? Thanks very much.

  7. how to add another sync item such as sync calendar in the Data & synchronization category. how to design the interface? does it need another service or adapter to sync the calendar. What’s more, if I want to custom the item, how can i do ?

  8. any idea of how to change/show the sync interval options? thanks

  9. Hi !

    When writting the sync-adapter, accountType must be fill with something (if it is not it will crash the contact app and eventually the phone).

    But do you know if there is a way to set a default accountType ? I mean i want my app to add (and a ContactSyncAdapterService) a new field in each contact who already exist, so i don’t need to create a new contact type, do i ?

  10. Hi!

    Great implementation!!!

    One question i´ve made the tutorial but updateContactStatus() is never call.

    Do you have some tip?

    Thank you!

  11. My code

    ArrayList operationList = new ArrayList();
    try {
    // If we don’t have any contacts, create one. Otherwise, set a
    // status message
    if (localContacts.get(“Me”) == null) {
    addContact(account, “Me”, “yes”);
    } else {
    updateContactStatus(operationList, localContacts.get(“Me”), “Statuses”);
    }
    if (operationList.size() > 0)
    mContentResolver.applyBatch(ContactsContract.AUTHORITY, operationList);
    } catch (Exception e1) {
    // TODO Auto-generated catch block
    e1.printStackTrace();
    }

    Thank you very much!!!

  12. i use the Android SampleSyncAdapter in my application and it work done but when i made join for the contacts the name of application not appear its show unknown so how i can change this unknown to my application name as in face book and twitter thanx for reply

  13. how i can change in the sync adapter the
    Icons and Titles

  14. Very powerful

  15. Hii…. Can u Please Help me with android sync… I want to use it in my application for sync contacts on server side… How to Sync android device with server… Thanks

  16. thx
    Your two posts will help me a lot.

  17. Hi,thanks
    Actually i need to sync local sqlite db content with the 3rd party db and vice verse.Is sync adapter is benefited or no if yes plz share how can be done and interaction are done through rest web service calls

  18. Hi is there a way to do contacts synchronization without using a token at all? I want to send username and password only. I see that it is less secure, but is there a way to do it and how to do it? I searched but I couldn’t finds any example without using a token.

  19. Hi,

    inspired by your works I’m writing a bidirectional sync-provider (android:supportsUploading=”true”).

    I have near the result: I have obtained that when I add a contact (via phone GUI) the phone request on which account I want add the new contact. BUT after I select my sync-provider the GUI allow me to insert only name and surname, no others infos (phone, mobile, email …).

    If you have a guru-suggest I will appreciate it.

    TIA

  20. Hi Sam:

    I enter the pathe “Last.fm github project” not found under the terms of the GNU General Public License

    Can you send the source code to me?

    I try to learn how to coding the “Add account & Sync”

  21. Sam

    Excellent tutorial.

    I am having the same problem a Brian except his solution does not work for me. Initally I could not add or edit contact to accounts provided with this provider. After his fix I can edit (contact added with myPhoneExplorer) but not add contacts. This is with both the tutorial and downloaded demo. Should I be able to add new contact?

  22. @Dominic
    Sam,

    I forgot to mention that I am running 4.1.2 and seeing same thing on both phone and emulator.

  23. @Dominic No, you should not be able to add contacts, this tutorial is for a read-only contacts source. To enable adding contacts, you need to change android:supportsUploading to true in sync_contacts.xml, and add syncing logic to your adapter.

  24. Hiee… I want to integrate openERP contacts.. How can i achieve that???? Plz help me out…

  25. I want to know if the custom data I will add to those contact that bleong to my Syncadapter will be modifiable by other app. I want to prevent that, I mean I don’t care if people can read that but I don’t want them to modify it. Is it possible? if so can you point me how.

  26. Hi.. onPerformSync is not getting called..although am calling ContentResolver.requestSync…could you please help

  27. LastFmServer should be diplayed. I need to understand how to connect to the server

  28. I am using your code but this code is adding only one contact at a time, but I want to add multiple contacts in my contact list at a time, how it can be possible? thanks in Advance….

  29. hi . i wrote app and it inserts a record into contact list successfuly . but there is a problem in my app that i don`t want to insert contact to contact list i want to update existing contact with my app and add app icon to contact but i diden`t successful . i want you u please help me

Comments are closed.

Close Menu