I'm currently working on an android app which relies on a SyncAdapter
to refresh its content from a server. I basically followed these instructions: https://developer.android.com/training/sync-adapters/creating-sync-adapter.html
This worked perfectly until recently. I know this might sound stupid, but I honestly have no idea how I screwed it up :(
The setup
I have one ContentProvider
and therefore one SyncAdapter
and one Account for all items i want to sync. I use integer flags to determine which item has to be synced:
public static final int EVENTS = 0x1;
public static final int NEWS = 0x2;
public static final int SUBSTITUTIONS = 0x4;
public static final int TEACHERS = 0x8;
public static final int ALL = 0xF;
So in my onPerformSync
I have something like:
ArrayList<ContentProviderOperation> batchList = new ArrayList<>();
int which = extras.getInt(SYNC.ARG, SYNC.ALL);
if((which & SYNC.NEWS) == SYNC.NEWS) { syncNews(provider, batchList, syncResult, 0, 1); }
if((which & SYNC.EVENTS) == SYNC.EVENTS) { syncEvents(provider, batchList, syncResult); }
if((which & SYNC.TEACHERS) == SYNC.TEACHERS) { syncTeachers(provider, batchList, syncResult); }
if((which & SYNC.SUBSTITUTIONS) == SYNC.SUBSTITUTIONS) { syncSubstitutions(provider, batchList, syncResult); }
Log.i(TAG, "Merge solution ready. Applying batch update to database...");
provider.applyBatch(batchList);
Because I also want the user to be able to force a refresh, I use a SwipeRefreshLayout
to fire up the sync service:
@Override
public void onRefresh() {
Log.d(TAG, "Force refresh triggered!");
SyncUtils.triggerRefresh(SyncAdapter.SYNC.NEWS | SyncAdapter.SYNC.EVENTS);
}
I also want to monitor the sync state, so I register / unregister a SyncStatusObserver
in my fragment's onResume
/ onPause
:
private final SyncStatusObserver syncStatusObserver = new SyncStatusObserver() {
@Override
public void onStatusChanged(int which) {
Account account = AuthenticatorService.getAccount(SyncUtils.ACCOUNT_TYPE);
boolean syncActive = ContentResolver.isSyncActive(account, DataProvider.AUTHORITY);
boolean syncPending = ContentResolver.isSyncPending(account, DataProvider.AUTHORITY);
final boolean refresh = syncActive || syncPending;
Log.d(TAG, "Status change detected. Active: %b, pending: %b, refreshing: %b", syncActive, syncPending, refresh);
swipeRefreshLayout.post(new Runnable() {
@Override
public void run() {
swipeRefreshLayout.setRefreshing(refresh);
}
});
}
};
The problem
Whenever I start the application, the Refresh layout
is indication a sync is active. I logged nearly everything and found out that a sync is in pending state. Whenever I try to force refresh the sync will either
- Become active, do all the stuff and then go back to pending or
- Simply not become active and staying in pending mode forever
Here's an example log:
D/HomeFragment﹕ Status change detected. Active: false, pending: true, refreshing: true
D/HomeFragment﹕ Status change detected. Active: false, pending: true, refreshing: true
D/HomeFragment﹕ Status change detected. Active: true, pending: true, refreshing: true
D/HomeFragment﹕ Status change detected. Active: false, pending: true, refreshing: true
As you can see, it will never be Active: false, pending: false
to indicate the sync finished. This really grinds my gears.
Some more code
I do the initial setup of the stub account (and the periodic syncs) in my application class:
public static void createSyncAccount(Context context) {
boolean newAccount = false;
boolean setupComplete = PreferenceManager
.getDefaultSharedPreferences(context).getBoolean(PREF_SETUP_COMPLETE, false);
// Create account, if it's missing. (Either first run, or user has deleted account.)
Account account = AuthenticatorService.getAccount(ACCOUNT_TYPE);
AccountManager accountManager =
(AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
if (accountManager.addAccountExplicitly(account, null, null)) {
// Inform the system that this account supports sync
ContentResolver.setIsSyncable(account, DataProvider.AUTHORITY, 1);
// Inform the system that this account is eligible for auto sync when the network is up
ContentResolver.setSyncAutomatically(account, DataProvider.AUTHORITY, true);
// Recommend a schedule for automatic synchronization. The system may modify this based
// on other scheduled syncs and network utilization.
requestPeriodic(account, SYNC.EVENTS, 172800);
requestPeriodic(account, SYNC.NEWS, 604800);
requestPeriodic(account, SYNC.SUBSTITUTIONS, 1800);
requestPeriodic(account, SYNC.TEACHERS, 2419200);
newAccount = true;
}
// Schedule an initial sync if we detect problems with either our account or our local
// data has been deleted. (Note that it's possible to clear app data WITHOUT affecting
// the account list, so wee need to check both.)
if (newAccount || !setupComplete) {
triggerRefresh(SYNC.ALL);
PreferenceManager.getDefaultSharedPreferences(context).edit()
.putBoolean(PREF_SETUP_COMPLETE, true).commit();
}
}
Where requestPeriodic()
is the following:
public static void requestPeriodic(Account account, int which, long seconds) {
Bundle options = new Bundle();
options.putInt(SYNC.ARG, which);
ContentResolver.addPeriodicSync(account,
DataProvider.AUTHORITY, options, seconds);
}
And my triggerRefresh()
looks like:
public static void triggerRefresh(int which) {
Log.d(TAG, "Force refresh triggered for id: %d", which);
Bundle options = new Bundle();
options.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
options.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
options.putInt(SYNC.ARG, which);
ContentResolver.requestSync(
AuthenticatorService.getAccount(ACCOUNT_TYPE),
DataProvider.AUTHORITY,
options
);
}
Has anyone encountered similar problems or an idea of what I made wrong?
Update 1
I tried changing the way I use the SyncStatusObserver
. I now get the information from the which
flag parameter like so:
private final SyncStatusObserver syncStatusObserver = new SyncStatusObserver() {
@Override
public void onStatusChanged(int which) {
boolean syncActive = (which & ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE) == ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE;
boolean syncPending = (which & ContentResolver.SYNC_OBSERVER_TYPE_PENDING) == ContentResolver.SYNC_OBSERVER_TYPE_PENDING;
boolean refreshing = syncActive || syncPending;
// update UI...
}
};
When I do this, the pending
state seems to be correct, so its returning false
as soon as the adapter is starting the sync process, but now the adapter stays active all the time and I have the same wrong result for boolean refreshing
as always. :/
triggerRefresh(SYNC.NEWS | SYNC.EVENTS)
.&
is not a logicaland
, its a bitwise operation. Consider this:EVENTS
equals0001
andNEWS
is equal to0010
.EVENTS | NEWS
would be0011
and0011 & 0001 = 0001
and0011 & 0010 = 0010
. Therefore both conditions are true and both news and events would be synced – Anoxemia