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

Writing an Android Sync Provider: Part 1

January 23rd, 2010

One of the highlights of the Android 2.0 SDK is that you can write custom sync providers to integrate with the system contacts, calendars, etc. The only problem is that there’s very little documentation on how it all fits together. And worse, if you mess up in certain places, the Android system will crash and reboot! Always up for a challenge, I’ve navigated through the sparse documentation, vague mailing list posts, and the Android source code itself to build a sync provider for our Last.fm app. Want to know how to build your own? Read on!

Account Authenticators

The first piece of the puzzle is called an Account Authenticator, which defines how the user’s account will appear in the “Accounts & Sync” settings. Implementing an Account Authenticator requires 3 pieces: a service that returns a subclass of AbstractAccountAuthenticator from the onBind method, an activity to prompt the user to enter their credentials, and an xml file describing how your account should look when displayed to the user. You’ll also need to add the android.permission.AUTHENTICATE_ACCOUNTS permission to your AndroidManifest.xml.

The Service

The authenticator service is expected to return a subclass of AbstractAccountAuthenticator from the onBind method — if you don’t, Android will crash and reboot when you try to add a new account to the system. The only method in AbstractAccountAuthenticator we really need to implement is addAccount, which returns an Intent that the system will use to display the login dialog to the user. The implementation below will launch our app’s main launcher activity with an action of “fm.last.android.sync.LOGIN” and an extra containing the AccountAuthenticatorResponse object we use to pass data back to the system after the user has logged in.

AccountAuthenticatorService.java

  1. import fm.last.android.LastFm;
  2. import android.accounts.AbstractAccountAuthenticator;
  3. import android.accounts.Account;
  4. import android.accounts.AccountAuthenticatorResponse;
  5. import android.accounts.AccountManager;
  6. import android.accounts.NetworkErrorException;
  7. import android.app.Service;
  8. import android.content.Context;
  9. import android.content.Intent;
  10. import android.os.Bundle;
  11. import android.os.IBinder;
  12. import android.util.Log;
  13.  
  14. /**
  15.  * Authenticator service that returns a subclass of AbstractAccountAuthenticator in onBind()
  16.  */
  17. public class AccountAuthenticatorService extends Service {
  18.  private static final String TAG = "AccountAuthenticatorService";
  19.  private static AccountAuthenticatorImpl sAccountAuthenticator = null;
  20.  
  21.  public AccountAuthenticatorService() {
  22.   super();
  23.  }
  24.  
  25.  public IBinder onBind(Intent intent) {
  26.   IBinder ret = null;
  27.   if (intent.getAction().equals(android.accounts.AccountManager.ACTION_AUTHENTICATOR_INTENT))
  28.    ret = getAuthenticator().getIBinder();
  29.   return ret;
  30.  }
  31.  
  32.  private AccountAuthenticatorImpl getAuthenticator() {
  33.   if (sAccountAuthenticator == null)
  34.    sAccountAuthenticator = new AccountAuthenticatorImpl(this);
  35.   return sAccountAuthenticator;
  36.  }
  37.  
  38.  private static class AccountAuthenticatorImpl extends AbstractAccountAuthenticator {
  39.   private Context mContext;
  40.  
  41.   public AccountAuthenticatorImpl(Context context) {
  42.    super(context);
  43.    mContext = context;
  44.   }
  45.  
  46.   /*
  47.    *  The user has requested to add a new account to the system.  We return an intent that will launch our login screen if the user has not logged in yet,
  48.    *  otherwise our activity will just pass the user's credentials on to the account manager.
  49.    */
  50.   @Override
  51.   public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options)
  52.     throws NetworkErrorException {
  53.    Bundle reply = new Bundle();
  54.    
  55.    Intent i = new Intent(mContext, LastFm.class);
  56.    i.setAction("fm.last.android.sync.LOGIN");
  57.    i.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
  58.    reply.putParcelable(AccountManager.KEY_INTENT, i);
  59.    
  60.    return reply;
  61.   }
  62.  
  63.   @Override
  64.   public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account, Bundle options) {
  65.    return null;
  66.   }
  67.  
  68.   @Override
  69.   public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
  70.    return null;
  71.   }
  72.  
  73.   @Override
  74.   public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) throws NetworkErrorException {
  75.    return null;
  76.   }
  77.  
  78.   @Override
  79.   public String getAuthTokenLabel(String authTokenType) {
  80.    return null;
  81.   }
  82.  
  83.   @Override
  84.   public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account, String[] features) throws NetworkErrorException {
  85.    return null;
  86.   }
  87.  
  88.   @Override
  89.   public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account, String authTokenType, Bundle options) {
  90.    return null;
  91.   }
  92.  }
  93. }

