root/sync/android/java/src/org/chromium/sync/notifier/SyncStatusHelper.java

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. ensureSettingsAreForAccount
  2. clearUpdateStatus
  3. getDidUpdateStatus
  4. getSyncAutomatically
  5. updateSyncSettingsForAccount
  6. setIsSyncable
  7. setSyncAutomatically
  8. updateSyncSettingsForAccountInternal
  9. setIsSyncableInternal
  10. setSyncAutomaticallyInternal
  11. syncSettingsChanged
  12. updateMasterSyncAutomaticallySetting
  13. get
  14. overrideSyncStatusHelperForTests
  15. overrideSyncStatusHelperForTests
  16. getContractAuthority
  17. registerSyncSettingsChangedObserver
  18. unregisterSyncSettingsChangedObserver
  19. isSyncEnabled
  20. isSyncEnabled
  21. isSyncEnabledForChrome
  22. isMasterSyncAutomaticallyEnabled
  23. enableAndroidSync
  24. disableAndroidSync
  25. makeSyncable
  26. onStatusChanged
  27. temporarilyAllowDiskWritesAndDiskReads
  28. getAndClearDidUpdateStatus
  29. notifyObserversIfAccountSettingsChanged
  30. notifyObservers

// Copyright 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.sync.notifier;

import android.accounts.Account;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SyncStatusObserver;
import android.os.StrictMode;

import com.google.common.annotations.VisibleForTesting;

import org.chromium.base.ObserverList;
import org.chromium.sync.signin.AccountManagerHelper;
import org.chromium.sync.signin.ChromeSigninController;

import javax.annotation.concurrent.NotThreadSafe;
import javax.annotation.concurrent.ThreadSafe;

/**
 * A helper class to handle the current status of sync for Chrome in Android settings.
 *
 * It also provides an observer to be used whenever Android sync settings change.
 *
 * To retrieve an instance of this class, call SyncStatusHelper.get(someContext).
 *
 * All new public methods MUST call notifyObservers at the end.
 */
@ThreadSafe
public class SyncStatusHelper {

    /**
     * In-memory holder of the sync configurations for a given account. On each
     * access, updates the cache if the account has changed. This lazy-updating
     * model is appropriate as the account changes rarely but may not be known
     * when initially constructed. So long as we keep a single account, no
     * expensive calls to Android are made.
     */
    @NotThreadSafe
    @VisibleForTesting
    public static class CachedAccountSyncSettings {
        private final String mContractAuthority;
        private final SyncContentResolverDelegate mSyncContentResolverDelegate;
        private Account mAccount;
        private boolean mDidUpdate;
        private boolean mSyncAutomatically;
        private int mIsSyncable;

        public CachedAccountSyncSettings(String contractAuthority,
                SyncContentResolverDelegate contentResolverWrapper) {
            mContractAuthority = contractAuthority;
            mSyncContentResolverDelegate = contentResolverWrapper;
        }

        private void ensureSettingsAreForAccount(Account account) {
            assert account != null;
            if (account.equals(mAccount)) return;
            updateSyncSettingsForAccount(account);
            mDidUpdate = true;
        }

        public void clearUpdateStatus() {
            mDidUpdate = false;
        }

        public boolean getDidUpdateStatus() {
            return mDidUpdate;
        }

        // Calling this method may have side-effects.
        public boolean getSyncAutomatically(Account account) {
            ensureSettingsAreForAccount(account);
            return mSyncAutomatically;
        }

        public void updateSyncSettingsForAccount(Account account) {
            if (account == null) return;
            updateSyncSettingsForAccountInternal(account);
        }

        public void setIsSyncable(Account account) {
            ensureSettingsAreForAccount(account);
            if (mIsSyncable == 1) return;
            setIsSyncableInternal(account);
        }

        public void setSyncAutomatically(Account account, boolean value) {
            ensureSettingsAreForAccount(account);
            if (mSyncAutomatically == value) return;
            setSyncAutomaticallyInternal(account, value);
        }

