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 com.google.common.annotations.VisibleForTesting;
15 import org.chromium.base.ObserverList;
16 import org.chromium.sync.signin.AccountManagerHelper;
17 import org.chromium.sync.signin.ChromeSigninController;
19 import javax.annotation.concurrent.NotThreadSafe;
20 import javax.annotation.concurrent.ThreadSafe;
23 * A helper class to handle the current status of sync for Chrome in Android settings.
25 * It also provides an observer to be used whenever Android sync settings change.
27 * To retrieve an instance of this class, call SyncStatusHelper.get(someContext).
29 * All new public methods MUST call notifyObservers at the end.
32 public class SyncStatusHelper {
35 * In-memory holder of the sync configurations for a given account. On each
36 * access, updates the cache if the account has changed. This lazy-updating
37 * model is appropriate as the account changes rarely but may not be known
38 * when initially constructed. So long as we keep a single account, no
39 * expensive calls to Android are made.
43 public static class CachedAccountSyncSettings {
44 private final String mContractAuthority;
45 private final SyncContentResolverDelegate mSyncContentResolverDelegate;
46 private Account mAccount;
47 private boolean mDidUpdate;
48 private boolean mSyncAutomatically;
49 private int mIsSyncable;
51 public CachedAccountSyncSettings(String contractAuthority,
52 SyncContentResolverDelegate contentResolverWrapper) {
53 mContractAuthority = contractAuthority;
54 mSyncContentResolverDelegate = contentResolverWrapper;
57 private void ensureSettingsAreForAccount(Account account) {
58 assert account != null;
59 if (account.equals(mAccount)) return;
60 updateSyncSettingsForAccount(account);
64 public void clearUpdateStatus() {
68 public boolean getDidUpdateStatus() {
72 // Calling this method may have side-effects.
73 public boolean getSyncAutomatically(Account account) {
74 ensureSettingsAreForAccount(account);
75 return mSyncAutomatically;
78 public void updateSyncSettingsForAccount(Account account) {
79 if (account == null) return;
80 updateSyncSettingsForAccountInternal(account);
83 public void setIsSyncable(Account account) {
84 ensureSettingsAreForAccount(account);
85 if (mIsSyncable == 1) return;
86 setIsSyncableInternal(account);
89 public void setSyncAutomatically(Account account, boolean value) {
90 ensureSettingsAreForAccount(account);
91 if (mSyncAutomatically == value) return;
92 setSyncAutomaticallyInternal(account, value);
96 protected void updateSyncSettingsForAccountInternal(Account account) {
97 // Null check here otherwise Findbugs complains.
98 if (account == null) return;
100 boolean oldSyncAutomatically = mSyncAutomatically;
101 int oldIsSyncable = mIsSyncable;
102 Account oldAccount = mAccount;
106 StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads();
107 mSyncAutomatically = mSyncContentResolverDelegate.getSyncAutomatically(
108 account, mContractAuthority);
109 mIsSyncable = mSyncContentResolverDelegate.getIsSyncable(account, mContractAuthority);
110 StrictMode.setThreadPolicy(oldPolicy);
111 mDidUpdate = (oldIsSyncable != mIsSyncable)
112 || (oldSyncAutomatically != mSyncAutomatically)
113 || (!account.equals(oldAccount));
117 protected void setIsSyncableInternal(Account account) {
119 StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads();
120 mSyncContentResolverDelegate.setIsSyncable(account, mContractAuthority, 1);
121 StrictMode.setThreadPolicy(oldPolicy);
126 protected void setSyncAutomaticallyInternal(Account account, boolean value) {
127 mSyncAutomatically = value;
128 StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads();
129 mSyncContentResolverDelegate.setSyncAutomatically(account, mContractAuthority, value);
130 StrictMode.setThreadPolicy(oldPolicy);
135 // This should always have the same value as GaiaConstants::kChromeSyncOAuth2Scope.
136 public static final String CHROME_SYNC_OAUTH2_SCOPE =
137 "https://www.googleapis.com/auth/chromesync";
139 public static final String TAG = "SyncStatusHelper";
142 * Lock for ensuring singleton instantiation across threads.
144 private static final Object INSTANCE_LOCK = new Object();
146 private static SyncStatusHelper sSyncStatusHelper;
148 private final String mContractAuthority;
150 private final Context mApplicationContext;
152 private final SyncContentResolverDelegate mSyncContentResolverDelegate;
154 private boolean mCachedMasterSyncAutomatically;
156 // Instantiation of SyncStatusHelper is guarded by a lock so volatile is unneeded.
157 private CachedAccountSyncSettings mCachedSettings;
159 private final ObserverList<SyncSettingsChangedObserver> mObservers =
160 new ObserverList<SyncSettingsChangedObserver>();
163 * Provides notifications when Android sync settings have changed.
165 public interface SyncSettingsChangedObserver {
166 public void syncSettingsChanged();
170 * @param context the context
171 * @param syncContentResolverDelegate an implementation of {@link SyncContentResolverDelegate}.
173 private SyncStatusHelper(Context context,
174 SyncContentResolverDelegate syncContentResolverDelegate,
175 CachedAccountSyncSettings cachedAccountSettings) {
176 mApplicationContext = context.getApplicationContext();
177 mSyncContentResolverDelegate = syncContentResolverDelegate;
178 mContractAuthority = getContractAuthority();
179 mCachedSettings = cachedAccountSettings;
181 updateMasterSyncAutomaticallySetting();
183 mSyncContentResolverDelegate.addStatusChangeListener(
184 ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS,
185 new AndroidSyncSettingsChangedObserver());
188 private void updateMasterSyncAutomaticallySetting() {
189 StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads();
190 synchronized (mCachedSettings) {
191 mCachedMasterSyncAutomatically = mSyncContentResolverDelegate
192 .getMasterSyncAutomatically();
194 StrictMode.setThreadPolicy(oldPolicy);
198 * A factory method for the SyncStatusHelper.
200 * It is possible to override the {@link SyncContentResolverDelegate} to use in tests for the
201 * instance of the SyncStatusHelper by calling overrideSyncStatusHelperForTests(...) with
202 * your {@link SyncContentResolverDelegate}.
204 * @param context the ApplicationContext is retrieved from the context used as an argument.
205 * @return a singleton instance of the SyncStatusHelper
207 public static SyncStatusHelper get(Context context) {
208 synchronized (INSTANCE_LOCK) {
209 if (sSyncStatusHelper == null) {
210 SyncContentResolverDelegate contentResolverDelegate =
211 new SystemSyncContentResolverDelegate();
212 CachedAccountSyncSettings cache = new CachedAccountSyncSettings(
213 context.getPackageName(), contentResolverDelegate);
214 sSyncStatusHelper = new SyncStatusHelper(context, contentResolverDelegate, cache);
217 return sSyncStatusHelper;
221 * Tests might want to consider overriding the context and {@link SyncContentResolverDelegate}
222 * so they do not use the real ContentResolver in Android.
224 * @param context the context to use
225 * @param syncContentResolverDelegate the {@link SyncContentResolverDelegate} to use
228 public static void overrideSyncStatusHelperForTests(Context context,
229 SyncContentResolverDelegate syncContentResolverDelegate,
230 CachedAccountSyncSettings cachedAccountSettings) {
231 synchronized (INSTANCE_LOCK) {
232 if (sSyncStatusHelper != null) {
233 throw new IllegalStateException("SyncStatusHelper already exists");
235 sSyncStatusHelper = new SyncStatusHelper(context, syncContentResolverDelegate,
236 cachedAccountSettings);
241 public static void overrideSyncStatusHelperForTests(Context context,
242 SyncContentResolverDelegate syncContentResolverDelegate) {
243 CachedAccountSyncSettings cachedAccountSettings = new CachedAccountSyncSettings(
244 context.getPackageName(), syncContentResolverDelegate);
245 overrideSyncStatusHelperForTests(context, syncContentResolverDelegate,
246 cachedAccountSettings);
250 * Returns the contract authority to use when requesting sync.
252 public String getContractAuthority() {
253 return mApplicationContext.getPackageName();
257 * Wrapper method for the ContentResolver.addStatusChangeListener(...) when we are only
258 * interested in the settings type.
260 public void registerSyncSettingsChangedObserver(SyncSettingsChangedObserver observer) {
261 mObservers.addObserver(observer);
265 * Wrapper method for the ContentResolver.removeStatusChangeListener(...).
267 public void unregisterSyncSettingsChangedObserver(SyncSettingsChangedObserver observer) {
268 mObservers.removeObserver(observer);
272 * Checks whether sync is currently enabled from Chrome for a given account.
274 * It checks both the master sync for the device, and Chrome sync setting for the given account.
276 * @param account the account to check if Chrome sync is enabled on.
277 * @return true if sync is on, false otherwise
279 public boolean isSyncEnabled(Account account) {
280 if (account == null) return false;
282 synchronized (mCachedSettings) {
283 returnValue = mCachedMasterSyncAutomatically &&
284 mCachedSettings.getSyncAutomatically(account);
287 notifyObserversIfAccountSettingsChanged();
292 * Checks whether sync is currently enabled from Chrome for the currently signed in account.
294 * It checks both the master sync for the device, and Chrome sync setting for the given account.
295 * If no user is currently signed in it returns false.
297 * @return true if sync is on, false otherwise
299 public boolean isSyncEnabled() {
300 return isSyncEnabled(ChromeSigninController.get(mApplicationContext).getSignedInUser());
304 * Checks whether sync is currently enabled from Chrome for a given account.
306 * It checks only Chrome sync setting for the given account,
307 * and ignores the master sync setting.
309 * @param account the account to check if Chrome sync is enabled on.
310 * @return true if sync is on, false otherwise
312 public boolean isSyncEnabledForChrome(Account account) {
313 if (account == null) return false;
316 synchronized (mCachedSettings) {
317 returnValue = mCachedSettings.getSyncAutomatically(account);
320 notifyObserversIfAccountSettingsChanged();
325 * Checks whether the master sync flag for Android is currently set.
327 * @return true if the global master sync is on, false otherwise
329 public boolean isMasterSyncAutomaticallyEnabled() {
330 synchronized (mCachedSettings) {
331 return mCachedMasterSyncAutomatically;
336 * Make sure Chrome is syncable, and enable sync.
338 * @param account the account to enable sync on
340 public void enableAndroidSync(Account account) {
341 makeSyncable(account);
343 synchronized (mCachedSettings) {
344 mCachedSettings.setSyncAutomatically(account, true);
347 notifyObserversIfAccountSettingsChanged();
351 * Disables Android Chrome sync
353 * @param account the account to disable Chrome sync on
355 public void disableAndroidSync(Account account) {
356 synchronized (mCachedSettings) {
357 mCachedSettings.setSyncAutomatically(account, false);
360 notifyObserversIfAccountSettingsChanged();
364 * Register with Android Sync Manager. This is what causes the "Chrome" option to appear in
365 * Settings -> Accounts / Sync .
367 * @param account the account to enable Chrome sync on
369 private void makeSyncable(Account account) {
370 synchronized (mCachedSettings) {
371 mCachedSettings.setIsSyncable(account);
374 StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads();
375 // Disable the syncability of Chrome for all other accounts. Don't use
376 // our cache as we're touching many accounts that aren't signed in, so this saves
377 // extra calls to Android sync configuration.
378 Account[] googleAccounts = AccountManagerHelper.get(mApplicationContext).
380 for (Account accountToSetNotSyncable : googleAccounts) {
381 if (!accountToSetNotSyncable.equals(account) &&
382 mSyncContentResolverDelegate.getIsSyncable(
383 accountToSetNotSyncable, mContractAuthority) > 0) {
384 mSyncContentResolverDelegate.setIsSyncable(accountToSetNotSyncable,
385 mContractAuthority, 0);
388 StrictMode.setThreadPolicy(oldPolicy);
392 * Helper class to be used by observers whenever sync settings change.
394 * To register the observer, call SyncStatusHelper.registerObserver(...).
396 private class AndroidSyncSettingsChangedObserver implements SyncStatusObserver {
398 public void onStatusChanged(int which) {
399 if (ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS == which) {
400 // Sync settings have changed; update our in-memory caches
401 synchronized (mCachedSettings) {
402 mCachedSettings.updateSyncSettingsForAccount(
403 ChromeSigninController.get(mApplicationContext).getSignedInUser());
406 boolean oldMasterSyncEnabled = isMasterSyncAutomaticallyEnabled();
407 updateMasterSyncAutomaticallySetting();
408 boolean didMasterSyncChanged =
409 oldMasterSyncEnabled != isMasterSyncAutomaticallyEnabled();
410 // Notify observers if MasterSync or account level settings change.
411 if (didMasterSyncChanged || getAndClearDidUpdateStatus())
418 * Sets a new StrictMode.ThreadPolicy based on the current one, but allows disk reads
421 * The return value is the old policy, which must be applied after the disk access is finished,
422 * by using StrictMode.setThreadPolicy(oldPolicy).
424 * @return the policy before allowing reads and writes.
426 private static StrictMode.ThreadPolicy temporarilyAllowDiskWritesAndDiskReads() {
427 StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
428 StrictMode.ThreadPolicy.Builder newPolicy =
429 new StrictMode.ThreadPolicy.Builder(oldPolicy);
430 newPolicy.permitDiskReads();
431 newPolicy.permitDiskWrites();
432 StrictMode.setThreadPolicy(newPolicy.build());
436 private boolean getAndClearDidUpdateStatus() {
437 boolean didGetStatusUpdate;
438 synchronized (mCachedSettings) {
439 didGetStatusUpdate = mCachedSettings.getDidUpdateStatus();
440 mCachedSettings.clearUpdateStatus();
442 return didGetStatusUpdate;
445 private void notifyObserversIfAccountSettingsChanged() {
446 if (getAndClearDidUpdateStatus()) {
451 private void notifyObservers() {
452 for (SyncSettingsChangedObserver observer : mObservers) {
453 observer.syncSettingsChanged();