The account authenticator service should be defined in your AndroidManifest.xml, with a meta-data tag referencing an xml definition file, as follows:

Snippet from AndroidManifest.xml

  1. <service android:name="AccountAuthenticatorService"
  2.  android:exported="true" android:process=":auth">
  3.  <intent-filter>
  4.   <action android:name="android.accounts.AccountAuthenticator" />
  5.  </intent-filter>
  6.  <meta-data android:name="android.accounts.AccountAuthenticator"
  7.   android:resource="@xml/authenticator" />
  8. </service>

The Activity

If you don’t already have a login screen, there’s a convenience class AccountAuthenticatorActivity you can subclass that will pass your response back to the authentication manager for you, however if you already have a login activity in place you may find it easier to just pass the data back yourself, as I have done here. When the user has successfully been authenticated, we create an Account object for the user’s credentials. An account has an account name, such as the username or email address, and an account type, which you will define in your xml file next. You may find it easier to store your account type in strings.xml and use getString() to fetch it, as it is used in multiple places.

Snippet from the Last.fm login activity

  1. Account account = new Account(username, getString(R.string.ACCOUNT_TYPE)));
  2. AccountManager am = AccountManager.get(this);
  3. boolean accountCreated = am.addAccountExplicitly(account, password, null);
  4.  
  5. Bundle extras = getIntent.getExtras();
  6. if (extras != null) {
  7.  if (accountCreated) {  //Pass the new account back to the account manager
  8.   AccountAuthenticatorResponse response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
  9.   Bundle result = new Bundle();
  10.   result.putString(AccountManager.KEY_ACCOUNT_NAME, username);
  11.   result.putString(AccountManager.KEY_ACCOUNT_TYPE, getString(R.string.ACCOUNT_TYPE));
  12.   response.onResult(result);
  13.  }
  14.  finish();
  15. }

The XML definition file

The account xml file defines what the user will see when they’re interacting with your account. It contains a user-readable name, the system account type you’re defining, various icons, and a reference to an xml file containing PreferenceScreens the user will see when modifying your account.

authenticator.xml

  1. <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
  2.     android:accountType="fm.last.android.account"
  3.     android:icon="@drawable/icon"
  4.     android:smallIcon="@drawable/icon"
  5.     android:label="@string/app_name"
  6.     android:accountPreferences="@xml/account_preferences"/>

account_preferences.xml

  1. <PreferenceScreen
  2.   xmlns:android="http://schemas.android.com/apk/res/android">
  3.     <PreferenceCategory
  4.             android:title="General Settings" />
  5.  
  6.     <PreferenceScreen
  7.         android:key="account_settings"
  8.         android:title="Account Settings"
  9.         android:summary="Sync frequency, notifications, etc.">
  10.         <intent
  11.             android:action="fm.last.android.activity.Preferences.ACCOUNT_SETUP"
  12.             android:targetPackage="fm.last.android"
  13.             android:targetClass="fm.last.android.activity.Preferences" />
  14.     </PreferenceScreen>
  15. </PreferenceScreen>

Putting it all together

Now we’re ready for testing! The Android accounts setting screen doesn’t handle exceptions very well — if something goes wrong, your device will reboot! A better way to test is to launch the emulator, run the “Dev Tools” app, and pick “AccountsTester”.

You should see your new account type in the list, along with the built-in “Corporate” account type. Go ahead and select your account type from the drop-down list, and then press the “Add” button, and you should be presented with your login activity. After authenticating, your account should appear in a list below the buttons. At this point, it should be safe to use the system “Accounts & Sync” settings screen to remove or modify your account.

