Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / sync / android / java / src / org / chromium / sync / notifier / SyncStatusHelper.java
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
51         public CachedAccountSyncSettings(String contractAuthority,
52                 SyncContentResolverDelegate contentResolverWrapper) {
53             mContractAuthority = contractAuthority;
54             mSyncContentResolverDelegate = contentResolverWrapper;
55         }
56
57         private void ensureSettingsAreForAccount(Account account) {
58             assert account != null;
59             if (account.equals(mAccount)) return;
60             updateSyncSettingsForAccount(account);
61             mDidUpdate = true;
62         }
63
64         public void clearUpdateStatus() {
65             mDidUpdate = false;
66         }
67
68         public boolean getDidUpdateStatus() {
69             return mDidUpdate;
70         }
71
72         // Calling this method may have side-effects.
73         public boolean getSyncAutomatically(Account account) {
74             ensureSettingsAreForAccount(account);
75             return mSyncAutomatically;
76         }
77
78         public void updateSyncSettingsForAccount(Account account) {
79             if (account == null) return;
80             updateSyncSettingsForAccountInternal(account);
81         }
82
83         public void setIsSyncable(Account account) {
84             ensureSettingsAreForAccount(account);
85             if (mIsSyncable == 1) return;
86             setIsSyncableInternal(account);
87         }
88
89         public void setSyncAutomatically(Account account, boolean value) {
90             ensureSettingsAreForAccount(account);
91             if (mSyncAutomatically == value) return;
92             setSyncAutomaticallyInternal(account, value);
93         }
94
95         @VisibleForTesting
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
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
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 {
166         public void syncSettingsChanged();
167     }
168
169     /**
170      * @param context the context
171      * @param syncContentResolverDelegate an implementation of {@link SyncContentResolverDelegate}.
172      */
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
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      */
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
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
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      */
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      */
260     public void registerSyncSettingsChangedObserver(SyncSettingsChangedObserver observer) {
261         mObservers.addObserver(observer);
262     }
263
264     /**
265      * Wrapper method for the ContentResolver.removeStatusChangeListener(...).
266      */
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      */
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      */
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      */
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      */
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      */
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      */
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      */
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
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      */
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
436     private boolean getAndClearDidUpdateStatus() {
437         boolean didGetStatusUpdate;
438         synchronized (mCachedSettings) {
439             didGetStatusUpdate = mCachedSettings.getDidUpdateStatus();
440             mCachedSettings.clearUpdateStatus();
441         }
442         return didGetStatusUpdate;
443     }
444
445     private void notifyObserversIfAccountSettingsChanged() {
446         if (getAndClearDidUpdateStatus()) {
447             notifyObservers();
448         }
449     }
450
451     private void notifyObservers() {
452         for (SyncSettingsChangedObserver observer : mObservers) {
453             observer.syncSettingsChanged();
454         }
455     }
456 }