        @VisibleForTesting
        protected void updateSyncSettingsForAccountInternal(Account account) {
            // Null check here otherwise Findbugs complains.
            if (account == null) return;

            boolean oldSyncAutomatically = mSyncAutomatically;
            int oldIsSyncable = mIsSyncable;
            Account oldAccount = mAccount;

            mAccount = account;

            StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads();
            mSyncAutomatically = mSyncContentResolverDelegate.getSyncAutomatically(
                    account, mContractAuthority);
            mIsSyncable = mSyncContentResolverDelegate.getIsSyncable(account, mContractAuthority);
            StrictMode.setThreadPolicy(oldPolicy);
            mDidUpdate = (oldIsSyncable != mIsSyncable)
                || (oldSyncAutomatically != mSyncAutomatically)
                || (!account.equals(oldAccount));
        }

        @VisibleForTesting
        protected void setIsSyncableInternal(Account account) {
            mIsSyncable = 1;
            StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads();
            mSyncContentResolverDelegate.setIsSyncable(account, mContractAuthority, 1);
            StrictMode.setThreadPolicy(oldPolicy);
            mDidUpdate = true;
        }

        @VisibleForTesting
        protected void setSyncAutomaticallyInternal(Account account, boolean value) {
            mSyncAutomatically = value;
            StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads();
            mSyncContentResolverDelegate.setSyncAutomatically(account, mContractAuthority, value);
            StrictMode.setThreadPolicy(oldPolicy);
            mDidUpdate = true;
        }
    }

    // This should always have the same value as GaiaConstants::kChromeSyncOAuth2Scope.
    public static final String CHROME_SYNC_OAUTH2_SCOPE =
            "https://www.googleapis.com/auth/chromesync";

    public static final String TAG = "SyncStatusHelper";

    /**
     * Lock for ensuring singleton instantiation across threads.
     */
    private static final Object INSTANCE_LOCK = new Object();

    private static SyncStatusHelper sSyncStatusHelper;

    private final String mContractAuthority;

    private final Context mApplicationContext;

    private final SyncContentResolverDelegate mSyncContentResolverDelegate;

    private boolean mCachedMasterSyncAutomatically;

    // Instantiation of SyncStatusHelper is guarded by a lock so volatile is unneeded.
    private CachedAccountSyncSettings mCachedSettings;

    private final ObserverList<SyncSettingsChangedObserver> mObservers =
            new ObserverList<SyncSettingsChangedObserver>();

    /**
     * Provides notifications when Android sync settings have changed.
     */
    public interface SyncSettingsChangedObserver {
        public void syncSettingsChanged();
    }

    /**
     * @param context the context
     * @param syncContentResolverDelegate an implementation of {@link SyncContentResolverDelegate}.
     */
    private SyncStatusHelper(Context context,
            SyncContentResolverDelegate syncContentResolverDelegate,
            CachedAccountSyncSettings cachedAccountSettings) {
        mApplicationContext = context.getApplicationContext();
        mSyncContentResolverDelegate = syncContentResolverDelegate;
        mContractAuthority = getContractAuthority();
        mCachedSettings = cachedAccountSettings;

        updateMasterSyncAutomaticallySetting();

        mSyncContentResolverDelegate.addStatusChangeListener(
                ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS,
                new AndroidSyncSettingsChangedObserver());
    }

    private void updateMasterSyncAutomaticallySetting() {
        StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads();
        synchronized (mCachedSettings) {
            mCachedMasterSyncAutomatically = mSyncContentResolverDelegate
                    .getMasterSyncAutomatically();
        }
        StrictMode.setThreadPolicy(oldPolicy);
    }

    /**
     * A factory method for the SyncStatusHelper.
     *
     * It is possible to override the {@link SyncContentResolverDelegate} to use in tests for the
     * instance of the SyncStatusHelper by calling overrideSyncStatusHelperForTests(...) with
     * your {@link SyncContentResolverDelegate}.
     *
     * @param context the ApplicationContext is retrieved from the context used as an argument.
     * @return a singleton instance of the SyncStatusHelper
     */
    public static SyncStatusHelper get(Context context) {
        synchronized (INSTANCE_LOCK) {
            if (sSyncStatusHelper == null) {
                SyncContentResolverDelegate contentResolverDelegate =
                        new SystemSyncContentResolverDelegate();
                CachedAccountSyncSettings cache = new CachedAccountSyncSettings(
                        context.getPackageName(), contentResolverDelegate);
                sSyncStatusHelper = new SyncStatusHelper(context, contentResolverDelegate, cache);
            }
        }
        return sSyncStatusHelper;
    }

