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. 4 5 package org.chromium.sync.notifier; 6 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; 12 13 import com.google.common.annotations.VisibleForTesting; 14 15 import org.chromium.base.ObserverList; 16 import org.chromium.sync.signin.AccountManagerHelper; 17 import org.chromium.sync.signin.ChromeSigninController; 18 19 import javax.annotation.concurrent.NotThreadSafe; 20 import javax.annotation.concurrent.ThreadSafe; 21 22 /** 23 * A helper class to handle the current status of sync for Chrome in Android settings. 24 * 25 * It also provides an observer to be used whenever Android sync settings change. 26 * 27 * To retrieve an instance of this class, call SyncStatusHelper.get(someContext). 28 * 29 * All new public methods MUST call notifyObservers at the end. 30 */ 31 @ThreadSafe 32 public class SyncStatusHelper { 33 34 /** 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. 40 */ 41 @NotThreadSafe 42 @VisibleForTesting 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; 50 CachedAccountSyncSettings(String contractAuthority, SyncContentResolverDelegate contentResolverWrapper)51 public CachedAccountSyncSettings(String contractAuthority, 52 SyncContentResolverDelegate contentResolverWrapper) { 53 mContractAuthority = contractAuthority; 54 mSyncContentResolverDelegate = contentResolverWrapper; 55 } 56 ensureSettingsAreForAccount(Account account)57 private void ensureSettingsAreForAccount(Account account) { 58 assert account != null; 59 if (account.equals(mAccount)) return; 60 updateSyncSettingsForAccount(account); 61 mDidUpdate = true; 62 } 63 clearUpdateStatus()64 public void clearUpdateStatus() { 65 mDidUpdate = false; 66 } 67 getDidUpdateStatus()68 public boolean getDidUpdateStatus() { 69 return mDidUpdate; 70 } 71 72 // Calling this method may have side-effects. getSyncAutomatically(Account account)73 public boolean getSyncAutomatically(Account account) { 74 ensureSettingsAreForAccount(account); 75 return mSyncAutomatically; 76 } 77 updateSyncSettingsForAccount(Account account)78 public void updateSyncSettingsForAccount(Account account) { 79 if (account == null) return; 80 updateSyncSettingsForAccountInternal(account); 81 } 82 setIsSyncable(Account account)83 public void setIsSyncable(Account account) { 84 ensureSettingsAreForAccount(account); 85 if (mIsSyncable == 1) return; 86 setIsSyncableInternal(account); 87 } 88 setSyncAutomatically(Account account, boolean value)89 public void setSyncAutomatically(Account account, boolean value) { 90 ensureSettingsAreForAccount(account); 91 if (mSyncAutomatically == value) return; 92 setSyncAutomaticallyInternal(account, value); 93 } 94 95 @VisibleForTesting updateSyncSettingsForAccountInternal(Account account)96 protected void updateSyncSettingsForAccountInternal(Account account) { 97 // Null check here otherwise Findbugs complains. 98 if (account == null) return; 99 100 boolean oldSyncAutomatically = mSyncAutomatically; 101 int oldIsSyncable = mIsSyncable; 102 Account oldAccount = mAccount; 103 104 mAccount = account; 105 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)); 114 } 115 116 @VisibleForTesting setIsSyncableInternal(Account account)117 protected void setIsSyncableInternal(Account account) { 118 mIsSyncable = 1; 119 StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads(); 120 mSyncContentResolverDelegate.setIsSyncable(account, mContractAuthority, 1); 121 StrictMode.setThreadPolicy(oldPolicy); 122 mDidUpdate = true; 123 } 124 125 @VisibleForTesting setSyncAutomaticallyInternal(Account account, boolean value)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); 131 mDidUpdate = true; 132 } 133 } 134 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"; 138 139 public static final String TAG = "SyncStatusHelper"; 140 141 /** 142 * Lock for ensuring singleton instantiation across threads. 143 */ 144 private static final Object INSTANCE_LOCK = new Object(); 145 146 private static SyncStatusHelper sSyncStatusHelper; 147 148 private final String mContractAuthority; 149 150 private final Context mApplicationContext; 151 152 private final SyncContentResolverDelegate mSyncContentResolverDelegate; 153 154 private boolean mCachedMasterSyncAutomatically; 155 156 // Instantiation of SyncStatusHelper is guarded by a lock so volatile is unneeded. 157 private CachedAccountSyncSettings mCachedSettings; 158 159 private final ObserverList<SyncSettingsChangedObserver> mObservers = 160 new ObserverList<SyncSettingsChangedObserver>(); 161 162 /** 163 * Provides notifications when Android sync settings have changed. 164 */ 165 public interface SyncSettingsChangedObserver { syncSettingsChanged()166 public void syncSettingsChanged(); 167 } 168 169 /** 170 * @param context the context 171 * @param syncContentResolverDelegate an implementation of {@link SyncContentResolverDelegate}. 172 */ SyncStatusHelper(Context context, SyncContentResolverDelegate syncContentResolverDelegate, CachedAccountSyncSettings cachedAccountSettings)173 private SyncStatusHelper(Context context, 174 SyncContentResolverDelegate syncContentResolverDelegate, 175 CachedAccountSyncSettings cachedAccountSettings) { 176 mApplicationContext = context.getApplicationContext(); 177 mSyncContentResolverDelegate = syncContentResolverDelegate; 178 mContractAuthority = getContractAuthority(); 179 mCachedSettings = cachedAccountSettings; 180 181 updateMasterSyncAutomaticallySetting(); 182 183 mSyncContentResolverDelegate.addStatusChangeListener( 184 ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, 185 new AndroidSyncSettingsChangedObserver()); 186 } 187 updateMasterSyncAutomaticallySetting()188 private void updateMasterSyncAutomaticallySetting() { 189 StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads(); 190 synchronized (mCachedSettings) { 191 mCachedMasterSyncAutomatically = mSyncContentResolverDelegate 192 .getMasterSyncAutomatically(); 193 } 194 StrictMode.setThreadPolicy(oldPolicy); 195 } 196 197 /** 198 * A factory method for the SyncStatusHelper. 199 * 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}. 203 * 204 * @param context the ApplicationContext is retrieved from the context used as an argument. 205 * @return a singleton instance of the SyncStatusHelper 206 */ get(Context context)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); 215 } 216 } 217 return sSyncStatusHelper; 218 } 219 220 /** 221 * Tests might want to consider overriding the context and {@link SyncContentResolverDelegate} 222 * so they do not use the real ContentResolver in Android. 223 * 224 * @param context the context to use 225 * @param syncContentResolverDelegate the {@link SyncContentResolverDelegate} to use 226 */ 227 @VisibleForTesting overrideSyncStatusHelperForTests(Context context, SyncContentResolverDelegate syncContentResolverDelegate, CachedAccountSyncSettings cachedAccountSettings)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"); 234 } 235 sSyncStatusHelper = new SyncStatusHelper(context, syncContentResolverDelegate, 236 cachedAccountSettings); 237 } 238 } 239 240 @VisibleForTesting overrideSyncStatusHelperForTests(Context context, SyncContentResolverDelegate syncContentResolverDelegate)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); 247 } 248 249 /** 250 * Returns the contract authority to use when requesting sync. 251 */ getContractAuthority()252 public String getContractAuthority() { 253 return mApplicationContext.getPackageName(); 254 } 255 256 /** 257 * Wrapper method for the ContentResolver.addStatusChangeListener(...) when we are only 258 * interested in the settings type. 259 */ registerSyncSettingsChangedObserver(SyncSettingsChangedObserver observer)260 public void registerSyncSettingsChangedObserver(SyncSettingsChangedObserver observer) { 261 mObservers.addObserver(observer); 262 } 263 264 /** 265 * Wrapper method for the ContentResolver.removeStatusChangeListener(...). 266 */ unregisterSyncSettingsChangedObserver(SyncSettingsChangedObserver observer)267 public void unregisterSyncSettingsChangedObserver(SyncSettingsChangedObserver observer) { 268 mObservers.removeObserver(observer); 269 } 270 271 /** 272 * Checks whether sync is currently enabled from Chrome for a given account. 273 * 274 * It checks both the master sync for the device, and Chrome sync setting for the given account. 275 * 276 * @param account the account to check if Chrome sync is enabled on. 277 * @return true if sync is on, false otherwise 278 */ isSyncEnabled(Account account)279 public boolean isSyncEnabled(Account account) { 280 if (account == null) return false; 281 boolean returnValue; 282 synchronized (mCachedSettings) { 283 returnValue = mCachedMasterSyncAutomatically && 284 mCachedSettings.getSyncAutomatically(account); 285 } 286 287 notifyObserversIfAccountSettingsChanged(); 288 return returnValue; 289 } 290 291 /** 292 * Checks whether sync is currently enabled from Chrome for the currently signed in account. 293 * 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. 296 * 297 * @return true if sync is on, false otherwise 298 */ isSyncEnabled()299 public boolean isSyncEnabled() { 300 return isSyncEnabled(ChromeSigninController.get(mApplicationContext).getSignedInUser()); 301 } 302 303 /** 304 * Checks whether sync is currently enabled from Chrome for a given account. 305 * 306 * It checks only Chrome sync setting for the given account, 307 * and ignores the master sync setting. 308 * 309 * @param account the account to check if Chrome sync is enabled on. 310 * @return true if sync is on, false otherwise 311 */ isSyncEnabledForChrome(Account account)312 public boolean isSyncEnabledForChrome(Account account) { 313 if (account == null) return false; 314 315 boolean returnValue; 316 synchronized (mCachedSettings) { 317 returnValue = mCachedSettings.getSyncAutomatically(account); 318 } 319 320 notifyObserversIfAccountSettingsChanged(); 321 return returnValue; 322 } 323 324 /** 325 * Checks whether the master sync flag for Android is currently set. 326 * 327 * @return true if the global master sync is on, false otherwise 328 */ isMasterSyncAutomaticallyEnabled()329 public boolean isMasterSyncAutomaticallyEnabled() { 330 synchronized (mCachedSettings) { 331 return mCachedMasterSyncAutomatically; 332 } 333 } 334 335 /** 336 * Make sure Chrome is syncable, and enable sync. 337 * 338 * @param account the account to enable sync on 339 */ enableAndroidSync(Account account)340 public void enableAndroidSync(Account account) { 341 makeSyncable(account); 342 343 synchronized (mCachedSettings) { 344 mCachedSettings.setSyncAutomatically(account, true); 345 } 346 347 notifyObserversIfAccountSettingsChanged(); 348 } 349 350 /** 351 * Disables Android Chrome sync 352 * 353 * @param account the account to disable Chrome sync on 354 */ disableAndroidSync(Account account)355 public void disableAndroidSync(Account account) { 356 synchronized (mCachedSettings) { 357 mCachedSettings.setSyncAutomatically(account, false); 358 } 359 360 notifyObserversIfAccountSettingsChanged(); 361 } 362 363 /** 364 * Register with Android Sync Manager. This is what causes the "Chrome" option to appear in 365 * Settings -> Accounts / Sync . 366 * 367 * @param account the account to enable Chrome sync on 368 */ makeSyncable(Account account)369 private void makeSyncable(Account account) { 370 synchronized (mCachedSettings) { 371 mCachedSettings.setIsSyncable(account); 372 } 373 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). 379 getGoogleAccounts(); 380 for (Account accountToSetNotSyncable : googleAccounts) { 381 if (!accountToSetNotSyncable.equals(account) && 382 mSyncContentResolverDelegate.getIsSyncable( 383 accountToSetNotSyncable, mContractAuthority) > 0) { 384 mSyncContentResolverDelegate.setIsSyncable(accountToSetNotSyncable, 385 mContractAuthority, 0); 386 } 387 } 388 StrictMode.setThreadPolicy(oldPolicy); 389 } 390 391 /** 392 * Helper class to be used by observers whenever sync settings change. 393 * 394 * To register the observer, call SyncStatusHelper.registerObserver(...). 395 */ 396 private class AndroidSyncSettingsChangedObserver implements SyncStatusObserver { 397 @Override onStatusChanged(int which)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()); 404 } 405 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()) 412 notifyObservers(); 413 } 414 } 415 } 416 417 /** 418 * Sets a new StrictMode.ThreadPolicy based on the current one, but allows disk reads 419 * and disk writes. 420 * 421 * The return value is the old policy, which must be applied after the disk access is finished, 422 * by using StrictMode.setThreadPolicy(oldPolicy). 423 * 424 * @return the policy before allowing reads and writes. 425 */ temporarilyAllowDiskWritesAndDiskReads()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()); 433 return oldPolicy; 434 } 435 getAndClearDidUpdateStatus()436 private boolean getAndClearDidUpdateStatus() { 437 boolean didGetStatusUpdate; 438 synchronized (mCachedSettings) { 439 didGetStatusUpdate = mCachedSettings.getDidUpdateStatus(); 440 mCachedSettings.clearUpdateStatus(); 441 } 442 return didGetStatusUpdate; 443 } 444 notifyObserversIfAccountSettingsChanged()445 private void notifyObserversIfAccountSettingsChanged() { 446 if (getAndClearDidUpdateStatus()) { 447 notifyObservers(); 448 } 449 } 450 notifyObservers()451 private void notifyObservers() { 452 for (SyncSettingsChangedObserver observer : mObservers) { 453 observer.syncSettingsChanged(); 454 } 455 } 456 } 457