Home > Android > Writing an Android Sync Provider: Part 2

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

  1. <uses-permission android:name="android.permission.INTERNET" />
  2. <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  3. <uses-permission android:name="android.permission.READ_CONTACTS" />
  4. <uses-permission android:name="android.permission.WRITE_CONTACTS" />
  5. <uses-permission android:name="android.permission.GET_ACCOUNTS" />
  6. <uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
  7. <uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
  8. <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
  9. <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />

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

  1. public class ContactsSyncAdapterService extends Service {
  2.  private static final String TAG = "ContactsSyncAdapterService";
  3.  private static SyncAdapterImpl sSyncAdapter = null;
  4.  private static ContentResolver mContentResolver = null;
  5.  
  6.  public ContactsSyncAdapterService() {
  7.   super();
  8.  }
  9.  
  10.  private static class SyncAdapterImpl extends AbstractThreadedSyncAdapter {
  11.   private Context mContext;
  12.  
  13.   public SyncAdapterImpl(Context context) {
  14.    super(context, true);
  15.    mContext = context;
  16.   }
  17.  
  18.   @Override
  19.   public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
  20.    try {
  21.     ContactsSyncAdapterService.performSync(mContext, account, extras, authority, provider, syncResult);
  22.    } catch (OperationCanceledException e) {
  23.    }
  24.   }
  25.  }
  26.  
  27.  @Override
  28.  public IBinder onBind(Intent intent) {
  29.   IBinder ret = null;
  30.   ret = getSyncAdapter().getSyncAdapterBinder();
  31.   return ret;
  32.  }
  33.  
  34.  private SyncAdapterImpl getSyncAdapter() {
  35.   if (sSyncAdapter == null)
  36.    sSyncAdapter = new SyncAdapterImpl(this);
  37.   return sSyncAdapter;
  38.  }
  39.  
  40.  private static void performSync(Context context, Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult)
  41.    throws OperationCanceledException {
  42.   mContentResolver = context.getContentResolver();
  43.   Log.i(TAG, "performSync: " + account.toString());
  44.   //This is where the magic will happen!
  45.  }
  46. }

The service is defined in AndroidManifest.xml like so:

