mobile/android/base/sync/syncadapter/SyncAdapter.java

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

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 }

mercurial