    /**
     * Tests might want to consider overriding the context and {@link SyncContentResolverDelegate}
     * so they do not use the real ContentResolver in Android.
     *
     * @param context the context to use
     * @param syncContentResolverDelegate the {@link SyncContentResolverDelegate} to use
     */
    @VisibleForTesting
    public static void overrideSyncStatusHelperForTests(Context context,
            SyncContentResolverDelegate syncContentResolverDelegate,
            CachedAccountSyncSettings cachedAccountSettings) {
        synchronized (INSTANCE_LOCK) {
            if (sSyncStatusHelper != null) {
                throw new IllegalStateException("SyncStatusHelper already exists");
            }
            sSyncStatusHelper = new SyncStatusHelper(context, syncContentResolverDelegate,
                    cachedAccountSettings);
        }
    }

    @VisibleForTesting
    public static void overrideSyncStatusHelperForTests(Context context,
            SyncContentResolverDelegate syncContentResolverDelegate) {
        CachedAccountSyncSettings cachedAccountSettings = new CachedAccountSyncSettings(
                context.getPackageName(), syncContentResolverDelegate);
        overrideSyncStatusHelperForTests(context, syncContentResolverDelegate,
                cachedAccountSettings);
    }

    /**
     * Returns the contract authority to use when requesting sync.
     */
    public String getContractAuthority() {
        return mApplicationContext.getPackageName();
    }

    /**
     * Wrapper method for the ContentResolver.addStatusChangeListener(...) when we are only
     * interested in the settings type.
     */
    public void registerSyncSettingsChangedObserver(SyncSettingsChangedObserver observer) {
        mObservers.addObserver(observer);
    }

    /**
     * Wrapper method for the ContentResolver.removeStatusChangeListener(...).
     */
    public void unregisterSyncSettingsChangedObserver(SyncSettingsChangedObserver observer) {
        mObservers.removeObserver(observer);
    }

    /**
     * Checks whether sync is currently enabled from Chrome for a given account.
     *
     * It checks both the master sync for the device, and Chrome sync setting for the given account.
     *
     * @param account the account to check if Chrome sync is enabled on.
     * @return true if sync is on, false otherwise
     */
    public boolean isSyncEnabled(Account account) {
        if (account == null) return false;
        boolean returnValue;
        synchronized (mCachedSettings) {
            returnValue = mCachedMasterSyncAutomatically &&
                mCachedSettings.getSyncAutomatically(account);
        }

        notifyObserversIfAccountSettingsChanged();
        return returnValue;
    }

    /**
     * Checks whether sync is currently enabled from Chrome for the currently signed in account.
     *
     * It checks both the master sync for the device, and Chrome sync setting for the given account.
     * If no user is currently signed in it returns false.
     *
     * @return true if sync is on, false otherwise
     */
    public boolean isSyncEnabled() {
        return isSyncEnabled(ChromeSigninController.get(mApplicationContext).getSignedInUser());
    }

    /**
     * Checks whether sync is currently enabled from Chrome for a given account.
     *
     * It checks only Chrome sync setting for the given account,
     * and ignores the master sync setting.
     *
     * @param account the account to check if Chrome sync is enabled on.
     * @return true if sync is on, false otherwise
     */
    public boolean isSyncEnabledForChrome(Account account) {
        if (account == null) return false;

        boolean returnValue;
        synchronized (mCachedSettings) {
            returnValue = mCachedSettings.getSyncAutomatically(account);
        }

        notifyObserversIfAccountSettingsChanged();
        return returnValue;
    }

    /**
     * Checks whether the master sync flag for Android is currently set.
     *
     * @return true if the global master sync is on, false otherwise
     */
    public boolean isMasterSyncAutomaticallyEnabled() {
        synchronized (mCachedSettings) {
            return mCachedMasterSyncAutomatically;
        }
    }

