Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 package org.mozilla.gecko.sync.syncadapter;
7 import java.io.IOException;
8 import java.net.URI;
9 import java.security.NoSuchAlgorithmException;
10 import java.util.Collection;
11 import java.util.concurrent.atomic.AtomicBoolean;
13 import org.json.simple.parser.ParseException;
14 import org.mozilla.gecko.background.common.GlobalConstants;
15 import org.mozilla.gecko.background.common.log.Logger;
16 import org.mozilla.gecko.db.BrowserContract;
17 import org.mozilla.gecko.sync.AlreadySyncingException;
18 import org.mozilla.gecko.sync.BackoffHandler;
19 import org.mozilla.gecko.sync.CredentialException;
20 import org.mozilla.gecko.sync.GlobalSession;
21 import org.mozilla.gecko.sync.NonObjectJSONException;
22 import org.mozilla.gecko.sync.PrefsBackoffHandler;
23 import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate;
24 import org.mozilla.gecko.sync.SharedPreferencesNodeAssignmentCallback;
25 import org.mozilla.gecko.sync.Sync11Configuration;
26 import org.mozilla.gecko.sync.SyncConfiguration;
27 import org.mozilla.gecko.sync.SyncConfigurationException;
28 import org.mozilla.gecko.sync.SyncConstants;
29 import org.mozilla.gecko.sync.SyncException;
30 import org.mozilla.gecko.sync.ThreadPool;
31 import org.mozilla.gecko.sync.Utils;
32 import org.mozilla.gecko.sync.config.AccountPickler;
33 import org.mozilla.gecko.sync.crypto.CryptoException;
34 import org.mozilla.gecko.sync.crypto.KeyBundle;
35 import org.mozilla.gecko.sync.delegates.BaseGlobalSessionCallback;
36 import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
37 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
38 import org.mozilla.gecko.sync.net.BasicAuthHeaderProvider;
39 import org.mozilla.gecko.sync.net.ConnectionMonitorThread;
40 import org.mozilla.gecko.sync.setup.Constants;
41 import org.mozilla.gecko.sync.setup.SyncAccounts;
42 import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters;
43 import org.mozilla.gecko.sync.stage.GlobalSyncStage.Stage;
45 import android.accounts.Account;
46 import android.accounts.AccountManager;
47 import android.accounts.AuthenticatorException;
48 import android.accounts.OperationCanceledException;
49 import android.content.AbstractThreadedSyncAdapter;
50 import android.content.ContentProviderClient;
51 import android.content.ContentResolver;
52 import android.content.Context;
53 import android.content.SharedPreferences;
54 import android.content.SyncResult;
55 import android.database.sqlite.SQLiteConstraintException;
56 import android.database.sqlite.SQLiteException;
57 import android.os.Bundle;
59 public class SyncAdapter extends AbstractThreadedSyncAdapter implements BaseGlobalSessionCallback {
60 private static final String LOG_TAG = "SyncAdapter";
62 private static final int BACKOFF_PAD_SECONDS = 5;
63 public static final int MULTI_DEVICE_INTERVAL_MILLISECONDS = 5 * 60 * 1000; // 5 minutes.
64 public static final int SINGLE_DEVICE_INTERVAL_MILLISECONDS = 24 * 60 * 60 * 1000; // 24 hours.
66 private final Context mContext;
68 protected long syncStartTimestamp;
70 protected volatile BackoffHandler backoffHandler;
72 public SyncAdapter(Context context, boolean autoInitialize) {
73 super(context, autoInitialize);
74 mContext = context;
75 }
77 /**
78 * Handle an exception: update stats, log errors, etc.
79 * Wakes up sleeping threads by calling notifyMonitor().
80 *
81 * @param globalSession
82 * current global session, or null.
83 * @param e
84 * Exception to handle.
85 */
86 protected void processException(final GlobalSession globalSession, final Exception e) {
87 try {
88 if (e instanceof SQLiteConstraintException) {
89 Logger.error(LOG_TAG, "Constraint exception. Aborting sync.", e);
90 syncResult.stats.numParseExceptions++; // This is as good as we can do.
91 return;
92 }
93 if (e instanceof SQLiteException) {
94 Logger.error(LOG_TAG, "Couldn't open database (locked?). Aborting sync.", e);
95 syncResult.stats.numIoExceptions++;
96 return;
97 }
98 if (e instanceof OperationCanceledException) {
99 Logger.error(LOG_TAG, "Operation canceled. Aborting sync.", e);
100 return;
101 }
102 if (e instanceof AuthenticatorException) {
103 syncResult.stats.numParseExceptions++;
104 Logger.error(LOG_TAG, "AuthenticatorException. Aborting sync.", e);
105 return;
106 }
107 if (e instanceof IOException) {
108 syncResult.stats.numIoExceptions++;
109 Logger.error(LOG_TAG, "IOException. Aborting sync.", e);
110 e.printStackTrace();
111 return;
112 }
114 // Blanket stats updating for SyncException subclasses.
115 if (e instanceof SyncException) {
116 ((SyncException) e).updateStats(globalSession, syncResult);
117 } else {
118 // Generic exception.
119 syncResult.stats.numIoExceptions++;
120 }
122 if (e instanceof CredentialException.MissingAllCredentialsException) {
123 // This is bad: either we couldn't fetch credentials, or the credentials
124 // were totally blank. Most likely the user has two copies of Firefox
125 // installed, and something is misbehaving.
126 // Either way, disable this account.
127 if (localAccount == null) {
128 // Should not happen, but be safe.
129 Logger.error(LOG_TAG, "No credentials attached to account. Aborting sync.");
130 return;
131 }
133 Logger.error(LOG_TAG, "No credentials attached to account " + localAccount.name + ". Aborting sync.");
134 try {
135 SyncAccounts.setSyncAutomatically(localAccount, false);
136 } catch (Exception ex) {
137 Logger.error(LOG_TAG, "Unable to disable account " + localAccount.name + ".", ex);
138 }
139 return;
140 }
142 if (e instanceof CredentialException.MissingCredentialException) {
143 Logger.error(LOG_TAG, "Credentials attached to account, but missing " +
144 ((CredentialException.MissingCredentialException) e).missingCredential + ". Aborting sync.");
145 return;
146 }
148 if (e instanceof CredentialException) {
149 Logger.error(LOG_TAG, "Credentials attached to account were bad.");
150 return;
151 }
153 // Bug 755638 - Uncaught SecurityException when attempting to sync multiple Fennecs
154 // to the same Sync account.
155 // Uncheck Sync checkbox because we cannot sync this instance.
156 if (e instanceof SecurityException) {
157 Logger.error(LOG_TAG, "SecurityException, multiple Fennecs. Disabling this instance.", e);
158 SyncAccounts.backgroundSetSyncAutomatically(localAccount, false);
159 return;
160 }
161 // Generic exception.
162 Logger.error(LOG_TAG, "Unknown exception. Aborting sync.", e);
163 } finally {
164 notifyMonitor();
165 }
166 }
168 @Override
169 public void onSyncCanceled() {
170 super.onSyncCanceled();
171 // TODO: cancel the sync!
172 // From the docs: "This will be invoked on a separate thread than the sync
173 // thread and so you must consider the multi-threaded implications of the
174 // work that you do in this method."
175 }
177 public Object syncMonitor = new Object();
178 private SyncResult syncResult;
180 protected Account localAccount;
181 protected boolean thisSyncIsForced = false;
182 protected SharedPreferences accountSharedPreferences;
183 protected SharedPreferencesClientsDataDelegate clientsDataDelegate;
184 protected SharedPreferencesNodeAssignmentCallback nodeAssignmentDelegate;
186 /**
187 * Request that no sync start right away. A new sync won't start until
188 * at least <code>backoff</code> milliseconds from now.
189 *
190 * Don't call this unless you are inside `run`.
191 *
192 * @param backoff time to wait in milliseconds.
193 */
194 @Override
195 public void requestBackoff(final long backoff) {
196 if (this.backoffHandler == null) {
197 throw new IllegalStateException("No backoff handler: requesting backoff outside run()?");
198 }
199 if (backoff > 0) {
200 // Fuzz the backoff time (up to 25% more) to prevent client lock-stepping; agrees with desktop.
201 final long fuzzedBackoff = backoff + Math.round((double) backoff * 0.25d * Math.random());
202 this.backoffHandler.extendEarliestNextRequest(System.currentTimeMillis() + fuzzedBackoff);
203 }
204 }
206 @Override
207 public boolean shouldBackOffStorage() {
208 if (thisSyncIsForced) {
209 /*
210 * If the user asks us to sync, we should sync regardless. This path is
211 * hit if the user force syncs and we restart a session after a
212 * freshStart.
213 */
214 return false;
215 }
217 if (nodeAssignmentDelegate.wantNodeAssignment()) {
218 /*
219 * We recently had a 401 and we aborted the last sync. We should kick off
220 * another sync to fetch a new node/weave cluster URL, since ours is
221 * stale. If we have a user authentication error, the next sync will
222 * determine that and will stop requesting node assignment, so this will
223 * only force one abnormally scheduled sync.
224 */
225 return false;
226 }
228 if (this.backoffHandler == null) {
229 throw new IllegalStateException("No backoff handler: checking backoff outside run()?");
230 }
231 return this.backoffHandler.delayMilliseconds() > 0;
232 }
234 /**
235 * Asynchronously request an immediate sync, optionally syncing only the given
236 * named stages.
237 * <p>
238 * Returns immediately.
239 *
240 * @param account
241 * the Android <code>Account</code> instance to sync.
242 * @param stageNames
243 * stage names to sync, or <code>null</code> to sync all known stages.
244 */
245 public static void requestImmediateSync(final Account account, final String[] stageNames) {
246 requestImmediateSync(account, stageNames, null);
247 }
249 /**
250 * Asynchronously request an immediate sync, optionally syncing only the given
251 * named stages.
252 * <p>
253 * Returns immediately.
254 *
255 * @param account
256 * the Android <code>Account</code> instance to sync.
257 * @param stageNames
258 * stage names to sync, or <code>null</code> to sync all known stages.
259 * @param moreExtras
260 * bundle of extras to give to the sync, or <code>null</code>
261 */
262 public static void requestImmediateSync(final Account account, final String[] stageNames, Bundle moreExtras) {
263 if (account == null) {
264 Logger.warn(LOG_TAG, "Not requesting immediate sync because Android Account is null.");
265 return;
266 }
268 final Bundle extras = new Bundle();
269 Utils.putStageNamesToSync(extras, stageNames, null);
270 extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
272 if (moreExtras != null) {
273 extras.putAll(moreExtras);
274 }
276 ContentResolver.requestSync(account, BrowserContract.AUTHORITY, extras);
277 }
279 @Override
280 public void onPerformSync(final Account account,
281 final Bundle extras,
282 final String authority,
283 final ContentProviderClient provider,
284 final SyncResult syncResult) {
285 syncStartTimestamp = System.currentTimeMillis();
287 Logger.setThreadLogTag(SyncConstants.GLOBAL_LOG_TAG);
288 Logger.resetLogging();
289 Utils.reseedSharedRandom(); // Make sure we don't work with the same random seed for too long.
291 // Set these so that we don't need to thread them through assorted calls and callbacks.
292 this.syncResult = syncResult;
293 this.localAccount = account;
295 SyncAccountParameters params;
296 try {
297 params = SyncAccounts.blockingFromAndroidAccountV0(mContext, AccountManager.get(mContext), this.localAccount);
298 } catch (Exception e) {
299 // Updates syncResult and (harmlessly) calls notifyMonitor().
300 processException(null, e);
301 return;
302 }
304 // params and the following fields are non-null at this point.
305 final String username = params.username; // Encoded with Utils.usernameFromAccount.
306 final String password = params.password;
307 final String serverURL = params.serverURL;
308 final String syncKey = params.syncKey;
310 final AtomicBoolean setNextSync = new AtomicBoolean(true);
311 final SyncAdapter self = this;
312 final Runnable runnable = new Runnable() {
313 @Override
314 public void run() {
315 Logger.trace(LOG_TAG, "AccountManagerCallback invoked.");
316 // TODO: N.B.: Future must not be used on the main thread.
317 try {
318 if (Logger.LOG_PERSONAL_INFORMATION) {
319 Logger.pii(LOG_TAG, "Syncing account named " + account.name +
320 " for authority " + authority + ".");
321 } else {
322 // Replace "foo@bar.com" with "XXX@XXX.XXX".
323 Logger.info(LOG_TAG, "Syncing account named like " + Utils.obfuscateEmail(account.name) +
324 " for authority " + authority + ".");
325 }
327 // We dump this information right away to help with debugging.
328 Logger.debug(LOG_TAG, "Username: " + username);
329 Logger.debug(LOG_TAG, "Server: " + serverURL);
330 if (Logger.LOG_PERSONAL_INFORMATION) {
331 Logger.debug(LOG_TAG, "Password: " + password);
332 Logger.debug(LOG_TAG, "Sync key: " + syncKey);
333 } else {
334 Logger.debug(LOG_TAG, "Password? " + (password != null));
335 Logger.debug(LOG_TAG, "Sync key? " + (syncKey != null));
336 }
338 // Support multiple accounts by mapping each server/account pair to a branch of the
339 // shared preferences space.
340 final String product = GlobalConstants.BROWSER_INTENT_PACKAGE;
341 final String profile = Constants.DEFAULT_PROFILE;
342 final long version = SyncConfiguration.CURRENT_PREFS_VERSION;
343 self.accountSharedPreferences = Utils.getSharedPreferences(mContext, product, username, serverURL, profile, version);
344 self.clientsDataDelegate = new SharedPreferencesClientsDataDelegate(accountSharedPreferences);
345 self.backoffHandler = new PrefsBackoffHandler(accountSharedPreferences, SyncConstants.BACKOFF_PREF_SUFFIX_11);
346 final String nodeWeaveURL = Utils.nodeWeaveURL(serverURL, username);
347 self.nodeAssignmentDelegate = new SharedPreferencesNodeAssignmentCallback(accountSharedPreferences, nodeWeaveURL);
349 Logger.info(LOG_TAG,
350 "Client is named '" + clientsDataDelegate.getClientName() + "'" +
351 ", has client guid " + clientsDataDelegate.getAccountGUID() +
352 ", and has " + clientsDataDelegate.getClientsCount() + " clients.");
354 final boolean thisSyncIsForced = (extras != null) && (extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false));
355 final long delayMillis = backoffHandler.delayMilliseconds();
356 boolean shouldSync = thisSyncIsForced || (delayMillis <= 0L);
357 if (!shouldSync) {
358 long remainingSeconds = delayMillis / 1000;
359 syncResult.delayUntil = remainingSeconds + BACKOFF_PAD_SECONDS;
360 setNextSync.set(false);
361 self.notifyMonitor();
362 return;
363 }
365 final String prefsPath = Utils.getPrefsPath(product, username, serverURL, profile, version);
366 self.performSync(account, extras, authority, provider, syncResult,
367 username, password, prefsPath, serverURL, syncKey);
368 } catch (Exception e) {
369 self.processException(null, e);
370 return;
371 }
372 }
373 };
375 synchronized (syncMonitor) {
376 // Perform the work in a new thread from within this synchronized block,
377 // which allows us to be waiting on the monitor before the callback can
378 // notify us in a failure case. Oh, concurrent programming.
379 new Thread(runnable).start();
381 // Start our stale connection monitor thread.
382 ConnectionMonitorThread stale = new ConnectionMonitorThread();
383 stale.start();
385 Logger.trace(LOG_TAG, "Waiting on sync monitor.");
386 try {
387 syncMonitor.wait();
389 if (setNextSync.get()) {
390 long interval = getSyncInterval(clientsDataDelegate);
391 long next = System.currentTimeMillis() + interval;
393 if (thisSyncIsForced) {
394 Logger.info(LOG_TAG, "Setting minimum next sync time to " + next + " (" + interval + "ms from now).");
395 self.backoffHandler.setEarliestNextRequest(next);
396 } else {
397 Logger.info(LOG_TAG, "Extending minimum next sync time to " + next + " (" + interval + "ms from now).");
398 self.backoffHandler.extendEarliestNextRequest(next);
399 }
400 }
401 Logger.info(LOG_TAG, "Sync took " + Utils.formatDuration(syncStartTimestamp, System.currentTimeMillis()) + ".");
402 } catch (InterruptedException e) {
403 Logger.warn(LOG_TAG, "Waiting on sync monitor interrupted.", e);
404 } finally {
405 // And we're done with HTTP stuff.
406 stale.shutdown();
407 }
408 }
409 }
411 public int getSyncInterval(ClientsDataDelegate clientsDataDelegate) {
412 // Must have been a problem that means we can't access the Account.
413 if (this.localAccount == null) {
414 return SINGLE_DEVICE_INTERVAL_MILLISECONDS;
415 }
417 int clientsCount = clientsDataDelegate.getClientsCount();
418 if (clientsCount <= 1) {
419 return SINGLE_DEVICE_INTERVAL_MILLISECONDS;
420 }
422 return MULTI_DEVICE_INTERVAL_MILLISECONDS;
423 }
425 /**
426 * Now that we have a sync key and password, go ahead and do the work.
427 * @throws NoSuchAlgorithmException
428 * @throws IllegalArgumentException
429 * @throws SyncConfigurationException
430 * @throws AlreadySyncingException
431 * @throws NonObjectJSONException
432 * @throws ParseException
433 * @throws IOException
434 * @throws CryptoException
435 */
436 protected void performSync(final Account account,
437 final Bundle extras,
438 final String authority,
439 final ContentProviderClient provider,
440 final SyncResult syncResult,
441 final String username,
442 final String password,
443 final String prefsPath,
444 final String serverURL,
445 final String syncKey)
446 throws NoSuchAlgorithmException,
447 SyncConfigurationException,
448 IllegalArgumentException,
449 AlreadySyncingException,
450 IOException, ParseException,
451 NonObjectJSONException, CryptoException {
452 Logger.trace(LOG_TAG, "Performing sync.");
454 /**
455 * Bug 769745: pickle Sync account parameters to JSON file. Un-pickle in
456 * <code>SyncAccounts.syncAccountsExist</code>.
457 */
458 try {
459 // Constructor can throw on nulls, which should not happen -- but let's be safe.
460 final SyncAccountParameters params = new SyncAccountParameters(mContext, null,
461 account.name, // Un-encoded, like "test@mozilla.com".
462 syncKey,
463 password,
464 serverURL,
465 null, // We'll re-fetch cluster URL; not great, but not harmful.
466 clientsDataDelegate.getClientName(),
467 clientsDataDelegate.getAccountGUID());
469 // Bug 772971: pickle Sync account parameters on background thread to
470 // avoid strict mode warnings.
471 ThreadPool.run(new Runnable() {
472 @Override
473 public void run() {
474 final boolean syncAutomatically = ContentResolver.getSyncAutomatically(account, authority);
475 try {
476 AccountPickler.pickle(mContext, Constants.ACCOUNT_PICKLE_FILENAME, params, syncAutomatically);
477 } catch (Exception e) {
478 // Should never happen, but we really don't want to die in a background thread.
479 Logger.warn(LOG_TAG, "Got exception pickling current account details; ignoring.", e);
480 }
481 }
482 });
483 } catch (IllegalArgumentException e) {
484 // Do nothing.
485 }
487 if (username == null) {
488 throw new IllegalArgumentException("username must not be null.");
489 }
491 if (syncKey == null) {
492 throw new SyncConfigurationException();
493 }
495 final KeyBundle keyBundle = new KeyBundle(username, syncKey);
497 if (keyBundle == null ||
498 keyBundle.getEncryptionKey() == null ||
499 keyBundle.getHMACKey() == null) {
500 throw new SyncConfigurationException();
501 }
503 final AuthHeaderProvider authHeaderProvider = new BasicAuthHeaderProvider(username, password);
504 final SharedPreferences prefs = getContext().getSharedPreferences(prefsPath, Utils.SHARED_PREFERENCES_MODE);
505 final SyncConfiguration config = new Sync11Configuration(username, authHeaderProvider, prefs, keyBundle);
507 Collection<String> knownStageNames = SyncConfiguration.validEngineNames();
508 config.stagesToSync = Utils.getStagesToSyncFromBundle(knownStageNames, extras);
510 GlobalSession globalSession = new GlobalSession(config, this, this.mContext, clientsDataDelegate, nodeAssignmentDelegate);
511 globalSession.start();
512 }
514 private void notifyMonitor() {
515 synchronized (syncMonitor) {
516 Logger.trace(LOG_TAG, "Notifying sync monitor.");
517 syncMonitor.notifyAll();
518 }
519 }
521 // Implementing GlobalSession callbacks.
522 @Override
523 public void handleError(GlobalSession globalSession, Exception ex) {
524 Logger.info(LOG_TAG, "GlobalSession indicated error.");
525 this.processException(globalSession, ex);
526 }
528 @Override
529 public void handleAborted(GlobalSession globalSession, String reason) {
530 Logger.warn(LOG_TAG, "Sync aborted: " + reason);
531 notifyMonitor();
532 }
534 @Override
535 public void handleSuccess(GlobalSession globalSession) {
536 Logger.info(LOG_TAG, "GlobalSession indicated success.");
537 globalSession.config.persistToPrefs();
538 notifyMonitor();
539 }
541 @Override
542 public void handleStageCompleted(Stage currentState,
543 GlobalSession globalSession) {
544 Logger.trace(LOG_TAG, "Stage completed: " + currentState);
545 }
547 @Override
548 public void informUnauthorizedResponse(GlobalSession session, URI oldClusterURL) {
549 nodeAssignmentDelegate.setClusterURLIsStale(true);
550 }
552 @Override
553 public void informUpgradeRequiredResponse(final GlobalSession session) {
554 final AccountManager manager = AccountManager.get(mContext);
555 final Account toDisable = localAccount;
556 if (toDisable == null || manager == null) {
557 Logger.warn(LOG_TAG, "Attempting to disable account, but null found.");
558 return;
559 }
560 // Sync needs to be upgraded. Don't automatically sync anymore.
561 ThreadPool.run(new Runnable() {
562 @Override
563 public void run() {
564 manager.setUserData(toDisable, Constants.DATA_ENABLE_ON_UPGRADE, "1");
565 SyncAccounts.setSyncAutomatically(toDisable, false);
566 }
567 });
568 }
569 }