Update To 11.40.268.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 org.chromium.base.ObserverList;
14 import org.chromium.base.VisibleForTesting;
15 import org.chromium.sync.signin.AccountManagerHelper;
16 import org.chromium.sync.signin.ChromeSigninController;
17
18 import javax.annotation.concurrent.NotThreadSafe;
19 import javax.annotation.concurrent.ThreadSafe;
20
21 /**
22  * A helper class to handle the current status of sync for Chrome in Android settings.
23  *
24  * It also provides an observer to be used whenever Android sync settings change.
25  *
26  * To retrieve an instance of this class, call SyncStatusHelper.get(someContext).
27  *
28  * All new public methods MUST call notifyObservers at the end.
29  */
30 @ThreadSafe
31 public class SyncStatusHelper {
32
33     /**
34      * In-memory holder of the sync configurations for a given account. On each
35      * access, updates the cache if the account has changed. This lazy-updating
36      * model is appropriate as the account changes rarely but may not be known
37      * when initially constructed. So long as we keep a single account, no
38      * expensive calls to Android are made.
39      */
40     @NotThreadSafe
41     @VisibleForTesting
42     public static class CachedAccountSyncSettings {
43         private final String mContractAuthority;
44         private final SyncContentResolverDelegate mSyncContentResolverDelegate;
45         private Account mAccount;
46         private boolean mDidUpdate;
47         private boolean mSyncAutomatically;
48         private int mIsSyncable;
49
50         public CachedAccountSyncSettings(String contractAuthority,
51                 SyncContentResolverDelegate contentResolverWrapper) {
52             mContractAuthority = contractAuthority;
53             mSyncContentResolverDelegate = contentResolverWrapper;
54         }
55
56         private void ensureSettingsAreForAccount(Account account) {
57             assert account != null;
58             if (account.equals(mAccount)) return;
59             updateSyncSettingsForAccount(account);
60             mDidUpdate = true;
61         }
62
63         public void clearUpdateStatus() {
64             mDidUpdate = false;
65         }
66
67         public boolean getDidUpdateStatus() {
68             return mDidUpdate;
69         }
70
71         // Calling this method may have side-effects.
72         public boolean getSyncAutomatically(Account account) {
73             ensureSettingsAreForAccount(account);
74             return mSyncAutomatically;
75         }
76
77         public void updateSyncSettingsForAccount(Account account) {
78             if (account == null) return;
79             updateSyncSettingsForAccountInternal(account);
80         }
81
82         public void setIsSyncable(Account account) {
83             ensureSettingsAreForAccount(account);
84             if (mIsSyncable == 1) return;
85             setIsSyncableInternal(account);
86         }
87
88         public void setSyncAutomatically(Account account, boolean value) {
89             ensureSettingsAreForAccount(account);
90             if (mSyncAutomatically == value) return;
91             setSyncAutomaticallyInternal(account, value);
92         }
93
94         @VisibleForTesting
95         protected void updateSyncSettingsForAccountInternal(Account account) {
96             // Null check here otherwise Findbugs complains.
97             if (account == null) return;
98
99             boolean oldSyncAutomatically = mSyncAutomatically;
100             int oldIsSyncable = mIsSyncable;
101             Account oldAccount = mAccount;
102
103             mAccount = account;
104
105             StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
106             mSyncAutomatically = mSyncContentResolverDelegate.getSyncAutomatically(
107                     account, mContractAuthority);
108             mIsSyncable = mSyncContentResolverDelegate.getIsSyncable(account, mContractAuthority);
109             StrictMode.setThreadPolicy(oldPolicy);
110             mDidUpdate = (oldIsSyncable != mIsSyncable)
111                 || (oldSyncAutomatically != mSyncAutomatically)
112                 || (!account.equals(oldAccount));
113         }
114
115         @VisibleForTesting
116         protected void setIsSyncableInternal(Account account) {
117             mIsSyncable = 1;
118             StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
119             mSyncContentResolverDelegate.setIsSyncable(account, mContractAuthority, 1);
120             StrictMode.setThreadPolicy(oldPolicy);
121             mDidUpdate = true;
122         }
123
124         @VisibleForTesting
125         protected void setSyncAutomaticallyInternal(Account account, boolean value) {
126             mSyncAutomatically = value;
127             StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
128             mSyncContentResolverDelegate.setSyncAutomatically(account, mContractAuthority, value);
129             StrictMode.setThreadPolicy(oldPolicy);
130             mDidUpdate = true;
131         }
132     }
133
134     // This should always have the same value as GaiaConstants::kChromeSyncOAuth2Scope.
135     public static final String CHROME_SYNC_OAUTH2_SCOPE =
136             "https://www.googleapis.com/auth/chromesync";
137
138     public static final String TAG = "SyncStatusHelper";
139
140     /**
141      * Lock for ensuring singleton instantiation across threads.
142      */
143     private static final Object INSTANCE_LOCK = new Object();
144
145     private static SyncStatusHelper sSyncStatusHelper;
146
147     private final String mContractAuthority;
148
149     private final Context mApplicationContext;
150
151     private final SyncContentResolverDelegate mSyncContentResolverDelegate;
152
153     private boolean mCachedMasterSyncAutomatically;
154
155     // Instantiation of SyncStatusHelper is guarded by a lock so volatile is unneeded.
156     private CachedAccountSyncSettings mCachedSettings;
157
158     private final ObserverList<SyncSettingsChangedObserver> mObservers =
159             new ObserverList<SyncSettingsChangedObserver>();
160
161     /**
162      * Provides notifications when Android sync settings have changed.
163      */
164     public interface SyncSettingsChangedObserver {
165         public void syncSettingsChanged();
166     }
167
168     /**
169      * @param context the context
170      * @param syncContentResolverDelegate an implementation of {@link SyncContentResolverDelegate}.
171      */
172     private SyncStatusHelper(Context context,
173             SyncContentResolverDelegate syncContentResolverDelegate,
174             CachedAccountSyncSettings cachedAccountSettings) {
175         mApplicationContext = context.getApplicationContext();
176         mSyncContentResolverDelegate = syncContentResolverDelegate;
177         mContractAuthority = getContractAuthority();
178         mCachedSettings = cachedAccountSettings;
179
180         updateMasterSyncAutomaticallySetting();
181
182         mSyncContentResolverDelegate.addStatusChangeListener(
183                 ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS,
184                 new AndroidSyncSettingsChangedObserver());
185     }
186
187     private void updateMasterSyncAutomaticallySetting() {
188         StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
189         synchronized (mCachedSettings) {
190             mCachedMasterSyncAutomatically = mSyncContentResolverDelegate
191                     .getMasterSyncAutomatically();
192         }
193         StrictMode.setThreadPolicy(oldPolicy);
194     }
195
196     /**
197      * A factory method for the SyncStatusHelper.
198      *
199      * It is possible to override the {@link SyncContentResolverDelegate} to use in tests for the
200      * instance of the SyncStatusHelper by calling overrideSyncStatusHelperForTests(...) with
201      * your {@link SyncContentResolverDelegate}.
202      *
203      * @param context the ApplicationContext is retrieved from the context used as an argument.
204      * @return a singleton instance of the SyncStatusHelper
205      */
206     public static SyncStatusHelper get(Context context) {
207         synchronized (INSTANCE_LOCK) {
208             if (sSyncStatusHelper == null) {
209                 SyncContentResolverDelegate contentResolverDelegate =
210                         new SystemSyncContentResolverDelegate();
211                 CachedAccountSyncSettings cache = new CachedAccountSyncSettings(
212                         context.getPackageName(), contentResolverDelegate);
213                 sSyncStatusHelper = new SyncStatusHelper(context, contentResolverDelegate, cache);
214             }
215         }
216         return sSyncStatusHelper;
217     }
218
219     /**
220      * Tests might want to consider overriding the context and {@link SyncContentResolverDelegate}
221      * so they do not use the real ContentResolver in Android.
222      *
223      * @param context the context to use
224      * @param syncContentResolverDelegate the {@link SyncContentResolverDelegate} to use
225      */
226     @VisibleForTesting
227     public static void overrideSyncStatusHelperForTests(Context context,
228             SyncContentResolverDelegate syncContentResolverDelegate,
229             CachedAccountSyncSettings cachedAccountSettings) {
230         synchronized (INSTANCE_LOCK) {
231             if (sSyncStatusHelper != null) {
232                 throw new IllegalStateException("SyncStatusHelper already exists");
233             }
234             sSyncStatusHelper = new SyncStatusHelper(context, syncContentResolverDelegate,
235                     cachedAccountSettings);
236         }
237     }
238
239     @VisibleForTesting
240     public static void overrideSyncStatusHelperForTests(Context context,
241             SyncContentResolverDelegate syncContentResolverDelegate) {
242         CachedAccountSyncSettings cachedAccountSettings = new CachedAccountSyncSettings(
243                 context.getPackageName(), syncContentResolverDelegate);
244         overrideSyncStatusHelperForTests(context, syncContentResolverDelegate,
245                 cachedAccountSettings);
246     }
247
248     /**
249      * Returns the contract authority to use when requesting sync.
250      */
251     public String getContractAuthority() {
252         return mApplicationContext.getPackageName();
253     }
254
255     /**
256      * Wrapper method for the ContentResolver.addStatusChangeListener(...) when we are only
257      * interested in the settings type.
258      */
259     public void registerSyncSettingsChangedObserver(SyncSettingsChangedObserver observer) {
260         mObservers.addObserver(observer);
261     }
262
263     /**
264      * Wrapper method for the ContentResolver.removeStatusChangeListener(...).
265      */
266     public void unregisterSyncSettingsChangedObserver(SyncSettingsChangedObserver observer) {
267         mObservers.removeObserver(observer);
268     }
269
270     /**
271      * Checks whether sync is currently enabled from Chrome for a given account.
272      *
273      * It checks both the master sync for the device, and Chrome sync setting for the given account.
274      *
275      * @param account the account to check if Chrome sync is enabled on.
276      * @return true if sync is on, false otherwise
277      */
278     public boolean isSyncEnabled(Account account) {
279         if (account == null) return false;
280         boolean returnValue;
281         synchronized (mCachedSettings) {
282             returnValue = mCachedMasterSyncAutomatically
283                     && mCachedSettings.getSyncAutomatically(account);
284         }
285
286         notifyObserversIfAccountSettingsChanged();
287         return returnValue;
288     }
289
290     /**
291      * Checks whether sync is currently enabled from Chrome for the currently signed in account.
292      *
293      * It checks both the master sync for the device, and Chrome sync setting for the given account.
294      * If no user is currently signed in it returns false.
295      *
296      * @return true if sync is on, false otherwise
297      */
298     public boolean isSyncEnabled() {
299         return isSyncEnabled(ChromeSigninController.get(mApplicationContext).getSignedInUser());
300     }
301
302     /**
303      * Checks whether sync is currently enabled from Chrome for a given account.
304      *
305      * It checks only Chrome sync setting for the given account,
306      * and ignores the master sync setting.
307      *
308      * @param account the account to check if Chrome sync is enabled on.
309      * @return true if sync is on, false otherwise
310      */
311     public boolean isSyncEnabledForChrome(Account account) {
312         if (account == null) return false;
313
314         boolean returnValue;
315         synchronized (mCachedSettings) {
316             returnValue = mCachedSettings.getSyncAutomatically(account);
317         }
318
319         notifyObserversIfAccountSettingsChanged();
320         return returnValue;
321     }
322
323     /**
324      * Checks whether the master sync flag for Android is currently set.
325      *
326      * @return true if the global master sync is on, false otherwise
327      */
328     public boolean isMasterSyncAutomaticallyEnabled() {
329         synchronized (mCachedSettings) {
330             return mCachedMasterSyncAutomatically;
331         }
332     }
333
334     /**
335      * Make sure Chrome is syncable, and enable sync.
336      *
337      * @param account the account to enable sync on
338      */
339     public void enableAndroidSync(Account account) {
340         makeSyncable(account);
341
342         synchronized (mCachedSettings) {
343             mCachedSettings.setSyncAutomatically(account, true);
344         }
345
346         notifyObserversIfAccountSettingsChanged();
347     }
348
349     /**
350      * Disables Android Chrome sync
351      *
352      * @param account the account to disable Chrome sync on
353      */
354     public void disableAndroidSync(Account account) {
355         synchronized (mCachedSettings) {
356             mCachedSettings.setSyncAutomatically(account, false);
357         }
358
359         notifyObserversIfAccountSettingsChanged();
360     }
361
362     /**
363      * Register with Android Sync Manager. This is what causes the "Chrome" option to appear in
364      * Settings -> Accounts / Sync .
365      *
366      * @param account the account to enable Chrome sync on
367      */
368     private void makeSyncable(Account account) {
369         synchronized (mCachedSettings) {
370             mCachedSettings.setIsSyncable(account);
371         }
372
373         StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
374         // Disable the syncability of Chrome for all other accounts. Don't use
375         // our cache as we're touching many accounts that aren't signed in, so this saves
376         // extra calls to Android sync configuration.
377         Account[] googleAccounts = AccountManagerHelper.get(mApplicationContext)
378                 .getGoogleAccounts();
379         for (Account accountToSetNotSyncable : googleAccounts) {
380             if (!accountToSetNotSyncable.equals(account)
381                     && mSyncContentResolverDelegate.getIsSyncable(
382                             accountToSetNotSyncable, mContractAuthority) > 0) {
383                 mSyncContentResolverDelegate.setIsSyncable(accountToSetNotSyncable,
384                         mContractAuthority, 0);
385             }
386         }
387         StrictMode.setThreadPolicy(oldPolicy);
388     }
389
390     /**
391      * Helper class to be used by observers whenever sync settings change.
392      *
393      * To register the observer, call SyncStatusHelper.registerObserver(...).
394      */
395     private class AndroidSyncSettingsChangedObserver implements SyncStatusObserver {
396         @Override
397         public void onStatusChanged(int which) {
398             if (ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS == which) {
399                 // Sync settings have changed; update our in-memory caches
400                 synchronized (mCachedSettings) {
401                     mCachedSettings.updateSyncSettingsForAccount(
402                             ChromeSigninController.get(mApplicationContext).getSignedInUser());
403                 }
404
405                 boolean oldMasterSyncEnabled = isMasterSyncAutomaticallyEnabled();
406                 updateMasterSyncAutomaticallySetting();
407                 boolean didMasterSyncChanged =
408                         oldMasterSyncEnabled != isMasterSyncAutomaticallyEnabled();
409                 // Notify observers if MasterSync or account level settings change.
410                 if (didMasterSyncChanged || getAndClearDidUpdateStatus())
411                     notifyObservers();
412             }
413         }
414     }
415
416     private boolean getAndClearDidUpdateStatus() {
417         boolean didGetStatusUpdate;
418         synchronized (mCachedSettings) {
419             didGetStatusUpdate = mCachedSettings.getDidUpdateStatus();
420             mCachedSettings.clearUpdateStatus();
421         }
422         return didGetStatusUpdate;
423     }
424
425     private void notifyObserversIfAccountSettingsChanged() {
426         if (getAndClearDidUpdateStatus()) {
427             notifyObservers();
428         }
429     }
430
431     private void notifyObservers() {
432         for (SyncSettingsChangedObserver observer : mObservers) {
433             observer.syncSettingsChanged();
434         }
435     }
436 }