Android - how to get notified when sync finishes, when requesting a sync with ContentResolver.requestSync()
Asked Answered
N

4

5

I have my own ContentProvider and SyncAdapter which both work fine.

I set them to be automatically synced with ContentResolver.setSyncAutomatically() an this work. I can also test the sync with Dev Tools -> Sync Tester.

Now I would like to request a sync from my app (if we have no data yet) and be notified when it finishes, so I can updated the interface (I'm showing a progress bar with logo while it's syncing). I'm doing this with ContentResolver.requestSync(), but I have not found a way to get notified when the sync finishes.

Does anyone know how to do this? Thanks.

Nth answered 22/11, 2011 at 13:17 Comment(0)
S
4

Using addStatusChangeListener() will give you callbacks when the sync is SYNC_OBSERVER_TYPE_ACTIVE or SYNC_OBSERVER_TYPE_PENDING. It is well weird that there is no finished event.

Here is a workaround suggested by Felix. He suggests that you should ditch the ContentResolver in favour of Broadcasts.

Stogy answered 22/11, 2011 at 13:27 Comment(2)
Thanks, I used the workaround. Though this is a bit of a roundabout way of doing this. The SDK should really include a better way.Nth
@Nth I suppose you are syncing some kind of data between server and client. Each time there should be a server response which you can evaluate and sent a local Broadcast yourself.Decadence
C
7

addStatusChangeListener() does notify you when the sync finishes, it just does so in a slightly roundabout way: SyncStatusObserver.onStatusChanged() is called to notify you that the state changed. You must then call ContentResolver.isSyncPending() or ContentResolver.isSyncActive() to check the new state.

...
ContentResolver.addStatusChangeListener(
        ContentResolver.SYNC_OBSERVER_TYPE_PENDING
            | ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE,
        new MySyncStatusObserver());
...

private class MySyncStatusObserver implements SyncStatusObserver {
    @Override
    public void onStatusChanged(int which) {
        if (which == ContentResolver.SYNC_OBSERVER_TYPE_PENDING) {
            // 'Pending' state changed.
            if (ContentResolver.isSyncPending(mAccount, MY_AUTHORITY)) {
                // There is now a pending sync.
            } else {
                // There is no longer a pending sync.
            }
        } else if (which == ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE) {
            // 'Active' state changed.
            if (ContentResolver.isSyncActive(mAccount, MY_AUTHORITY)) {
                // There is now an active sync.
            } else {
                // There is no longer an active sync.
            }
        }
    }
}

One additional note: In my testing my onStatusChanged() method is called four times when I request a sync:

  1. pending is changed to true
  2. pending is changed to false
  3. active is changed to true
  4. active is changed to false

So it appears there is a window between pending and active where both are set to false even though an active sync is about to commence.

Castleman answered 19/12, 2014 at 23:9 Comment(0)
B
6

Here is a fully working code snippet with javadocs for anyone wanting a drop in solution rather than having to guess how to put everything together. It builds on Mark's answer above. Supports monitoring multiple account syncs.

import android.accounts.Account;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
/**
 * Sync status observer that reports back via a callback interface when syncing has begun
 * and finished.
 */
public static class MySyncStatusObserver implements SyncStatusObserver {
    /**
     * Defines the various sync states for an account.
     */
    private enum SyncState {
        /**
         * Indicates a sync is pending.
         */
        PENDING,
        /**
         * Indicates a sync is no longer pending but isn't active yet.
         */
        PENDING_ACTIVE,
        /**
         * Indicates a sync is active.
         */
        ACTIVE,
        /**
         * Indicates syncing is finished.
         */
        FINISHED
    }

    /**
     * Lifecycle events.
     */
    public interface Callback {
        /**
         * Indicates syncing of calendars has begun.
         */
        void onSyncsStarted();

        /**
         * Indicates syncing of calendars has finished.
         */
        void onSyncsFinished();
    }

    /**
     * The original list of accounts that are being synced.
     */
    @NonNull private final List<Account> mAccounts;
    /**
     * Map of accounts and their current sync states.
     */
    private final Map<Account, SyncState> mAccountSyncState =
            Collections.synchronizedMap(new HashMap<Account, SyncState>());

    /**
     * The calendar authority we're listening for syncs on.
     */
    @NonNull private final String mCalendarAuthority;
    /**
     * Callback implementation.
     */
    @Nullable private final Callback mCallback;

    /**
     * {@code true} when a "sync started" callback has been called.
     *
     * <p>Keeps us from reporting this event more than once.</p>
     */
    private boolean mSyncStartedReported;
    /**
     * Provider handle returned from
     * {@link ContentResolver#addStatusChangeListener(int, SyncStatusObserver)} used to
     * unregister for sync status changes.
     */
    @Nullable private Object mProviderHandle;

