• Post category:Android

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. Berto

    Excellent documentation! I’ve been pawing around through the source code for a few days and got everything set up except how to get the contact information to show up in the contact. Thanks to your post, I realized I had all the right pieces, but I wasn’t aware of how to implement them.

    To make your SyncAdapter read-only, you can use: android:supportsUploading=”false” in your sync adapter’s xml file.

  2. Sharad

    This is great documentation which unfortunately we should have got from Google. Anyways my next question is how do we add our application to QuickContact list?
    I think this should be the next step after this, what do u say?

  3. Sam Steele

    @Sharad Your application will appear in the Quick Contact list when you define an activity to view your custom MIME-type, “vnd.android.cursor.item/vnd.fm.last.android.profile” in my example above.

  4. Sharad

    Thanks for the reply…i got that after some investigation, i have one more question, when i try to add a new contact it gives a list and gives me the name of my application. I don’t want that. User should not create contacts under my application. How is this possible.

  5. Sam Steele

    @Sharad add android:supportsUploading=”false” to your contact provider xml (sync_contacts.xml in my example) to disable creation of new contacts

  6. Sharad

    Worked thanks for the help

  7. Paul

    Thanks for this great tutorial.

    I am wondering if creating a sync provider is the only way to include ones app in the Quick Contact widget. If my application has nothing to sync, but would benefit the user being in the QC list, must I create a phantom sync service (authenticate and the “whole-nine”)?

    I’m wondering if its possible to simply include my app on the QC list and, if so, how?

    Thanks in advance!

  8. Sam Steele

    The QuickContact list is built from the MIME-types associated with the contacts Data records, so as far as I know there’s no way for your app to appear in the QC list without having added a record to the data table using a sync provider. I suppose it may be possible to insert records into that table inside your app, but it still needs to be associated with your own account type.

  9. Berto

    Hey Sam,

    I’m a little curious why you explicitly perform:

    #
    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());

    in your updateContactStatus method. When I was playing around with statuses, it automatically did that for me because of the association with the data row.

  10. Sam Steele

    It’s probably unnecessary then, I hadn’t tried it without that.

  11. Sharad

    How do we enable the Sync by default. When i goto Accounts & Sync and in Manage Accounts i see that for my Application by default the the Sync service is off “Sync is OFF” i want this to be on by default.

  12. Sam Steele

    @Sharad You can enable syncing with ContentResolver.setSyncAutomatically(), an example is below:

    AccountManager am = AccountManager.get(this);
    Account[] accounts = am.getAccountsByType(getString(R.string.ACCOUNT_TYPE));
    ContentResolver.setIsSyncable(accounts[0], ContactsContract.AUTHORITY, 1);
    ContentResolver.setSyncAutomatically(accounts[0], ContactsContract.AUTHORITY, true);

  13. Sharad

    You are awesome…..Thanks it worked 🙂

  14. ronem

    What a great guide! Thanks for this. This is really helpful because there is very little or no documentation in the Android resources about this issue.

    However, I am still struggling with defining the activity that could be launched when a custom field/contact item is clicked. Also a QuickContact item is what I’m trying to implement. Could you shortly describe how the intent and activity should be defined? Should it be in the AndroidManifest.xml? Thank you very much. I currently just getting the log output describing that no activity for the custom contact item was found:

    I/ActivityManager( 53): Starting activity: Intent { act=android.intent.action.VIEW dat=content://com.android.contacts/data/22 }
    E/ViewContact( 103): No activity found for intent: Intent { act=android.intent.action.VIEW dat=content://com.android.contacts/data/22 }

  15. Sam Steele

    @ronem You define your activity in your AndroidManifest.xml to handle your MIME type like so:

    <activity android:name=".activity.Profile" android:label="@string/profile_label">
    	<intent-filter>
    		<action android:name="android.intent.action.VIEW" />
    		<category android:name="android.intent.category.DEFAULT" />
    		<data android:mimeType="vnd.android.cursor.item/vnd.fm.last.android.profile" />
    	</intent-filter>
    </activity>
    

    You can then read the data that gets passed to your activity through getIntent().getData() like so (I store the username in the DATA1 field of my contacts data row):

    if(getIntent().getData() != null) {
    	Cursor cursor = managedQuery(getIntent().getData(), null, null, null, null);
    	if(cursor.moveToNext()) {
    		mUsername = cursor.getString(cursor.getColumnIndex("DATA1"));
    	}
    }
    
  16. ronem

    @Sam Steele

    Oh, I suspected something like that in the manifest 🙂 Anyway, thanks for confirming my thoughts. I’ll give this a try tomorrow and report if get any progress 🙂

  17. ronem

    Works great! Thank you very much!

  18. Jiaming

    @Sam Steele
    Thanks for your great example.
    I have a question about auto-sync.
    When or what will trigger the sync?

  19. Greg Bryniarski

    This example has been very helpful. What causes the sync to start? Is there any way to control this? I have looked and looked for any clues and could not find anything.

  20. Sam Steele

    @Greg Bryniarski you can use the Sync Tester in the developer tools app to start a sync, otherwise the Android system starts it in regular intervals (not quite sure how often). You can also force a sync by disabling and then re-enabling syncing for your provider.

  21. Berto

    Hey Sam,

    Does your QuickContact icon work? All my icons work except for the QuickContact icon. Do you know where it gets pulled from?

  22. Sam Steele

    @Berto yep, our QuickContact icons work fine. Make sure the MIME-type of your custom data matches an activity in your AndroidManifest.xml

  23. Berto

    Sam Steele :
    @Berto yep, our QuickContact icons work fine. Make sure the MIME-type of your custom data matches an activity in your AndroidManifest.xml

    The action works fine as it launches the profile page, but the icon shows the default android icon. I’m not too sure why… Any thoughts?

  24. Berto

    @Sam Steele
    Ah, I needed to declare the icon in the application tag. I’d declared it everywhere but there so everything else was working fine. I thought it retraced and used the icon associated with your mimetype, but I was wrong obviously. Thanks!

  25. Tng

    Thx for the great article! It helped me a lot. I still have a problem when editting or inserting a contact. Android displays only the photo and the names. How can I let android inflate other fields such as emails, phones and so on when editting the contact?

  26. Sharad

    I have an issue; here are the steps to repro it
    1. Launch “Accounts and Sync” from settings and press home key
    2. Install and launch the application which uses and registers the SyncAdapter
    3. Complete the Application registration so that it tries to register itself to the AccountManager
    4. The device crashes and re-boots as its not able to find the SyncAdapter in the application but the application has a SyncAdapter defined

    I am sure this would happen in all the application that uses the above flow, this doesn’t happen if in Step 1 we press Back Key instead of Home key or if the application is already installed and just launched.

    Is there way by which we can refresh the AccountManager before registering the application so that the AccountManager knows that the application has a SyncAdapter and it uses the same?

  27. Sam Steele

    @Sharad I can confirm this as well, I followed your steps of launching Accounts & Sync, pressing the home button, then installing Last.fm from the Android Market — the device rebooted after I logged in.

    I would suggest filing a bug in the Android bug tracker, as the Accounts & Sync preference screen should stop listening to AccountManager events when it’s in the background, and refresh itself when it’s resumed instead.

  28. Sharad

    @Sam Steele
    I think the easiest way Google can fix this is by having a PackageBroadcast Receiver which updates the SyncAdapters for the AccountManager.
    Thanks for the reply.

  29. Sharad

    I want to display all the contacts visible in the Contact List by default, without the user going to Display Options and selecting the check box against my Sync Adapter, how do i achieve that. I have seen this working with the Exchange application and i was looking the Email app source code and i could not figure out how this can be done.

  30. Sharad

    I got it, we just make the Group Visible

    try{
    ContentProviderClient client = getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY_URI);
    ContentValues cv = new ContentValues();
    cv.put(Groups.ACCOUNT_NAME, account.name);
    cv.put(Groups.ACCOUNT_TYPE, account.type);
    cv.put(Settings.UNGROUPED_VISIBLE, true);
    client.insert(Settings.CONTENT_URI.buildUpon()
    .appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, “true”)
    .build(), cv);
    } catch (RemoteException e) {
    Log.d(TAG, “Cannot make the Group Visible”);
    }

  31. Rui

    Thanks for the article. But, did you ever found out how to control the sync interval? And do you know if is it possible to sync data from the device to the cloud, having then a two-way sync mechanism?

  32. Alex Tang

    Hi, thanks for your article. And after I add a custom account, only name fields can be edited. Did you meet this problem? Thanks.

  33. Bostjan

    Hey,

    first of all, I’d like to thank you for a great tutorial. It’s been a big help. I do have one question though.
    Let’s say that when syncing I’d like to sync any changes done locally on my website back to the phone. Is it correct of me to think I could use SYNC2 field and update it with the date of the latest sync (of that contact) and then check that timestamp against the timestamp of the contact in the web DB. If the web timestamp were newer then I perform a sync.

    Would that work or do you have any other suggestions for this case?

  34. Ambika

    I have ContactsSyncAdapterService. Its does sync contacts for one of my existing account-Exchange.. I have added one more account type, Can I make use of same ContactsSyncAdapterService to sync my contacts for my newly added account.

    I found no way to do this. I feel the framework is also designed in the same way.

    Can this be done. if so how?

  35. Thank you very much for this workshop. But I have a Question:
    Why did you put the performSync() method into the ContactsSyncAdapterService class and made it static? Why didn’t you do this work directly in the SyncAdapterImpl classes onPerformSync() method?

    Natanael

  36. Alvin

    great work man!!

  37. Marvin

    I have an architecture question about Android’s sync API. Can you sync *any* type of data you want, or must you use one of Android’s included synchronization providers (like the Facebook app)? I’ve seen other blogs hinting that this is part of the private API.

  38. Berto

    @Marvin
    You can sync anything you want. You just have to authorities set up to do so and allow your sync adapter to sync that authority.

  39. Sam Steele

    testing to make sure comments still work after upgrading wordpress

  40. Athr

    Hi,
    On above pic ie below “Data & synchronization” there is “Sync Contacts” Option is there.
    How to add more option and Even custom option also ?

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

  41. Sharad

    Now i have one more question. I know now how we can disable adding contact through the addressbook by just adding the flag android:supportsUploading=”false” in the xml file.
    But now i want the user to be able to add the contact from the addressbook so i removed this entry from the xml and when i try to add a contact in addressbook it shows only the name field instead of showing all the fields (phone, email, address). How would i achieve that, is there a similar flag?
    Thanks for the help!!!!!!

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

  42. Sharad

    When i add
    android:supportsUploading=”false”
    in the sync-adapter xml and if i try to create a contact for my Account in the addressbook it just shows me photo and name for the new contact to add.
    How can i get all the other fields like email, phone, address, etc?

  43. Sharad

    @Sharad
    Sorry its android:supportsUploading=”true”

Comments are closed.