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 | |
8 | import android.accounts.Account; |
9 | import android.content.ContentResolver; |
10 | import android.content.Context; |
11 | import android.content.SyncStatusObserver; |
12 | import android.os.StrictMode; |
13 | |
14 | import com.google.common.annotations.VisibleForTesting; |
15 | |
16 | import org.chromium.base.ObserverList; |
17 | import org.chromium.sync.signin.AccountManagerHelper; |
18 | import org.chromium.sync.signin.ChromeSigninController; |
19 | |
20 | import javax.annotation.concurrent.NotThreadSafe; |
21 | import javax.annotation.concurrent.ThreadSafe; |
22 | |
23 | /** |
24 | * A helper class to handle the current status of sync for Chrome in Android settings. |
25 | * |
26 | * It also provides an observer to be used whenever Android sync settings change. |
27 | * |
28 | * To retrieve an instance of this class, call SyncStatusHelper.get(someContext). |
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 | private class CachedAccountSyncSettings { |
42 | private Account mAccount; |
43 | private boolean mSyncAutomatically; |
44 | private int mIsSyncable; |
45 | |
46 | private void ensureSettingsAreForAccount(Account account) { |
47 | assert account != null; |
48 | if (account.equals(mAccount)) return; |
49 | updateSyncSettingsForAccount(account); |
50 | } |
51 | |
52 | public void updateSyncSettingsForAccount(Account account) { |
53 | if (account == null) return; |
54 | mAccount = account; |
55 | |
56 | StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads(); |
57 | mSyncAutomatically = mSyncContentResolverWrapper.getSyncAutomatically( |
58 | account, mContractAuthority); |
59 | mIsSyncable = mSyncContentResolverWrapper.getIsSyncable(account, mContractAuthority); |
60 | StrictMode.setThreadPolicy(oldPolicy); |
61 | } |
62 | |
63 | public boolean getSyncAutomatically(Account account) { |
64 | ensureSettingsAreForAccount(account); |
65 | return mSyncAutomatically; |
66 | } |
67 | |
68 | public void setIsSyncable(Account account) { |
69 | ensureSettingsAreForAccount(account); |
70 | if (mIsSyncable == 0) return; |
71 | |
72 | mIsSyncable = 1; |
73 | StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads(); |
74 | mSyncContentResolverWrapper.setIsSyncable(account, mContractAuthority, 1); |
75 | StrictMode.setThreadPolicy(oldPolicy); |
76 | } |
77 | |
78 | public void setSyncAutomatically(Account account, boolean value) { |
79 | ensureSettingsAreForAccount(account); |
80 | if (mSyncAutomatically == value) return; |
81 | |
82 | mSyncAutomatically = value; |
83 | StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads(); |
84 | mSyncContentResolverWrapper.setSyncAutomatically(account, mContractAuthority, value); |
85 | StrictMode.setThreadPolicy(oldPolicy); |
86 | } |
87 | } |
88 | |
89 | @Deprecated |
90 | public static final String AUTH_TOKEN_TYPE_SYNC = "chromiumsync"; |
91 | |
92 | // This should always have the same value as GaiaConstants::kChromeSyncOAuth2Scope. |
93 | public static final String CHROME_SYNC_OAUTH2_SCOPE = |
94 | "https://www.googleapis.com/auth/chromesync"; |
95 | |
96 | public static final String TAG = "SyncStatusHelper"; |
97 | |
98 | /** |
99 | * Lock for ensuring singleton instantiation across threads. |
100 | */ |
101 | private static final Object INSTANCE_LOCK = new Object(); |
102 | |
103 | private static SyncStatusHelper sSyncStatusHelper; |
104 | |
105 | private final String mContractAuthority; |
106 | |
107 | private final Context mApplicationContext; |
108 | |
109 | private final SyncContentResolverDelegate mSyncContentResolverWrapper; |
110 | |
111 | private boolean mCachedMasterSyncAutomatically; |
112 | |
113 | // Instantiation of SyncStatusHelper is guarded by a lock so volatile is unneeded. |
114 | private final CachedAccountSyncSettings mCachedSettings = new CachedAccountSyncSettings(); |
115 | |
116 | private final ObserverList<SyncSettingsChangedObserver> mObservers = |
117 | new ObserverList<SyncSettingsChangedObserver>(); |
118 | |
119 | /** |
120 | * Provides notifications when Android sync settings have changed. |
121 | */ |
122 | public interface SyncSettingsChangedObserver { |
123 | public void syncSettingsChanged(); |
124 | } |
125 | |
126 | /** |
127 | * @param context the context |
128 | * @param syncContentResolverWrapper an implementation of SyncContentResolverWrapper |
129 | */ |
130 | private SyncStatusHelper(Context context, |
131 | SyncContentResolverDelegate syncContentResolverWrapper) { |
132 | mApplicationContext = context.getApplicationContext(); |
133 | mSyncContentResolverWrapper = syncContentResolverWrapper; |
134 | mContractAuthority = getContractAuthority(); |
135 | |
136 | updateMasterSyncAutomaticallySetting(); |
137 | |
138 | mSyncContentResolverWrapper.addStatusChangeListener( |
139 | ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, |
140 | new AndroidSyncSettingsChangedObserver()); |
141 | } |
142 | |
143 | private void updateMasterSyncAutomaticallySetting() { |
144 | StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads(); |
145 | synchronized (mCachedSettings) { |
146 | mCachedMasterSyncAutomatically = mSyncContentResolverWrapper |
147 | .getMasterSyncAutomatically(); |
148 | } |
149 | StrictMode.setThreadPolicy(oldPolicy); |
150 | } |
151 | |
152 | /** |
153 | * A factory method for the SyncStatusHelper. |
154 | * |
155 | * It is possible to override the SyncContentResolverWrapper to use in tests for the |
156 | * instance of the SyncStatusHelper by calling overrideSyncStatusHelperForTests(...) with |
157 | * your SyncContentResolverWrapper. |
158 | * |
159 | * @param context the ApplicationContext is retrieved from the context used as an argument. |
160 | * @return a singleton instance of the SyncStatusHelper |
161 | */ |
162 | public static SyncStatusHelper get(Context context) { |
163 | synchronized (INSTANCE_LOCK) { |
164 | if (sSyncStatusHelper == null) { |
165 | sSyncStatusHelper = new SyncStatusHelper(context, |
166 | new SystemSyncContentResolverDelegate()); |
167 | } |
168 | } |
169 | return sSyncStatusHelper; |
170 | } |
171 | |
172 | /** |
173 | * Tests might want to consider overriding the context and SyncContentResolverWrapper so they |
174 | * do not use the real ContentResolver in Android. |
175 | * |
176 | * @param context the context to use |
177 | * @param syncContentResolverWrapper the SyncContentResolverWrapper to use |
178 | */ |
179 | @VisibleForTesting |
180 | public static void overrideSyncStatusHelperForTests(Context context, |
181 | SyncContentResolverDelegate syncContentResolverWrapper) { |
182 | synchronized (INSTANCE_LOCK) { |
183 | if (sSyncStatusHelper != null) { |
184 | throw new IllegalStateException("SyncStatusHelper already exists"); |
185 | } |
186 | sSyncStatusHelper = new SyncStatusHelper(context, syncContentResolverWrapper); |
187 | } |
188 | } |
189 | |
190 | /** |
191 | * Returns the contract authority to use when requesting sync. |
192 | */ |
193 | public String getContractAuthority() { |
194 | return mApplicationContext.getPackageName(); |
195 | } |
196 | |
197 | /** |
198 | * Wrapper method for the ContentResolver.addStatusChangeListener(...) when we are only |
199 | * interested in the settings type. |
200 | */ |
201 | public void registerContentResolverObserver(SyncSettingsChangedObserver observer) { |
202 | mObservers.addObserver(observer); |
203 | } |
204 | |
205 | /** |
206 | * Wrapper method for the ContentResolver.removeStatusChangeListener(...). |
207 | */ |
208 | public void unregisterContentResolverObserver(SyncSettingsChangedObserver observer) { |
209 | mObservers.removeObserver(observer); |
210 | } |
211 | |
212 | /** |
213 | * Checks whether sync is currently enabled from Chrome for a given account. |
214 | * |
215 | * It checks both the master sync for the device, and Chrome sync setting for the given account. |
216 | * |
217 | * @param account the account to check if Chrome sync is enabled on. |
218 | * @return true if sync is on, false otherwise |
219 | */ |
220 | public boolean isSyncEnabled(Account account) { |
221 | if (account == null) return false; |
222 | synchronized (mCachedSettings) { |
223 | return mCachedMasterSyncAutomatically && mCachedSettings.getSyncAutomatically(account); |
224 | } |
225 | } |
226 | |
227 | /** |
228 | * Checks whether sync is currently enabled from Chrome for the currently signed in account. |
229 | * |
230 | * It checks both the master sync for the device, and Chrome sync setting for the given account. |
231 | * If no user is currently signed in it returns false. |
232 | * |
233 | * @return true if sync is on, false otherwise |
234 | */ |
235 | public boolean isSyncEnabled() { |
236 | return isSyncEnabled(ChromeSigninController.get(mApplicationContext).getSignedInUser()); |
237 | } |
238 | |
239 | /** |
240 | * Checks whether sync is currently enabled from Chrome for a given account. |
241 | * |
242 | * It checks only Chrome sync setting for the given account, |
243 | * and ignores the master sync setting. |
244 | * |
245 | * @param account the account to check if Chrome sync is enabled on. |
246 | * @return true if sync is on, false otherwise |
247 | */ |
248 | public boolean isSyncEnabledForChrome(Account account) { |
249 | if (account == null) return false; |
250 | synchronized (mCachedSettings) { |
251 | return mCachedSettings.getSyncAutomatically(account); |
252 | } |
253 | } |
254 | |
255 | /** |
256 | * Checks whether the master sync flag for Android is currently set. |
257 | * |
258 | * @return true if the global master sync is on, false otherwise |
259 | */ |
260 | public boolean isMasterSyncAutomaticallyEnabled() { |
261 | synchronized (mCachedSettings) { |
262 | return mCachedMasterSyncAutomatically; |
263 | } |
264 | } |
265 | |
266 | /** |
267 | * Make sure Chrome is syncable, and enable sync. |
268 | * |
269 | * @param account the account to enable sync on |
270 | */ |
271 | public void enableAndroidSync(Account account) { |
272 | makeSyncable(account); |
273 | |
274 | synchronized (mCachedSettings) { |
275 | mCachedSettings.setSyncAutomatically(account, true); |
276 | } |
277 | } |
278 | |
279 | /** |
280 | * Disables Android Chrome sync |
281 | * |
282 | * @param account the account to disable Chrome sync on |
283 | */ |
284 | public void disableAndroidSync(Account account) { |
285 | synchronized (mCachedSettings) { |
286 | mCachedSettings.setSyncAutomatically(account, false); |
287 | } |
288 | } |
289 | |
290 | /** |
291 | * Register with Android Sync Manager. This is what causes the "Chrome" option to appear in |
292 | * Settings -> Accounts / Sync . |
293 | * |
294 | * @param account the account to enable Chrome sync on |
295 | */ |
296 | private void makeSyncable(Account account) { |
297 | synchronized (mCachedSettings) { |
298 | mCachedSettings.setIsSyncable(account); |
299 | } |
300 | |
301 | StrictMode.ThreadPolicy oldPolicy = temporarilyAllowDiskWritesAndDiskReads(); |
302 | // Disable the syncability of Chrome for all other accounts. Don't use |
303 | // our cache as we're touching many accounts that aren't signed in, so this saves |
304 | // extra calls to Android sync configuration. |
305 | Account[] googleAccounts = AccountManagerHelper.get(mApplicationContext). |
306 | getGoogleAccounts(); |
307 | for (Account accountToSetNotSyncable : googleAccounts) { |
308 | if (!accountToSetNotSyncable.equals(account) && |
309 | mSyncContentResolverWrapper.getIsSyncable( |
310 | accountToSetNotSyncable, mContractAuthority) > 0) { |
311 | mSyncContentResolverWrapper.setIsSyncable(accountToSetNotSyncable, |
312 | mContractAuthority, 0); |
313 | } |
314 | } |
315 | StrictMode.setThreadPolicy(oldPolicy); |
316 | } |
317 | |
318 | /** |
319 | * Helper class to be used by observers whenever sync settings change. |
320 | * |
321 | * To register the observer, call SyncStatusHelper.registerObserver(...). |
322 | */ |
323 | private class AndroidSyncSettingsChangedObserver implements SyncStatusObserver { |
324 | @Override |
325 | public void onStatusChanged(int which) { |
326 | if (ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS == which) { |
327 | // Sync settings have changed; update our in-memory caches |
328 | updateMasterSyncAutomaticallySetting(); |
329 | synchronized (mCachedSettings) { |
330 | mCachedSettings.updateSyncSettingsForAccount( |
331 | ChromeSigninController.get(mApplicationContext).getSignedInUser()); |
332 | } |
333 | |
334 | // Notify anyone else that's interested |
335 | for (SyncSettingsChangedObserver observer: mObservers) { |
336 | observer.syncSettingsChanged(); |
337 | } |
338 | } |
339 | } |
340 | } |
341 | |
342 | /** |
343 | * Sets a new StrictMode.ThreadPolicy based on the current one, but allows disk reads |
344 | * and disk writes. |
345 | * |
346 | * The return value is the old policy, which must be applied after the disk access is finished, |
347 | * by using StrictMode.setThreadPolicy(oldPolicy). |
348 | * |
349 | * @return the policy before allowing reads and writes. |
350 | */ |
351 | private static StrictMode.ThreadPolicy temporarilyAllowDiskWritesAndDiskReads() { |
352 | StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy(); |
353 | StrictMode.ThreadPolicy.Builder newPolicy = |
354 | new StrictMode.ThreadPolicy.Builder(oldPolicy); |
355 | newPolicy.permitDiskReads(); |
356 | newPolicy.permitDiskWrites(); |
357 | StrictMode.setThreadPolicy(newPolicy.build()); |
358 | return oldPolicy; |
359 | } |
360 | } |