• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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