AndroidManifest.xml snippet

  1. <service android:name=".sync.ContactsSyncAdapterService"
  2.  android:exported="true" android:process=":contacts">
  3.  <intent-filter>
  4.   <action android:name="android.content.SyncAdapter" />
  5.  </intent-filter>
  6.  <meta-data android:name="android.content.SyncAdapter"
  7.   android:resource="@xml/sync_contacts" />
  8. </service>

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

  1. <sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
  2.     android:contentAuthority="com.android.contacts"
  3.     android:accountType="fm.last.android.account"/>

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

  1. <meta-data android:name="android.provider.CONTACTS_STRUCTURE"
  2.  android:resource="@xml/contacts" />

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

  1. <ContactsSource xmlns:android="http://schemas.android.com/apk/res/android">
  2.  <ContactsDataKind
  3.   android:icon="@drawable/icon"
  4.   android:mimeType="vnd.android.cursor.item/vnd.fm.last.android.profile"
  5.   android:summaryColumn="data2"
  6.   android:detailColumn="data3"
  7.   android:detailSocialSummary="true" />
  8. </ContactsSource>

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

  1. private static void addContact(Account account, String name, String username) {
  2.  Log.i(TAG, "Adding contact: " + name);
  3.  ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
  4.  
  5.  //Create our RawContact
  6.  ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI);
  7.  builder.withValue(RawContacts.ACCOUNT_NAME, account.name);
  8.  builder.withValue(RawContacts.ACCOUNT_TYPE, account.type);
  9.  builder.withValue(RawContacts.SYNC1, username);
  10.  operationList.add(builder.build());
  11.  
  12.  //Create a Data record of common type 'StructuredName' for our RawContact
  13.  builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
  14.  builder.withValueBackReference(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID, 0);
  15.  builder.withValue(ContactsContract.Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE);
  16.  builder.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, name);
  17.  operationList.add(builder.build());
  18.  
  19.  //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
  20.  builder = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI);
  21.  builder.withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0);
  22.  builder.withValue(ContactsContract.Data.MIMETYPE, "vnd.android.cursor.item/vnd.fm.last.android.profile");
  23.  builder.withValue(ContactsContract.Data.DATA1, username);
  24.  builder.withValue(ContactsContract.Data.DATA2, "Last.fm Profile");
  25.  builder.withValue(ContactsContract.Data.DATA3, "View profile");
  26.  operationList.add(builder.build());
  27.  
  28.  try {
  29.   mContentResolver.applyBatch(ContactsContract.AUTHORITY, operationList);
  30.  } catch (Exception e) {
  31.   Log.e(TAG, "Something went wrong during creation! " + e);
  32.   e.printStackTrace();
  33.  }
  34. }

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

  1. private static void updateContactStatus(ArrayList<ContentProviderOperation> operationList, long rawContactId, Track track) {
  2.  Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
  3.  Uri entityUri = Uri.withAppendedPath(rawContactUri, Entity.CONTENT_DIRECTORY);
  4.  Cursor c = mContentResolver.query(entityUri, new String[] { RawContacts.SOURCE_ID, Entity.DATA_ID, Entity.MIMETYPE, Entity.DATA1 }, null, null, null);
  5.  try {
  6.   while (c.moveToNext()) {
  7.    if (!c.isNull(1)) {
  8.     String mimeType = c.getString(2);
  9.     String status = "";
  10.     if (track.getNowPlaying() != null && track.getNowPlaying().equals("true"))
  11.      status = "Listening to " + track.getName() + " by " + track.getArtist();
  12.     else
  13.      status = "Listened to " + track.getName() + " by " + track.getArtist();
  14.  
  15.     if (mimeType.equals("vnd.android.cursor.item/vnd.fm.last.android.profile")) {
  16.      ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(ContactsContract.StatusUpdates.CONTENT_URI);
  17.      builder.withValue(ContactsContract.StatusUpdates.DATA_ID, c.getLong(1));
  18.      builder.withValue(ContactsContract.StatusUpdates.STATUS, status);
  19.      builder.withValue(ContactsContract.StatusUpdates.STATUS_RES_PACKAGE, "fm.last.android");
  20.      builder.withValue(ContactsContract.StatusUpdates.STATUS_LABEL, R.string.app_name);
  21.      builder.withValue(ContactsContract.StatusUpdates.STATUS_ICON, R.drawable.icon);
  22.      if (track.getDate() != null) {
  23.       long date = Long.parseLong(track.getDate()) * 1000;
  24.       builder.withValue(ContactsContract.StatusUpdates.STATUS_TIMESTAMP, date);
  25.      }
  26.      operationList.add(builder.build());
  27.  
  28.      builder = ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI);
  29.      builder.withSelection(BaseColumns._ID + " = '" + c.getLong(1) + "'", null);
  30.      builder.withValue(ContactsContract.Data.DATA3, status);
  31.      operationList.add(builder.build());
  32.     }
  33.    }
  34.   }
  35.  } finally {
  36.   c.close();
  37.  }
  38. }

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

  1. private static void performSync(Context context, Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult)
  2.   throws OperationCanceledException {
  3.  HashMap<String, Long> localContacts = new HashMap<String, Long>();
  4.  mContentResolver = context.getContentResolver();
  5.  Log.i(TAG, "performSync: " + account.toString());
  6.  
  7.  // Load the local Last.fm contacts
  8.  Uri rawContactUri = RawContacts.CONTENT_URI.buildUpon().appendQueryParameter(RawContacts.ACCOUNT_NAME, account.name).appendQueryParameter(
  9.    RawContacts.ACCOUNT_TYPE, account.type).build();
  10.  Cursor c1 = mContentResolver.query(rawContactUri, new String[] { BaseColumns._ID, RawContacts.SYNC1 }, null, null, null);
  11.  while (c1.moveToNext()) {
  12.   localContacts.put(c1.getString(1), c1.getLong(0));
  13.  }
  14.  
  15.  ArrayList<ContentProviderOperation> operationList = new ArrayList<ContentProviderOperation>();
  16.  LastFmServer server = AndroidLastFmServerFactory.getServer();
  17.  try {
  18.   Friends friends = server.getFriends(account.name, "", "50");
  19.   for (User user : friends.getFriends()) {
  20.    if (!localContacts.containsKey(user.getName())) {
  21.     if (user.getRealName().length() > 0)
  22.      addContact(account, user.getRealName(), user.getName());
  23.     else
  24.      addContact(account, user.getName(), user.getName());
  25.    } else {
  26.     Track[] tracks = server.getUserRecentTracks(user.getName(), "true", 1);
  27.     if (tracks.length > 0) {
  28.      updateContactStatus(operationList, localContacts.get(user.getName()), tracks[0]);
  29.     }
  30.    }
  31.   }
  32.   if(operationList.size() > 0)
  33.    mContentResolver.applyBatch(ContactsContract.AUTHORITY, operationList);
  34.  } catch (Exception e1) {
  35.   // TODO Auto-generated catch block
  36.   e1.printStackTrace();
  37.  }
  38. }

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.

  1. laxman
    December 27th, 2010 at 18:14 | #1

    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.

  2. laxman
    December 27th, 2010 at 18:29 | #2

    @Sharad

    @Sharad
    Hi, Still i am facing same problem. Please give how to get allfields in contact. Laxmanbalu@gmail.com

  3. himanshu
    January 20th, 2011 at 22:37 | #3

    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

  4. himanshu
    January 27th, 2011 at 09:51 | #4

    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??

  5. Jinliang
    February 19th, 2011 at 01:33 | #5

    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.

  6. paddy
    February 23rd, 2011 at 02:11 | #6

    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 ?

  7. eduardo
    March 21st, 2011 at 00:50 | #7

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

  8. ClementMahe
    July 25th, 2011 at 05:46 | #8

    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 ?

  9. fernando
    September 23rd, 2011 at 05:04 | #9

    Hi!

    Great implementation!!!

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

    Do you have some tip?

    Thank you!

  10. fernando
    September 23rd, 2011 at 05:09 | #10

    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!!!

  11. stevo
    February 14th, 2012 at 06:37 | #11

    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

  12. stevo
    February 15th, 2012 at 02:28 | #12

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

  13. Leo Sun
    February 20th, 2012 at 01:46 | #13

    Very powerful

  14. Ankit
    May 2nd, 2012 at 04:45 | #14

    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

  15. GENiAli
    July 11th, 2012 at 23:35 | #15

    thx
    Your two posts will help me a lot.

  16. vinayakkumar
    August 10th, 2012 at 12:18 | #16

    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

  17. nikmin
    January 10th, 2013 at 06:48 | #17

    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.

  18. Ice72
    February 21st, 2013 at 08:13 | #18

    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

  19. Ray
    April 20th, 2013 at 21:03 | #19

    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”

  20. Dominic
    June 6th, 2013 at 06:04 | #20

    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?

  21. Dominic
    June 6th, 2013 at 06:07 | #21

    @Dominic
    Sam,

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

  22. June 6th, 2013 at 07:16 | #22

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

  23. Angel
    August 21st, 2013 at 20:53 | #23

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

  24. Julien
    October 29th, 2013 at 09:59 | #24

    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.

  25. Latha
    November 10th, 2013 at 23:08 | #25

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

  26. Helevatic
    September 15th, 2014 at 09:43 | #26

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

Comment pages
1 2 1262
  1. February 6th, 2010 at 10:45 | #1
  2. August 17th, 2010 at 13:16 | #2
  3. January 5th, 2011 at 22:18 | #3
  4. April 30th, 2011 at 11:15 | #4
  5. November 27th, 2011 at 14:02 | #5
  6. January 12th, 2013 at 12:52 | #6
Enter your name and either your email address or an OpenID URL. If you're a LiveJournal user, you can enter the address of your LiveJournal as your OpenID URL.

Or...