    /**
     * Default constructor.
     *
     * @param accounts the accounts to monitor syncing for
     * @param calendarAuthority the calendar authority for the syncs
     * @param callback optional callback interface to receive events
     */
    public MySyncStatusObserver(@NonNull final Account[] accounts,
            @NonNull final String calendarAuthority, @Nullable final Callback callback) {
        mAccounts = Lists.newArrayList(accounts);
        mCalendarAuthority = calendarAuthority;
        mCallback = callback;
    }

    /**
     * Sets the provider handle to unregister for sync status changes with.
     */
    public void setProviderHandle(@Nullable final Object providerHandle) {
        mProviderHandle = providerHandle;
    }

    @Override
    public void onStatusChanged(int which) {
        for (final Account account : mAccounts) {
            if (which == ContentResolver.SYNC_OBSERVER_TYPE_PENDING) {
                if (ContentResolver.isSyncPending(account, mCalendarAuthority)) {
                    // There is now a pending sync.
                    mAccountSyncState.put(account, SyncState.PENDING);
                } else {
                    // There is no longer a pending sync.
                    mAccountSyncState.put(account, SyncState.PENDING_ACTIVE);
                }
            } else if (which == ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE) {
                if (ContentResolver.isSyncActive(account, mCalendarAuthority)) {
                    // There is now an active sync.
                    mAccountSyncState.put(account, SyncState.ACTIVE);

                    if (!mSyncStartedReported && mCallback != null) {
                        mCallback.onSyncsStarted();
                        mSyncStartedReported = true;
                    }
                } else {
                    // There is no longer an active sync.
                    mAccountSyncState.put(account, SyncState.FINISHED);
                }
            }
        }

        // We haven't finished processing sync states for all accounts yet
        if (mAccounts.size() != mAccountSyncState.size()) return;

        // Check if any accounts are not finished syncing yet. If so bail
        for (final SyncState syncState : mAccountSyncState.values()) {
            if (syncState != SyncState.FINISHED) return;
        }

        // 1. Unregister for sync status changes
        if (mProviderHandle != null) {
            ContentResolver.removeStatusChangeListener(mProviderHandle);
        }

        // 2. Report back that all syncs are finished
        if (mCallback != null) {
            mCallback.onSyncsFinished();
        }
    }
}

Here is the implementation:

public class MyActivity extends Activity implements MySyncStatusObserver.Callback {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.some_layout);

        // Retrieve your accounts
        final Account[] accounts = AccountManager.get(this).getAccountsByType("your_account_type");

        // Register for sync status changes
        final MySyncStatusObserver observer = new MySyncStatusObserver(accounts, "the sync authority", this);
        final Object providerHandle = ContentResolver.addStatusChangeListener(
            ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE |
                    ContentResolver.SYNC_OBSERVER_TYPE_PENDING, observer);
        // Pass in the handle so the observer can unregister itself from events when finished.
        // You could optionally save this handle at the Activity level but I prefer to
        // encapsulate everything in the observer and let it handle everything
        observer.setProviderHandle(providerHandle);

        for (final Account account : accounts) {
            // Request the sync
            ContentResolver.requestSync(account, "the sync authority", null);
        }
    }

    @Override
    public void onSyncsStarted() {
        // Show a refresh indicator if you need
    }

    @Override
    public void onSyncsFinished() {
        // Hide the refresh indicator if you need
    }
}
Berkeleianism answered 6/6, 2015 at 4:39 Comment(1)
Thank you for the great example. With a few modifications I was able to make this work for my particular caseTrophoplasm
S
4

Using addStatusChangeListener() will give you callbacks when the sync is SYNC_OBSERVER_TYPE_ACTIVE or SYNC_OBSERVER_TYPE_PENDING. It is well weird that there is no finished event.

Here is a workaround suggested by Felix. He suggests that you should ditch the ContentResolver in favour of Broadcasts.

Stogy answered 22/11, 2011 at 13:27 Comment(2)
Thanks, I used the workaround. Though this is a bit of a roundabout way of doing this. The SDK should really include a better way.Nth
@Nth I suppose you are syncing some kind of data between server and client. Each time there should be a server response which you can evaluate and sent a local Broadcast yourself.Decadence
F
1

This thread is a bit older, but it brought me on the right path. So here's my solution in Kotlin:

in onCreateView:

    ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE or ContentResolver.SYNC_OBSERVER_TYPE_PENDING) {
        model.showSyncProgressIndicator.postValue(
            ContentResolver.getCurrentSyncs().isNotEmpty()
        )
    }

    model.showSyncProgressIndicator.observe(viewLifecycleOwner) {
        if(it)
            binding.listSyncProgressIndicator.visibility = View.VISIBLE
        else
            binding.listSyncProgressIndicator.visibility = View.GONE
    }

Setting the value in the model and using the observer is necessary due to some threading-issues if the UI-update is called directly from the listener. The model itself just has a line to define the variable for the live data:

    val showSyncProgressIndicator = MutableLiveData(false)

I hope this helps someonein the future! Cheers!

Flagman answered 3/12, 2021 at 18:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.