    /**
     * Make sure Chrome is syncable, and enable sync.
     *
     * @param account the account to enable sync on
     */
    public void enableAndroidSync(Account account) {
        makeSyncable(account);

        synchronized (mCachedSettings) {
            mCachedSettings.setSyncAutomatically(account, true);
        }

        notifyObserversIfAccountSettingsChanged();
    }

    /**
     * Disables Android Chrome sync
     *
     * @param account the account to disable Chrome sync on
     */
    public void disableAndroidSync(Account account) {
        synchronized (mCachedSettings) {
            mCachedSettings.setSyncAutomatically(account, false);
        }

        notifyObserversIfAccountSettingsChanged();
    }

    /**
     * Register with Android Sync Manager. This is what causes the "Chrome" option to appear in
     * Settings -> Accounts / Sync .
     *
     * @param account the account to enable Chrome sync on
     */
    private void makeSyncable(Account account) {
        synchronized (mCachedSettings) {
            mCachedSettings.setIsSyncable(account);
        }

        StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads();
        // Disable the syncability of Chrome for all other accounts. Don't use
        // our cache as we're touching many accounts that aren't signed in, so this saves
        // extra calls to Android sync configuration.
        Account[] googleAccounts = AccountManagerHelper.get(mApplicationContext).
                getGoogleAccounts();
        for (Account accountToSetNotSyncable : googleAccounts) {
            if (!accountToSetNotSyncable.equals(account) &&
                    mSyncContentResolverDelegate.getIsSyncable(
                            accountToSetNotSyncable, mContractAuthority) > 0) {
                mSyncContentResolverDelegate.setIsSyncable(accountToSetNotSyncable,
                        mContractAuthority, 0);
            }
        }
        StrictMode.setThreadPolicy(oldPolicy);
    }

    /**
     * Helper class to be used by observers whenever sync settings change.
     *
     * To register the observer, call SyncStatusHelper.registerObserver(...).
     */
    private class AndroidSyncSettingsChangedObserver implements SyncStatusObserver {
        @Override
        public void onStatusChanged(int which) {
            if (ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS == which) {
                // Sync settings have changed; update our in-memory caches
                synchronized (mCachedSettings) {
                    mCachedSettings.updateSyncSettingsForAccount(
                            ChromeSigninController.get(mApplicationContext).getSignedInUser());
                }

                boolean oldMasterSyncEnabled = isMasterSyncAutomaticallyEnabled();
                updateMasterSyncAutomaticallySetting();
                boolean didMasterSyncChanged =
                        oldMasterSyncEnabled != isMasterSyncAutomaticallyEnabled();
                // Notify observers if MasterSync or account level settings change.
                if (didMasterSyncChanged || getAndClearDidUpdateStatus())
                    notifyObservers();
            }
        }
    }

    /**
     * Sets a new StrictMode.ThreadPolicy based on the current one, but allows disk reads
     * and disk writes.
     *
     * The return value is the old policy, which must be applied after the disk access is finished,
     * by using StrictMode.setThreadPolicy(oldPolicy).
     *
     * @return the policy before allowing reads and writes.
     */
    private static StrictMode.ThreadPolicy temporarilyAllowDiskWritesAndDiskReads() {
        StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
        StrictMode.ThreadPolicy.Builder newPolicy =
                new StrictMode.ThreadPolicy.Builder(oldPolicy);
        newPolicy.permitDiskReads();
        newPolicy.permitDiskWrites();
        StrictMode.setThreadPolicy(newPolicy.build());
        return oldPolicy;
    }

    private boolean getAndClearDidUpdateStatus() {
        boolean didGetStatusUpdate;
        synchronized (mCachedSettings) {
            didGetStatusUpdate = mCachedSettings.getDidUpdateStatus();
            mCachedSettings.clearUpdateStatus();
        }
        return didGetStatusUpdate;
    }

    private void notifyObserversIfAccountSettingsChanged() {
        if (getAndClearDidUpdateStatus()) {
            notifyObservers();
        }
    }

    private void notifyObservers() {
        for (SyncSettingsChangedObserver observer : mObservers) {
            observer.syncSettingsChanged();
        }
    }
}

/* [<][>][^][v][top][bottom][index][help] */