Ready to fill in that section below “Data & synchronization”? Let’s move on to part 2!

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. January 23rd, 2010 at 10:55 | #1

    Hah, I love that not catching an exception reboots the device 🙂 Thanks for this Sam. Very interesting.

  2. Hi
    February 1st, 2010 at 13:48 | #2

    How are you starting the AccountAuthenticatorService service? I’ve not been able to figure this out in your code so far. Thanks.

    • February 1st, 2010 at 13:57 | #3

      The Android system automatically starts the service when the user adds an account from the Accounts & Sync settings screen.

  3. Hi
    February 3rd, 2010 at 12:44 | #4

    I’m facing a problem with adding an account since the past few days.

    My addAccount implementation in AccountAuthenticatorService looks like the below:

    @Override
    public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options)
    throws NetworkErrorException {
    Bundle result;
    result = addAccount(mContext, “user”, “session_key”);
    return result;
    }
    The logcat output looks like the below:

    V/AddAccount( 53): Attempting to add account of type com.android.apps.myapp.account
    W/AccountManagerService( 53): caller uid 10044 is different than the authenticator’s uid.

    After this, nothing happens and I don’t see my account in the list under “Accounts & Sync”. Any idea why this could be happening? Do I have to give it a real user name and session key? Also what’s up with the caller UID warning? Any input would be appreciated. Thanks.

  4. February 3rd, 2010 at 12:49 | #5

    Try using addAccountExplicitly() instead of addAccount() as I do in my sample implementation above

  5. Hi
    February 3rd, 2010 at 13:25 | #6

    Well, I was using it from my addAccount function. Still I changed AbstractAccountAuthenticator.addAccount() it to the below to use addAccountExplicitly(). Still no luck though, getting the same message on logcat.

    @Override
    public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options)
    throws NetworkErrorException {
    Bundle result = null;
    Account account = new Account(“username”, AccAuthSvc.this.getString(R.string.ACCOUNT_TYPE));
    AccountManager am = AccountManager.get(AccAuthSvc.this);
    if (am.addAccountExplicitly(account, null, null)) {
    result = new Bundle();
    result.putString(AccountManager.KEY_ACCOUNT_NAME, account.name);
    result.putString(AccountManager.KEY_ACCOUNT_TYPE, account.type);
    }
    return result;
    }

  6. February 3rd, 2010 at 13:29 | #7

    Did you set your service to run as a separate process in the AndroidManifest.xml? android:exported=”true” android:process=”:auth” are the magic flags

  7. Hi
    February 3rd, 2010 at 15:37 | #8

    Yes. But I’m guessing its got something to with my not using the correct context to call the service from.

  8. Hmm
    February 4th, 2010 at 17:09 | #9

    Thanks for the code and explanation.

    However, when I click on Add Account in “Accounts & Sync” it takes me to the default Exchange screen. My service isnt getting fired at all. I don’t see Account Authenticator intent being raised either.

  9. February 6th, 2010 at 10:46 | #10

    I’ve posted a standalone sync provider demo project to GitHub that should make it easier to understand: http://github.com/c99koder/AndroidSyncProviderDemo

  10. February 6th, 2010 at 11:13 | #11

    @Hmm if your account doesn’t appear in the Accounts & Sync add screen, then you haven’t defined it properly in the various xml files, including your android manifest.

  11. Brian
    February 9th, 2010 at 12:43 | #12

    Thanks very much for a great post.

    One question. I’ve installed the AndroidSyncProviderDemo in the emulator and configured the account “efudd” now shows up on my “Account and sync Settings” settings page.

    However, I was expecting that when I create a new contact that it would either default to the new sync provider or prompt me for a choice.

    sqlite> select _id, account_name, account_type, display_name from raw_contacts;
    select _id, account_name, account_type, display_name from raw_contacts;
    _id|account_name|account_type|display_name
    1|efudd|org.c99.SyncProviderDemo.account|Elmer Fudd
    2|||Test1
    3|||Test2
    4|||Test3

    How can I make efudd|org.c99.SyncProviderDemo.account the account for all new contacts?

    Thanks again

  12. Brian
    February 9th, 2010 at 13:55 | #13

    Discovered the answer:
    – the AndroidSyncProviderDemo ships with android:supportsUploading=”false” in sync_contacts.xml

    – set it to android:supportsUploading=”true” and re-install the app

    Then all contacts created will be assigned to the demo provider

  13. Joe
    February 18th, 2010 at 09:00 | #14

    @Hi
    Having the same problem here; anyone know what causes it? Everything works fine up until the login is complete and the authenticator response is sent, and that’s when it crashes with:

    W/AccountManagerService( 52): caller uid 10026 is different than the authenticator’s uid
    E/AndroidRuntime( 348): java.lang.SecurityException: caller uid 10026 is different than the authenticator’s uid
    E/AndroidRuntime( 348): at android.os.Parcel.readException(Parcel.java:1218)
    E/AndroidRuntime( 348): at android.os.Parcel.readException(Parcel.java:1206)
    E/AndroidRuntime( 348): at android.accounts.IAccountManager$Stub$Proxy.addAccount(IAccountManager.java:506)
    E/AndroidRuntime( 348): at android.accounts.AccountManager.addAccountExplicitly(AccountManager.java:246)

    android:proc=”:auth” is set in AndroidManifest (and can be seen in logcat when the service starts up). What could be causing this?

  14. Berto
    February 19th, 2010 at 13:51 | #15

    That usually means that you are passing in the wrong context when you’re calling AccountManager.get(this).addAccountExplicitly. You need to make sure that you’re using the same context every time you call AccountManaget.get(). This is to prevent other things from messing with accounts your service has created.

  15. Joe
    March 16th, 2010 at 09:04 | #16

    @Berto
    I’ve apparently done something to break it even more, since then — now it doesn’t even launch my Login activity. Gets up to the return value of onBind(), after instantiating the AccountAuthenticatorImpl, then appears to do nothing with that IBinder. Doesn’t call AccountAuthenticatorImpl.addAccount(), which is what returns the Login activity’s Intent.

  16. Berto
    March 29th, 2010 at 12:42 | #17

    @Joe

    You may want to look at the code here: http://developer.android.com/resources/samples/SampleSyncAdapter/index.html

    It appears as though Google has finally put up a fully-coded and working sample.

  17. bikkits
    April 6th, 2010 at 10:53 | #18

    @Berto

    Fully-coded is a bit “strong” – it’s read-only for the simple reason that they’ve intentionally broken contacts sync so badly that only google contacts sync and their own exchange sync works properly with the default contacts app…

  18. James
    April 14th, 2010 at 19:17 | #19

    Great job!
    One question about the account management on Android. Now that we can add facebook account to the platform, is it possible to share the account across multiple facebook clients in the platform? I can login once, and update my facebook status in several applications if necessary. how to do that?
    thanks a lot!

  19. April 14th, 2010 at 19:24 | #20

    Facebook would need to implement that in their account provider. We’ve implemented third party authentication in the Last.fm app, and I’ve written a demo app to show how you can authenticate with the user’s existing Last.fm account here: http://github.com/c99koder/lastfm-android/tree/master/LastFmAuthTest/

  20. Susan
    May 19th, 2010 at 20:57 | #21

    Hi,
    Can I ask when will the override function onbind be called? I have my own code, but it cannot be called unless I use context.startService or context.bindService. Thanks!!

  21. July 13th, 2010 at 02:51 | #22

    @Joe
    If you get the “java.lang.SecurityException: caller uid” message, check your XML for
    android:accountType=”org.c99.SyncProviderDemo.account”.

    make sure the packagename is correct, especially if you rename the package and use all lowercase, the XML should also have all lowercase. At least that was what tripped me.

  22. parul
    July 23rd, 2010 at 05:26 | #23

    It may be wrong place to log my query, but its urgent.
    can anyone please help me how to delete google account from “accounts and sync”.
    I’m trying to call this line my app:
    AccountManagerService.getSingleton().onServiceChanged(null,true);

    whereas onServiceChanged() method is defined in AccountManagerService.java.

    this piece of code is not doing anything i feel exception is bieng thrown which is handled at lower layer.
    Please help if anyone is aware how to call this function or how to delete account.

  23. popopome
    July 29th, 2010 at 17:56 | #24

    @Joe
    Hey joe.
    I had same problems but when I added for the activity the problem is solved.

    Google’s sample does not define in manifest file
    I doubt whether their sample works for my emulator.

  24. popopome
    July 29th, 2010 at 17:57 | #25

    @popopome
    OOPS.

    <intent-filter>
    <action android:name=”android.intent.action.MAIN” />
    <category android:name=”android.intent.category.DEFAULT” />
    </intent-filter>

  25. parul
    August 1st, 2010 at 21:22 | #26

    Hi,
    in my previuos post, came to know we cant use AccountManagerService. Therefore using account manager.
    i need help to delete accounts which are displayed in “account and sync” from my app.

    i’m trying with this code:
    AccountManager accountManager = (AccountManager)mContext.getSystemService(Context.ACCOUNT_SERVICE);
    android.accounts.Account[] accounts = AccountManager.get(mContext).getAccounts();
    for (android.accounts.Account account: accounts) {
    AccountManager.get(mContext).removeAccount(account, null, null);
    }

    but i’m getting accounts = 0 i.e. no accounts. whereas accounts exist.
    It would be great help if u can suggest something to resolve my problem.

  26. chris
    August 12th, 2010 at 13:07 | #27

    If you get the UID doesn’t match error, check the AUTH_TOKEN you set in the app vs the one you have in your account-authenticator xml file, they need to match.

  27. Paul
    August 24th, 2010 at 16:39 | #28

    Yes, that seems to be the key to solve the uid not permitted issue.@hjo1620.myopenid.com/

  28. David
    September 15th, 2010 at 07:52 | #29

    I got a problem.
    The relevant part of my account_preferences.xml looks like this:
    […]

    […]

    The Activity-definition in the main-manifest looks like this:
    […]

    […]

    When I click on the Preference-Screen defined above then this activity should be started, but everytime I do so the whole process crashes and logcat says:

    09-15 16:38:57.191: ERROR/AndroidRuntime(30057): android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?

    Where is my mistake?

    David

  29. David
    September 15th, 2010 at 07:54 | #30

    Oh sorry:
    account_preferences:

    and manifest:

  30. David
    September 15th, 2010 at 07:58 | #31

    Sorry for spaming!
    Seems that its impossible to post a xml file.

    So here you go:
    http://pastebin.com/r867dhQ0

    Daniel

  31. Sean
    September 18th, 2010 at 01:42 | #32

    Anyone know what is the importance of account type?

    I tried to change the “accountType” field in xml

    android:accountType=”org.c99.SyncProviderDemo.account”.

    It seams like any name will do (e.g: abc.account , cde.abc.google, etc) as long as I change the same field in “sync_contacts.xml” and “authenticator.xml”. However, only “com.google” would result in UID error.

    anyone knows what happen?

  32. September 19th, 2010 at 01:21 | #33

    @Sean that field is what uniquely identifies your specific account type. You should choose an identifier that’s unique to your application.

  33. Sean
    September 19th, 2010 at 02:15 | #34

    @Sam Steele
    I can’t create account with “com.google” account type. Any ideas ?

  34. September 19th, 2010 at 09:28 | #35

    @Sean com.google belongs to Google. Use a different domain.

  35. Angela Hung
    September 29th, 2010 at 06:08 | #36

    Hi:
    I also encountered the alike error message, “W/AccountManagerService( 52): caller uid 10026 is different than the authenticator’s uid”.
    And I replaced the substring “org.c99” in “org.c99.SyncProviderDemo” as the path of my project. Then everything is ok.

    Thank you all for sharing what you know, it helps me a lot!! : )

  36. Angela Hung
    September 29th, 2010 at 06:09 | #37

    @Hi
    Hi:
    I also encountered the alike error message, “W/AccountManagerService( 52): caller uid 10026 is different than the authenticator’s uid”.
    And I replaced the substring “org.c99” in “org.c99.SyncProviderDemo” as the path of my project. Then everything is ok.

    Thank you all for sharing what you know, it helps me a lot!! : )

  37. Neil
    January 9th, 2011 at 14:13 | #38

    @Hi

    The account type you are using to create an account in code is probably different from the account type you specified in authenticator.xml:

    http://loganandandy.tumblr.com/post/613041897/caller-uid-is-different

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

  38. himanshu
    January 20th, 2011 at 22:38 | #39

    Hi Sam
    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

  39. himanshu
    January 21st, 2011 at 08:35 | #40

    himanshu :
    Hi Sam
    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

  40. Faisal
    March 30th, 2011 at 04:55 | #41

    Hi,

    I found the same error as @David’s.
    Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?

    Can anyone help to solve the issue?

  41. Anton Derevyanko
    March 31st, 2011 at 13:18 | #42

    Sam Steele, I’m even can’t even express in words how grateful to you! This topic is very difficult, and no one explain everythih so clear as you! THANKS.

  42. sendi
    August 17th, 2011 at 21:38 | #44

    how to turn auto sycn on ?

  43. sendi
    August 17th, 2011 at 21:39 | #45

    I meant programmatically

    sendi :
    how to turn auto sycn on ?

  44. August 21st, 2011 at 21:53 | #46

    You have a typo in your activity excerpt:

    Bundle extras = getIntent.getExtras();

    should be…

    Bundle extras = getIntent().getExtras();

    Thanks for the great tutorial!

    Cheers!

  45. manoj arora
    November 5th, 2011 at 01:30 | #47

    hii…..
    i need the use of account manager for adding a new account…Can u please tell me how to use your code because its showing error in the xmls tht “error: Error: No resource found that matches the given name (at ‘resource’ with value ‘@xml/authenticator’).”

Comment pages
1 2 1249