- add sources.
[platform/framework/web/crosswalk.git] / src / sync / android / java / src / org / chromium / sync / notifier / SyncStatusHelper.java
1 // Copyright (c) 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 mSyncContentResolverWrapper;
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             mSyncContentResolverWrapper = 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 = mSyncContentResolverWrapper.getSyncAutomatically(
108                     account, mContractAuthority);
109             mIsSyncable = mSyncContentResolverWrapper.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             mSyncContentResolverWrapper.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             mSyncContentResolverWrapper.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 mSyncContentResolverWrapper;
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 syncContentResolverWrapper an implementation of SyncContentResolverWrapper
172      */
173     private SyncStatusHelper(Context context,
174             SyncContentResolverDelegate syncContentResolverWrapper,
175             CachedAccountSyncSettings cachedAccountSettings) {
176         mApplicationContext = context.getApplicationContext();
177         mSyncContentResolverWrapper = syncContentResolverWrapper;
178         mContractAuthority = getContractAuthority();
179         mCachedSettings = cachedAccountSettings;
180
181         updateMasterSyncAutomaticallySetting();
182
183         mSyncContentResolverWrapper.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 = mSyncContentResolverWrapper
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 SyncContentResolverWrapper to use in tests for the
201      * instance of the SyncStatusHelper by calling overrideSyncStatusHelperForTests(...) with
202      * your SyncContentResolverWrapper.
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 SyncContentResolverWrapper so they
222      * do not use the real ContentResolver in Android.
223      *
224      * @param context the context to use
225      * @param syncContentResolverWrapper the SyncContentResolverWrapper to use
226      */
227     @VisibleForTesting
228     public static void overrideSyncStatusHelperForTests(Context context,
229             SyncContentResolverDelegate syncContentResolverWrapper,
230             CachedAccountSyncSettings cachedAccountSettings) {
231         synchronized (INSTANCE_LOCK) {
232             if (sSyncStatusHelper != null) {
233                 throw new IllegalStateException("SyncStatusHelper already exists");
234             }
235             sSyncStatusHelper = new SyncStatusHelper(context, syncContentResolverWrapper,
236                     cachedAccountSettings);
237         }
238     }
239
240     @VisibleForTesting
241     public static void overrideSyncStatusHelperForTests(Context context,
242             SyncContentResolverDelegate syncContentResolverWrapper) {
243         CachedAccountSyncSettings cachedAccountSettings = new CachedAccountSyncSettings(
244                 context.getPackageName(), syncContentResolverWrapper);
245         overrideSyncStatusHelperForTests(context, syncContentResolverWrapper,
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         notifyObservers();
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         notifyObservers();
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         notifyObservers();
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         notifyObservers();
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                     mSyncContentResolverWrapper.getIsSyncable(
383                             accountToSetNotSyncable, mContractAuthority) > 0) {
384                 mSyncContentResolverWrapper.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                 updateMasterSyncAutomaticallySetting();
402                 synchronized (mCachedSettings) {
403                     mCachedSettings.updateSyncSettingsForAccount(
404                             ChromeSigninController.get(mApplicationContext).getSignedInUser());
405                 }
406                 notifyObservers();
407             }
408         }
409     }
410
411     /**
412      * Sets a new StrictMode.ThreadPolicy based on the current one, but allows disk reads
413      * and disk writes.
414      *
415      * The return value is the old policy, which must be applied after the disk access is finished,
416      * by using StrictMode.setThreadPolicy(oldPolicy).
417      *
418      * @return the policy before allowing reads and writes.
419      */
420     private static StrictMode.ThreadPolicy temporarilyAllowDiskWritesAndDiskReads() {
421         StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
422         StrictMode.ThreadPolicy.Builder newPolicy =
423                 new StrictMode.ThreadPolicy.Builder(oldPolicy);
424         newPolicy.permitDiskReads();
425         newPolicy.permitDiskWrites();
426         StrictMode.setThreadPolicy(newPolicy.build());
427         return oldPolicy;
428     }
429
430     private boolean getAndClearDidUpdateStatus() {
431         boolean didGetStatusUpdate;
432         synchronized (mCachedSettings) {
433             didGetStatusUpdate = mCachedSettings.getDidUpdateStatus();
434             mCachedSettings.clearUpdateStatus();
435         }
436         return didGetStatusUpdate;
437     }
438
439     private void notifyObservers() {
440         if (!getAndClearDidUpdateStatus()) return;
441         for (SyncSettingsChangedObserver observer: mObservers) {
442             observer.syncSettingsChanged();
443         }
444     }
445 }