A curious case of SyncAdapter syncing

tl;dr

Any modification (add, remove, change password) to any Account from the AccountManager will trigger a sync for all of the system's SyncAdapters, including the one you've just created in your app!

Context

In my attempt to better understand how a SyncAdapter works, I've followed the official documentation and created a sample application. However, I've stumbled upon a curious case when my simple SyncAdapter performs a sync even if it wasn't configured to: every time an application added an Account using the AccountManager I could see that onPerformSync() method was called. This caught my eye and I wanted to find out more, so I dived into the AOSP source code.

The problem

Taking a look at SyncManager - the class responsible with scheduling syncs - I could see that a BroadcastReceiver with a descriptive name is registered:

private BroadcastReceiver mAccountsUpdatedReceiver = new BroadcastReceiver() {

     public void onReceive(Context context, Intent intent) {
         updateRunningAccounts();
         
         // Kick off sync for everyone, since this was a radical account change
         scheduleSync(null, UserHandle.USER_ALL, null, null, 0 /* no delay */, false);
     }
};

It turns out that when the broadcast is received, the SyncManager schedules a sync for every SyncAdapter. But who triggers the broadcast? Taking a step further and assuming, from the receiver's name, that it's something related with AccountManager, I started to take a look at AccountManagerService. Here I could see that there's one descriptive method that does exactly what I was looking for - triggering a broadcast for account change events:

private void sendAccountsChangedBroadcast(int userId) {
    Log.i(TAG, "the accounts changed, sending broadcast of " + ACCOUNTS_CHANGED_INTENT.getAction());
    mContext.sendBroadcastAsUser(ACCOUNTS_CHANGED_INTENT, new UserHandle(userId));
}

The only thing that was left to find out was when this method is called. Searching for its usages I could see that the broadcast is sent when the following methods are called:

  • private boolean addAccountInternal(UserAccounts, Account, String, Bundle)

  • private void removeAccountInternal(UserAccounts, Account)

  • private void setPasswordInternal(UserAccounts, Account, String)

Brieftly, whenever an application adds, removes or changes the password of an account, a broadcast is sent that triggers all the SyncAdapters to perform a sync. This is rather curious, especially because there's no documentation around it.

Moreover, if you're not aware of this behaviour your application can be trapped into some subtle bugs (e.g. your sync triggers a modification of an account and from here this triggers another sync and you're stuck into an infinite loop!).

Solution

If you don't want your SyncAdapter to perform a sync when an account is changed, you can easily make use of the Bundle that you can pass in the ContentResolver's requestSync(Account, String, Bundle) or addPeriodicSync(Account, String, Bundle, long) methods:

Bundle mySyncExtras = new Bundle();
mySyncExtras.putBoolean("com.andreic.extra.MY_SYNC", true);
ContentResolver.requestSync(account, AUTHORITY, mySyncExtras);

This bundle will be received in the onPerformSync(Account, Bundle, String, ContentProviderClient, SyncResult) method of your SyncAdapter. Here you can check if the bundle contains your custom extra or not, and perform the sync operation only if it does (the sync triggered by the system when an account has changed will not contain your custom bundle):

public void onPerformSync(Account account, Bundle extras, 
	String authority, ContentProviderClient provider, SyncResult syncResult) {
	if(extras.containsKey("com.andreic.extra.MY_SYNC")) {
           // perform your sync
    }
}