1 // Copyright 2010 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 package org.chromium.sync.notifier;
7 import android.accounts.Account;
8 import android.content.ContentResolver;
9 import android.content.Context;
10 import android.content.SyncStatusObserver;
11 import android.os.StrictMode;
13 import org.chromium.base.ObserverList;
14 import org.chromium.base.VisibleForTesting;
15 import org.chromium.sync.signin.AccountManagerHelper;
16 import org.chromium.sync.signin.ChromeSigninController;
18 import javax.annotation.concurrent.NotThreadSafe;
19 import javax.annotation.concurrent.ThreadSafe;
22 * A helper class to handle the current status of sync for Chrome in Android settings.
24 * It also provides an observer to be used whenever Android sync settings change.
26 * To retrieve an instance of this class, call SyncStatusHelper.get(someContext).
28 * All new public methods MUST call notifyObservers at the end.
31 public class SyncStatusHelper {
34 * In-memory holder of the sync configurations for a given account. On each
35 * access, updates the cache if the account has changed. This lazy-updating
36 * model is appropriate as the account changes rarely but may not be known
37 * when initially constructed. So long as we keep a single account, no
38 * expensive calls to Android are made.
42 public static class CachedAccountSyncSettings {
43 private final String mContractAuthority;
44 private final SyncContentResolverDelegate mSyncContentResolverDelegate;
45 private Account mAccount;
46 private boolean mDidUpdate;
47 private boolean mSyncAutomatically;
48 private int mIsSyncable;
50 public CachedAccountSyncSettings(String contractAuthority,
51 SyncContentResolverDelegate contentResolverWrapper) {
52 mContractAuthority = contractAuthority;
53 mSyncContentResolverDelegate = contentResolverWrapper;
56 private void ensureSettingsAreForAccount(Account account) {
57 assert account != null;
58 if (account.equals(mAccount)) return;
59 updateSyncSettingsForAccount(account);
63 public void clearUpdateStatus() {
67 public boolean getDidUpdateStatus() {
71 // Calling this method may have side-effects.
72 public boolean getSyncAutomatically(Account account) {
73 ensureSettingsAreForAccount(account);
74 return mSyncAutomatically;
77 public void updateSyncSettingsForAccount(Account account) {
78 if (account == null) return;
79 updateSyncSettingsForAccountInternal(account);
82 public void setIsSyncable(Account account) {
83 ensureSettingsAreForAccount(account);
84 if (mIsSyncable == 1) return;
85 setIsSyncableInternal(account);
88 public void setSyncAutomatically(Account account, boolean value) {
89 ensureSettingsAreForAccount(account);
90 if (mSyncAutomatically == value) return;
91 setSyncAutomaticallyInternal(account, value);
95 protected void updateSyncSettingsForAccountInternal(Account account) {
96 // Null check here otherwise Findbugs complains.
97 if (account == null) return;
99 boolean oldSyncAutomatically = mSyncAutomatically;
100 int oldIsSyncable = mIsSyncable;
101 Account oldAccount = mAccount;
105 StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
106 mSyncAutomatically = mSyncContentResolverDelegate.getSyncAutomatically(
107 account, mContractAuthority);
108 mIsSyncable = mSyncContentResolverDelegate.getIsSyncable(account, mContractAuthority);
109 StrictMode.setThreadPolicy(oldPolicy);
110 mDidUpdate = (oldIsSyncable != mIsSyncable)
111 || (oldSyncAutomatically != mSyncAutomatically)
112 || (!account.equals(oldAccount));
116 protected void setIsSyncableInternal(Account account) {
118 StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
119 mSyncContentResolverDelegate.setIsSyncable(account, mContractAuthority, 1);
120 StrictMode.setThreadPolicy(oldPolicy);
125 protected void setSyncAutomaticallyInternal(Account account, boolean value) {
126 mSyncAutomatically = value;
127 StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
128 mSyncContentResolverDelegate.setSyncAutomatically(account, mContractAuthority, value);
129 StrictMode.setThreadPolicy(oldPolicy);
134 // This should always have the same value as GaiaConstants::kChromeSyncOAuth2Scope.
135 public static final String CHROME_SYNC_OAUTH2_SCOPE =
136 "https://www.googleapis.com/auth/chromesync";
138 public static final String TAG = "SyncStatusHelper";
141 * Lock for ensuring singleton instantiation across threads.
143 private static final Object INSTANCE_LOCK = new Object();
145 private static SyncStatusHelper sSyncStatusHelper;
147 private final String mContractAuthority;
149 private final Context mApplicationContext;
151 private final SyncContentResolverDelegate mSyncContentResolverDelegate;
153 private boolean mCachedMasterSyncAutomatically;
155 // Instantiation of SyncStatusHelper is guarded by a lock so volatile is unneeded.
156 private CachedAccountSyncSettings mCachedSettings;
158 private final ObserverList<SyncSettingsChangedObserver> mObservers =
159 new ObserverList<SyncSettingsChangedObserver>();
162 * Provides notifications when Android sync settings have changed.
164 public interface SyncSettingsChangedObserver {
165 public void syncSettingsChanged();
169 * @param context the context
170 * @param syncContentResolverDelegate an implementation of {@link SyncContentResolverDelegate}.
172 private SyncStatusHelper(Context context,
173 SyncContentResolverDelegate syncContentResolverDelegate,
174 CachedAccountSyncSettings cachedAccountSettings) {
175 mApplicationContext = context.getApplicationContext();
176 mSyncContentResolverDelegate = syncContentResolverDelegate;
177 mContractAuthority = getContractAuthority();
178 mCachedSettings = cachedAccountSettings;
180 updateMasterSyncAutomaticallySetting();
182 mSyncContentResolverDelegate.addStatusChangeListener(
183 ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS,
184 new AndroidSyncSettingsChangedObserver());
187 private void updateMasterSyncAutomaticallySetting() {
188 StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
189 synchronized (mCachedSettings) {
190 mCachedMasterSyncAutomatically = mSyncContentResolverDelegate
191 .getMasterSyncAutomatically();
193 StrictMode.setThreadPolicy(oldPolicy);
197 * A factory method for the SyncStatusHelper.
199 * It is possible to override the {@link SyncContentResolverDelegate} to use in tests for the
200 * instance of the SyncStatusHelper by calling overrideSyncStatusHelperForTests(...) with
201 * your {@link SyncContentResolverDelegate}.
203 * @param context the ApplicationContext is retrieved from the context used as an argument.
204 * @return a singleton instance of the SyncStatusHelper
206 public static SyncStatusHelper get(Context context) {
207 synchronized (INSTANCE_LOCK) {
208 if (sSyncStatusHelper == null) {
209 SyncContentResolverDelegate contentResolverDelegate =
210 new SystemSyncContentResolverDelegate();
211 CachedAccountSyncSettings cache = new CachedAccountSyncSettings(
212 context.getPackageName(), contentResolverDelegate);
213 sSyncStatusHelper = new SyncStatusHelper(context, contentResolverDelegate, cache);
216 return sSyncStatusHelper;
220 * Tests might want to consider overriding the context and {@link SyncContentResolverDelegate}
221 * so they do not use the real ContentResolver in Android.
223 * @param context the context to use
224 * @param syncContentResolverDelegate the {@link SyncContentResolverDelegate} to use
227 public static void overrideSyncStatusHelperForTests(Context context,
228 SyncContentResolverDelegate syncContentResolverDelegate,
229 CachedAccountSyncSettings cachedAccountSettings) {
230 synchronized (INSTANCE_LOCK) {
231 if (sSyncStatusHelper != null) {
232 throw new IllegalStateException("SyncStatusHelper already exists");
234 sSyncStatusHelper = new SyncStatusHelper(context, syncContentResolverDelegate,
235 cachedAccountSettings);
240 public static void overrideSyncStatusHelperForTests(Context context,
241 SyncContentResolverDelegate syncContentResolverDelegate) {
242 CachedAccountSyncSettings cachedAccountSettings = new CachedAccountSyncSettings(
243 context.getPackageName(), syncContentResolverDelegate);
244 overrideSyncStatusHelperForTests(context, syncContentResolverDelegate,
245 cachedAccountSettings);
249 * Returns the contract authority to use when requesting sync.
251 public String getContractAuthority() {
252 return mApplicationContext.getPackageName();
256 * Wrapper method for the ContentResolver.addStatusChangeListener(...) when we are only
257 * interested in the settings type.
259 public void registerSyncSettingsChangedObserver(SyncSettingsChangedObserver observer) {
260 mObservers.addObserver(observer);
264 * Wrapper method for the ContentResolver.removeStatusChangeListener(...).
266 public void unregisterSyncSettingsChangedObserver(SyncSettingsChangedObserver observer) {
267 mObservers.removeObserver(observer);
271 * Checks whether sync is currently enabled from Chrome for a given account.
273 * It checks both the master sync for the device, and Chrome sync setting for the given account.
275 * @param account the account to check if Chrome sync is enabled on.
276 * @return true if sync is on, false otherwise
278 public boolean isSyncEnabled(Account account) {
279 if (account == null) return false;
281 synchronized (mCachedSettings) {
282 returnValue = mCachedMasterSyncAutomatically
283 && mCachedSettings.getSyncAutomatically(account);
286 notifyObserversIfAccountSettingsChanged();
291 * Checks whether sync is currently enabled from Chrome for the currently signed in account.
293 * It checks both the master sync for the device, and Chrome sync setting for the given account.
294 * If no user is currently signed in it returns false.
296 * @return true if sync is on, false otherwise
298 public boolean isSyncEnabled() {
299 return isSyncEnabled(ChromeSigninController.get(mApplicationContext).getSignedInUser());
303 * Checks whether sync is currently enabled from Chrome for a given account.
305 * It checks only Chrome sync setting for the given account,
306 * and ignores the master sync setting.
308 * @param account the account to check if Chrome sync is enabled on.
309 * @return true if sync is on, false otherwise
311 public boolean isSyncEnabledForChrome(Account account) {
312 if (account == null) return false;
315 synchronized (mCachedSettings) {
316 returnValue = mCachedSettings.getSyncAutomatically(account);
319 notifyObserversIfAccountSettingsChanged();
324 * Checks whether the master sync flag for Android is currently set.
326 * @return true if the global master sync is on, false otherwise
328 public boolean isMasterSyncAutomaticallyEnabled() {
329 synchronized (mCachedSettings) {
330 return mCachedMasterSyncAutomatically;
335 * Make sure Chrome is syncable, and enable sync.
337 * @param account the account to enable sync on
339 public void enableAndroidSync(Account account) {
340 makeSyncable(account);
342 synchronized (mCachedSettings) {
343 mCachedSettings.setSyncAutomatically(account, true);
346 notifyObserversIfAccountSettingsChanged();
350 * Disables Android Chrome sync
352 * @param account the account to disable Chrome sync on
354 public void disableAndroidSync(Account account) {
355 synchronized (mCachedSettings) {
356 mCachedSettings.setSyncAutomatically(account, false);
359 notifyObserversIfAccountSettingsChanged();
363 * Register with Android Sync Manager. This is what causes the "Chrome" option to appear in
364 * Settings -> Accounts / Sync .
366 * @param account the account to enable Chrome sync on
368 private void makeSyncable(Account account) {
369 synchronized (mCachedSettings) {
370 mCachedSettings.setIsSyncable(account);
373 StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
374 // Disable the syncability of Chrome for all other accounts. Don't use
375 // our cache as we're touching many accounts that aren't signed in, so this saves
376 // extra calls to Android sync configuration.
377 Account[] googleAccounts = AccountManagerHelper.get(mApplicationContext)
378 .getGoogleAccounts();
379 for (Account accountToSetNotSyncable : googleAccounts) {
380 if (!accountToSetNotSyncable.equals(account)
381 && mSyncContentResolverDelegate.getIsSyncable(
382 accountToSetNotSyncable, mContractAuthority) > 0) {
383 mSyncContentResolverDelegate.setIsSyncable(accountToSetNotSyncable,
384 mContractAuthority, 0);
387 StrictMode.setThreadPolicy(oldPolicy);
391 * Helper class to be used by observers whenever sync settings change.
393 * To register the observer, call SyncStatusHelper.registerObserver(...).
395 private class AndroidSyncSettingsChangedObserver implements SyncStatusObserver {
397 public void onStatusChanged(int which) {
398 if (ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS == which) {
399 // Sync settings have changed; update our in-memory caches
400 synchronized (mCachedSettings) {
401 mCachedSettings.updateSyncSettingsForAccount(
402 ChromeSigninController.get(mApplicationContext).getSignedInUser());
405 boolean oldMasterSyncEnabled = isMasterSyncAutomaticallyEnabled();
406 updateMasterSyncAutomaticallySetting();
407 boolean didMasterSyncChanged =
408 oldMasterSyncEnabled != isMasterSyncAutomaticallyEnabled();
409 // Notify observers if MasterSync or account level settings change.
410 if (didMasterSyncChanged || getAndClearDidUpdateStatus())
416 private boolean getAndClearDidUpdateStatus() {
417 boolean didGetStatusUpdate;
418 synchronized (mCachedSettings) {
419 didGetStatusUpdate = mCachedSettings.getDidUpdateStatus();
420 mCachedSettings.clearUpdateStatus();
422 return didGetStatusUpdate;
425 private void notifyObserversIfAccountSettingsChanged() {
426 if (getAndClearDidUpdateStatus()) {
431 private void notifyObservers() {
432 for (SyncSettingsChangedObserver observer : mObservers) {
433 observer.syncSettingsChanged();