SyncAdapter always in pending state
Asked Answered
A

1

12

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. :/

Anoxemia answered 24/9, 2015 at 19:46 Comment(2)
Those ids are integer flags. That way i can fire up multiple syncs like so triggerRefresh(SYNC.NEWS | SYNC.EVENTS) . & is not a logical and, its a bitwise operation. Consider this: EVENTS equals 0001 and NEWS is equal to 0010. EVENTS | NEWS would be 0011 and 0011 & 0001 = 0001 and 0011 & 0010 = 0010. Therefore both conditions are true and both news and events would be syncedAnoxemia
Ah, ok never thought about that. Can you please what the Problem could be?Anoxemia
P
9

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;
    }
Penelopa answered 30/9, 2015 at 16:53 Comment(3)
Thank you so much for your answer. I'll give it a try as soon as i can!Anoxemia
This could be some kind of a work around, but doesn't this prevent the periodic syncs?Anoxemia
sure...in my case i used it to prevent the periodic syncs and trigger the process only explicity..Penelopa

© 2022 - 2024 — McMap. All rights reserved.