Reputation: 719
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 :(
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);
}
});
}
};
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
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.
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?
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. :/
Upvotes: 12
Views: 1462
Reputation: 2393
i haved faced a similar issue with my syncAdapter...what i did to solve it was to turn auto sync off...since you explicity trigger the sync process you could delete the existing account set the Content Resolver's setSyncAutomatically to false and run the adapter again...
ContentResolver.setSyncAutomatically(account, CONTENT_AUTHORITY, false);
i also post the SyncAdapter's status changing callbacks where the state of the syncing process is logged
private SyncStatusObserver mSyncStatusObserver = new SyncStatusObserver() {
@Override
public void onStatusChanged(int which) {
Log.e("TAG", "Sync Status " + which);
runOnUiThread(new Runnable() {
@Override
public void run() {
Account account = GenericAccountService.getAccount(SyncUtils.ACCOUNT_TYPE);
if (account == null) {
// GetAccount() returned an invalid value. This shouldn't happen, but
// we'll set the status to "not refreshing".
//setRefreshActionButtonState(false);
return;
}
// Test the ContentResolver to see if the sync adapter is active or pending.
// Set the state of the refresh button accordingly.
boolean syncActive = ContentResolver.isSyncActive(
account, PlacesProvider.PROVIDER_NAME);
boolean syncPending = ContentResolver.isSyncPending(
account, PlacesProvider.PROVIDER_NAME);
Log.e("TAG", "SYNC PENDING " + syncPending);
if (!syncActive && !syncPending){
Log.e("TAG", "Sync is finished");
//if (syncProgressDialog != null) syncProgressDialog.dismiss();
// progressDialog.hide();
}
else {
}
//setRefreshActionButtonState(syncActive || syncPending);
}
});
}
};
in onResume i register the SyncStatusObserver
mSyncStatusObserver.onStatusChanged(0);
final int mask = ContentResolver.SYNC_OBSERVER_TYPE_PENDING |
ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE;
mSyncObserverHandle = ContentResolver.addStatusChangeListener(mask, mSyncStatusObserver);
and in onStop i remove the listener
if (mSyncObserverHandle != null) {
ContentResolver.removeStatusChangeListener(mSyncObserverHandle);
mSyncObserverHandle = null;
mSyncStatusObserver = null;
}
Upvotes: 9