Bug 724739, Bug 724740 - Part 2: Queue folder deletions, processing them after other records; reparent children during bookmark folder deletion. r=nalexander
authorRichard Newman <rnewman@mozilla.com>
Tue, 10 Apr 2012 23:21:51 -0700
changeset 94719 1c2a06f36ba3868f7ce9cd1dcf3b21007e425a2e
parent 94718 6124a190cc2b17f531c2e3dfcf0e1eb65a22504f
child 94720 3aef4f7cbd6d00d0d7e77b71f3e6045c74280166
push id886
push userlsblakk@mozilla.com
push dateMon, 04 Jun 2012 19:57:52 +0000
treeherdermozilla-beta@bbd8d5efd6d1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnalexander
bugs724739, 724740
milestone14.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 724739, Bug 724740 - Part 2: Queue folder deletions, processing them after other records; reparent children during bookmark folder deletion. r=nalexander
mobile/android/base/sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java
mobile/android/base/sync/repositories/android/AndroidBrowserRepositorySession.java
mobile/android/base/sync/repositories/android/BookmarksDeletionManager.java
mobile/android/sync/java-sources.mn
--- a/mobile/android/base/sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java
+++ b/mobile/android/base/sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java
@@ -19,24 +19,26 @@ import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.repositories.InactiveSessionException;
 import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
 import org.mozilla.gecko.sync.repositories.NoGuidForIdException;
 import org.mozilla.gecko.sync.repositories.NullCursorException;
 import org.mozilla.gecko.sync.repositories.ParentNotFoundException;
 import org.mozilla.gecko.sync.repositories.Repository;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
 import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
 import org.mozilla.gecko.sync.repositories.domain.Record;
 
 import android.content.Context;
 import android.database.Cursor;
 
 public class AndroidBrowserBookmarksRepositorySession extends AndroidBrowserRepositorySession {
 
+  public static final int DEFAULT_DELETION_FLUSH_THRESHOLD = 50;
   // TODO: synchronization for these.
   private HashMap<String, Long> guidToID = new HashMap<String, Long>();
   private HashMap<Long, String> idToGuid = new HashMap<Long, String>();
 
   /**
    * Some notes on reparenting/reordering.
    *
    * Fennec stores new items with a high-negative position, because it doesn't care.
@@ -92,16 +94,18 @@ public class AndroidBrowserBookmarksRepo
 
   // TODO: can we guarantee serial access to these?
   private HashMap<String, ArrayList<String>> missingParentToChildren = new HashMap<String, ArrayList<String>>();
   private HashMap<String, JSONArray>         parentToChildArray      = new HashMap<String, JSONArray>();
   private int needsReparenting = 0;
 
   private AndroidBrowserBookmarksDataAccessor dataAccessor;
 
+  protected BookmarksDeletionManager deletionManager;
+
   /**
    * An array of known-special GUIDs.
    */
   public static String[] SPECIAL_GUIDS = new String[] {
     // Mobile and desktop places roots have to come first.
     "places",
     "mobile",
     "toolbar",
@@ -154,17 +158,16 @@ public class AndroidBrowserBookmarksRepo
    * ----------  ----------  ----------
    * places      0           0
    * mobile      1           0
    * menu        2           0
    * etc.
    *
   */
   public static final Map<String, String> SPECIAL_GUID_PARENTS;
-
   static {
     HashMap<String, String> m = new HashMap<String, String>();
     m.put("places",  null);
     m.put("menu",    "places");
     m.put("toolbar", "places");
     m.put("tags",    "places");
     m.put("unfiled", "places");
     m.put("mobile",  "places");
@@ -533,22 +536,26 @@ public class AndroidBrowserBookmarksRepo
         guidToID.put(guid, id);
         idToGuid.put(id, guid);
         Logger.debug(LOG_TAG, "GUID " + guid + " maps to " + id);
         cur.moveToNext();
       }
     } finally {
       cur.close();
     }
+    deletionManager = new BookmarksDeletionManager(dataAccessor, DEFAULT_DELETION_FLUSH_THRESHOLD);
     Logger.debug(LOG_TAG, "Done with initial setup of bookmarks session.");
     super.begin(delegate);
   }
 
   @Override
   public void finish(RepositorySessionFinishDelegate delegate) throws InactiveSessionException {
+    // Allow this to be GCed.
+    deletionManager = null;
+
     // Override finish to do this check; make sure all records
     // needing re-parenting have been re-parented.
     if (needsReparenting != 0) {
       Logger.error(LOG_TAG, "Finish called but " + needsReparenting +
                             " bookmark(s) have been placed in unsorted bookmarks and not been reparented.");
 
       // TODO: handling of failed reparenting.
       // E.g., delegate.onFinishFailed(new BookmarkNeedsReparentingException(null));
@@ -691,45 +698,35 @@ public class AndroidBrowserBookmarksRepo
 
   @Override
   protected void storeRecordDeletion(final Record record, final Record existingRecord) {
     if (SPECIAL_GUIDS_MAP.containsKey(record.guid)) {
       Logger.debug(LOG_TAG, "Told to delete record " + record.guid + ". Ignoring.");
       return;
     }
     final BookmarkRecord bookmarkRecord = (BookmarkRecord) record;
-    if (bookmarkRecord.isFolder()) {
-      Logger.debug(LOG_TAG, "Deleting folder. Ensuring consistency of children. TODO: Bug 724470.");
-      handleFolderDeletion(bookmarkRecord);
-      return;
-    }
-    super.storeRecordDeletion(record);
+    final boolean isFolder = ((BookmarkRecord) existingRecord).isFolder();
+    deletionManager.deleteRecord(bookmarkRecord, isFolder);
   }
 
-  /**
-   * When a folder deletion is received, we must ensure -- for database
-   * consistency -- that its children are placed somewhere sane.
-   *
-   * Note that its children might also be deleted, but we'll process
-   * folders first. For that reason we might want to queue up these
-   * folder deletions and handle them in onStoreDone.
-   *
-   * See Bug 724739.
-   *
-   * @param folder
-   */
-  protected void handleFolderDeletion(final BookmarkRecord folder) {
-    // TODO: reparent children. Bug 724740.
-    // For now we'll trust that we'll process the item deletions, too.
-    super.storeRecordDeletion(folder);
+  protected void flushDeletions() {
+    Logger.debug(LOG_TAG, "Applying deletions.");
+    try {
+      long now = now();
+      untrackGUIDs(deletionManager.flushAll(getIDForGUID("unfiled"), now));
+      Logger.debug(LOG_TAG, "Done applying deletions.");
+    } catch (Exception e) {
+      Logger.error(LOG_TAG, "Unable to apply deletions.", e);
+    }
   }
 
   @SuppressWarnings("unchecked")
   private void finishUp() {
     try {
+      flushDeletions();
       Logger.debug(LOG_TAG, "Have " + parentToChildArray.size() + " folders whose children might need repositioning.");
       for (Entry<String, JSONArray> entry : parentToChildArray.entrySet()) {
         String guid = entry.getKey();
         JSONArray onServer = entry.getValue();
         try {
           final long folderID = getIDForGUID(guid);
           JSONArray inDB = getChildrenArray(folderID, false);
 
@@ -761,16 +758,42 @@ public class AndroidBrowserBookmarksRepo
           Logger.warn(LOG_TAG, "Error repositioning children for " + guid, e);
         }
       }
     } finally {
       super.storeDone();
     }
   }
 
+  /**
+   * Hook into the deletion manager on wipe.
+   */
+  class BookmarkWipeRunnable extends WipeRunnable {
+    public BookmarkWipeRunnable(RepositorySessionWipeDelegate delegate) {
+      super(delegate);
+    }
+
+    @Override
+    public void run() {
+      try {
+        super.run();
+      } catch (Exception ex) {
+        delegate.onWipeFailed(ex);
+        return;
+      }
+      // Clear our queued deletions.
+      deletionManager.clear();
+    }
+  }
+
+  @Override
+  protected WipeRunnable getWipeRunnable(RepositorySessionWipeDelegate delegate) {
+    return new BookmarkWipeRunnable(delegate);
+  }
+
   @Override
   public void storeDone() {
     Runnable command = new Runnable() {
       @Override
       public void run() {
         finishUp();
       }
     };
--- a/mobile/android/base/sync/repositories/android/AndroidBrowserRepositorySession.java
+++ b/mobile/android/base/sync/repositories/android/AndroidBrowserRepositorySession.java
@@ -650,30 +650,34 @@ public abstract class AndroidBrowserRepo
 
   protected abstract Record prepareRecord(Record record);
   protected void updateBookkeeping(Record record) throws NoGuidForIdException,
                                                  NullCursorException,
                                                  ParentNotFoundException {
     putRecordToGuidMap(buildRecordString(record), record.guid);
   }
 
-  // Wipe method and thread.
+  protected WipeRunnable getWipeRunnable(RepositorySessionWipeDelegate delegate) {
+    return new WipeRunnable(delegate);
+  }
+
   @Override
   public void wipe(RepositorySessionWipeDelegate delegate) {
-    Runnable command = new WipeRunnable(delegate);
+    Runnable command = getWipeRunnable(delegate);
     storeWorkQueue.execute(command);
   }
 
   class WipeRunnable implements Runnable {
-    private RepositorySessionWipeDelegate delegate;
+    protected RepositorySessionWipeDelegate delegate;
 
     public WipeRunnable(RepositorySessionWipeDelegate delegate) {
       this.delegate = delegate;
     }
 
+    @Override
     public void run() {
       if (!isActive()) {
         delegate.onWipeFailed(new InactiveSessionException(null));
         return;
       }
       dbHelper.wipe();
       delegate.onWipeSucceeded();
     }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/sync/repositories/android/BookmarksDeletionManager.java
@@ -0,0 +1,213 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.sync.repositories.android;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.sync.Logger;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord;
+
+/**
+ * Queue up deletions. Process them at the end.
+ *
+ * Algorithm:
+ *
+ * * Collect GUIDs as we go. For convenience we partition these into
+ *   folders and non-folders.
+ *
+ * * Non-folders can be deleted in batches as we go.
+ *
+ * * At the end of the sync:
+ *   * Delete all that aren't folders.
+ *   * Move the remaining children of any that are folders to an "Orphans" folder.
+ *     - We do this even for children that are _marked_ as deleted -- we still want
+ *       to upload them, and their parent is irrelevant.
+ *   * Delete all the folders.
+ *
+ * * Any outstanding records -- the ones we moved to "Orphans" -- are true orphans.
+ *   These should be reuploaded (because their parent has changed), as should their
+ *   new parent (because its children array has changed).
+ *   We achieve the former by moving them without tracking (but we don't make any
+ *   special effort here -- warning! Lurking bug!).
+ *   We achieve the latter by bumping its mtime. The caller should take care of untracking it.
+ *
+ * Note that we make no particular effort to handle repositioning or reparenting:
+ * batching deletes at the end should be handled seamlessly by existing code,
+ * because the deleted records could have arrived in a batch at the end regardless.
+ *
+ * Note that this class is not thread safe. This should be fine: call it only
+ * from within a store runnable.
+ *
+ */
+public class BookmarksDeletionManager {
+  private static final String LOG_TAG = "BookmarkDelete";
+
+  private final AndroidBrowserBookmarksDataAccessor dataAccessor;
+  private final int flushThreshold;
+
+  private final HashSet<String> folders    = new HashSet<String>();
+  private final HashSet<String> nonFolders = new HashSet<String>();
+  private int nonFolderCount = 0;
+
+  // Records that we need to touch once we've deleted the non-folders.
+  private HashSet<String> nonFolderParents = new HashSet<String>();
+  private HashSet<String> folderParents    = new HashSet<String>();
+
+  /**
+   * Create an instance to be used for tracking deletions in a bookmarks
+   * repository session.
+   *
+   * @param dataAccessor
+   *        Used to effect database changes.
+   *
+   * @param flushThreshold
+   *        When this many non-folder records have been stored for deletion,
+   *        an incremental flush occurs.
+   */
+  public BookmarksDeletionManager(AndroidBrowserBookmarksDataAccessor dataAccessor, int flushThreshold) {
+    this.dataAccessor = dataAccessor;
+    this.flushThreshold = flushThreshold;
+  }
+
+  public void deleteRecord(BookmarkRecord r, boolean isFolder) {
+    if (r.guid == null) {
+      Logger.warn(LOG_TAG, "Cannot queue deletion of record with no GUID.");
+      return;
+    }
+    Logger.debug(LOG_TAG, "Queuing deletion of " + r.guid);
+
+    if (isFolder) {
+      folders.add(r.guid);
+      if (!folders.contains(r.parentID)) {
+        // We're not going to delete its parent; will need to bump it.
+        folderParents.add(r.parentID);
+      }
+
+      nonFolderParents.remove(r.guid);
+      folderParents.remove(r.guid);
+      return;
+    }
+
+    if (!folders.contains(r.parentID)) {
+      // We're not going to delete its parent; will need to bump it.
+      nonFolderParents.add(r.parentID);
+    }
+
+    if (nonFolders.add(r.guid)) {
+      if (++nonFolderCount >= flushThreshold) {
+        deleteNonFolders();
+      }
+    }
+  }
+
+  /**
+   * Flush deletions that can be easily taken care of right now.
+   */
+  public void incrementalFlush() {
+    // Yes, this means we only bump when we finish, not during an incremental flush.
+    deleteNonFolders();
+  }
+
+  /**
+   * Apply all pending deletions and reset state for the next batch of stores.
+   *
+   * @param orphanDestination the ID of the folder to which orphaned children
+   *                          should be moved.
+   *
+   * @throws NullCursorException
+   * @return a set of IDs to untrack. Will not be null.
+   */
+  public Set<String> flushAll(long orphanDestination, long now) throws NullCursorException {
+    Logger.debug(LOG_TAG, "Doing complete flush of deleted items. Moving orphans to " + orphanDestination);
+    deleteNonFolders();
+
+    // Find out which parents *won't* be deleted, and thus need to have their
+    // modified times bumped.
+    nonFolderParents.removeAll(folders);
+
+    Logger.debug(LOG_TAG, "Bumping modified times for " + nonFolderParents.size() +
+                          " parents of deleted non-folders.");
+    dataAccessor.bumpModifiedByGUID(nonFolderParents, now);
+
+    if (folders.size() > 0) {
+      final String[] folderGUIDs = folders.toArray(new String[folders.size()]);
+      final String[] folderIDs = getIDs(folderGUIDs);   // Throws if any don't exist.
+      int moved = dataAccessor.moveChildren(folderIDs, orphanDestination);
+      if (moved > 0) {
+        dataAccessor.bumpModified(orphanDestination, now);
+      }
+
+      // We've deleted or moved anything that might be under these folders.
+      // Just delete them.
+      final String folderWhere = RepoUtils.computeSQLInClause(folders.size(), BrowserContract.Bookmarks.GUID);
+      dataAccessor.delete(folderWhere, folderGUIDs);
+
+      folderParents.removeAll(folders);
+      Logger.debug(LOG_TAG, "Bumping modified times for " + folderParents.size() +
+                            " parents of deleted folders.");
+      dataAccessor.bumpModifiedByGUID(folderParents, now);
+
+      // Clean up.
+      folders.clear();
+    }
+
+    HashSet<String> ret = nonFolderParents;
+    ret.addAll(folderParents);
+
+    nonFolderParents = new HashSet<String>();
+    folderParents    = new HashSet<String>();
+    return ret;
+  }
+
+  private String[] getIDs(String[] guids) throws NullCursorException {
+    // Convert GUIDs to numeric IDs.
+    String[] ids = new String[guids.length];
+    Map<String, Long> guidsToIDs = dataAccessor.idsForGUIDs(guids);
+    for (int i = 0; i < guids.length; ++i) {
+      String guid = guids[i];
+      Long id =  guidsToIDs.get(guid);
+      if (id == null) {
+        throw new IllegalArgumentException("Can't get ID for unknown record " + guid);
+      }
+      ids[i] = id.toString();
+    }
+    return ids;
+  }
+
+  /**
+   * Flush non-folder deletions. This can be called at any time.
+   */
+  private void deleteNonFolders() {
+    if (nonFolderCount == 0) {
+      Logger.debug(LOG_TAG, "No non-folders to delete.");
+      return;
+    }
+
+    Logger.debug(LOG_TAG, "Applying deletion of " + nonFolderCount + " non-folders.");
+    final String[] nonFolderGUIDs = nonFolders.toArray(new String[nonFolderCount]);
+    final String nonFolderWhere = RepoUtils.computeSQLInClause(nonFolderCount, BrowserContract.Bookmarks.GUID);
+    dataAccessor.delete(nonFolderWhere, nonFolderGUIDs);
+
+    // Discard these.
+    // Note that we maintain folderParents and nonFolderParents; we need them later.
+    nonFolders.clear();
+    nonFolderCount = 0;
+  }
+
+  /**
+   * Clear state in case of redundancy (e.g., wipe).
+   */
+  public void clear() {
+    nonFolders.clear();
+    nonFolderCount = 0;
+    folders.clear();
+    nonFolderParents.clear();
+    folderParents.clear();
+  }
+}
--- a/mobile/android/sync/java-sources.mn
+++ b/mobile/android/sync/java-sources.mn
@@ -1,1 +1,1 @@
-sync/AlreadySyncingException.java sync/CollectionKeys.java sync/CommandProcessor.java sync/CommandRunner.java sync/CredentialsSource.java sync/crypto/CryptoException.java sync/crypto/CryptoInfo.java sync/crypto/HKDF.java sync/crypto/HMACVerificationException.java sync/crypto/KeyBundle.java sync/crypto/MissingCryptoInputException.java sync/crypto/NoKeyBundleException.java sync/CryptoRecord.java sync/DelayedWorkTracker.java sync/delegates/ClientsDataDelegate.java sync/delegates/FreshStartDelegate.java sync/delegates/GlobalSessionCallback.java sync/delegates/InfoCollectionsDelegate.java sync/delegates/KeyUploadDelegate.java sync/delegates/MetaGlobalDelegate.java sync/delegates/WipeServerDelegate.java sync/ExtendedJSONObject.java sync/GlobalSession.java sync/HTTPFailureException.java sync/InfoCollections.java sync/jpake/BigIntegerHelper.java sync/jpake/Gx3OrGx4IsZeroOrOneException.java sync/jpake/IncorrectZkpException.java sync/jpake/JPakeClient.java sync/jpake/JPakeCrypto.java sync/jpake/JPakeJson.java sync/jpake/JPakeNoActivePairingException.java sync/jpake/JPakeNumGenerator.java sync/jpake/JPakeNumGeneratorRandom.java sync/jpake/JPakeParty.java sync/jpake/JPakeRequest.java sync/jpake/JPakeRequestDelegate.java sync/jpake/JPakeResponse.java sync/jpake/stage/CompleteStage.java sync/jpake/stage/ComputeFinalStage.java sync/jpake/stage/ComputeKeyVerificationStage.java sync/jpake/stage/ComputeStepOneStage.java sync/jpake/stage/ComputeStepTwoStage.java sync/jpake/stage/DecryptDataStage.java sync/jpake/stage/DeleteChannel.java sync/jpake/stage/GetChannelStage.java sync/jpake/stage/GetRequestStage.java sync/jpake/stage/JPakeStage.java sync/jpake/stage/PutRequestStage.java sync/jpake/stage/VerifyPairingStage.java sync/jpake/Zkp.java sync/KeyBundleProvider.java sync/Logger.java sync/MetaGlobal.java sync/MetaGlobalException.java sync/MetaGlobalMissingEnginesException.java sync/MetaGlobalNotSetException.java sync/middleware/Crypto5MiddlewareRepository.java sync/middleware/Crypto5MiddlewareRepositorySession.java sync/middleware/MiddlewareRepository.java sync/middleware/MiddlewareRepositorySession.java sync/net/BaseResource.java sync/net/CompletedEntity.java sync/net/ConnectionMonitorThread.java sync/net/HandleProgressException.java sync/net/HttpResponseObserver.java sync/net/Resource.java sync/net/ResourceDelegate.java sync/net/SyncResourceDelegate.java sync/net/SyncResponse.java sync/net/SyncStorageCollectionRequest.java sync/net/SyncStorageCollectionRequestDelegate.java sync/net/SyncStorageRecordRequest.java sync/net/SyncStorageRequest.java sync/net/SyncStorageRequestDelegate.java sync/net/SyncStorageRequestIncrementalDelegate.java sync/net/SyncStorageResponse.java sync/net/TLSSocketFactory.java sync/net/WBOCollectionRequestDelegate.java sync/net/WBORequestDelegate.java sync/NoCollectionKeysSetException.java sync/NodeAuthenticationException.java sync/NonArrayJSONException.java sync/NonObjectJSONException.java sync/NullClusterURLException.java sync/PrefsSource.java sync/repositories/android/AndroidBrowserBookmarksDataAccessor.java sync/repositories/android/AndroidBrowserBookmarksRepository.java sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java sync/repositories/android/AndroidBrowserHistoryDataAccessor.java sync/repositories/android/AndroidBrowserHistoryDataExtender.java sync/repositories/android/AndroidBrowserHistoryRepository.java sync/repositories/android/AndroidBrowserHistoryRepositorySession.java sync/repositories/android/AndroidBrowserPasswordsDataAccessor.java sync/repositories/android/AndroidBrowserPasswordsRepository.java sync/repositories/android/AndroidBrowserPasswordsRepositorySession.java sync/repositories/android/AndroidBrowserRepository.java sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java sync/repositories/android/AndroidBrowserRepositorySession.java sync/repositories/android/BrowserContractHelpers.java sync/repositories/android/CachedSQLiteOpenHelper.java sync/repositories/android/ClientsDatabase.java sync/repositories/android/ClientsDatabaseAccessor.java sync/repositories/android/FennecTabsRepository.java sync/repositories/android/FormHistoryRepositorySession.java sync/repositories/android/RepoUtils.java sync/repositories/BookmarkNeedsReparentingException.java sync/repositories/BookmarksRepository.java sync/repositories/ConstrainedServer11Repository.java sync/repositories/delegates/DeferrableRepositorySessionCreationDelegate.java sync/repositories/delegates/DeferredRepositorySessionBeginDelegate.java sync/repositories/delegates/DeferredRepositorySessionFetchRecordsDelegate.java sync/repositories/delegates/DeferredRepositorySessionFinishDelegate.java sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java sync/repositories/delegates/RepositorySessionBeginDelegate.java sync/repositories/delegates/RepositorySessionCleanDelegate.java sync/repositories/delegates/RepositorySessionCreationDelegate.java sync/repositories/delegates/RepositorySessionFetchRecordsDelegate.java sync/repositories/delegates/RepositorySessionFinishDelegate.java sync/repositories/delegates/RepositorySessionGuidsSinceDelegate.java sync/repositories/delegates/RepositorySessionStoreDelegate.java sync/repositories/delegates/RepositorySessionWipeDelegate.java sync/repositories/domain/BookmarkRecord.java sync/repositories/domain/BookmarkRecordFactory.java sync/repositories/domain/ClientRecord.java sync/repositories/domain/ClientRecordFactory.java sync/repositories/domain/FormHistoryRecord.java sync/repositories/domain/HistoryRecord.java sync/repositories/domain/HistoryRecordFactory.java sync/repositories/domain/PasswordRecord.java sync/repositories/domain/Record.java sync/repositories/domain/TabsRecord.java sync/repositories/HashSetStoreTracker.java sync/repositories/HistoryRepository.java sync/repositories/IdentityRecordFactory.java sync/repositories/InactiveSessionException.java sync/repositories/InvalidBookmarkTypeException.java sync/repositories/InvalidRequestException.java sync/repositories/InvalidSessionTransitionException.java sync/repositories/MultipleRecordsForGuidException.java sync/repositories/NoContentProviderException.java sync/repositories/NoGuidForIdException.java sync/repositories/NoStoreDelegateException.java sync/repositories/NullCursorException.java sync/repositories/ParentNotFoundException.java sync/repositories/ProfileDatabaseException.java sync/repositories/RecordFactory.java sync/repositories/RecordFilter.java sync/repositories/Repository.java sync/repositories/RepositorySession.java sync/repositories/RepositorySessionBundle.java sync/repositories/Server11Repository.java sync/repositories/Server11RepositorySession.java sync/repositories/StoreTracker.java sync/repositories/StoreTrackingRepositorySession.java sync/setup/activities/AccountActivity.java sync/setup/activities/ActivityUtils.java sync/setup/activities/SetupFailureActivity.java sync/setup/activities/SetupSuccessActivity.java sync/setup/activities/SetupSyncActivity.java sync/setup/Constants.java sync/setup/InvalidSyncKeyException.java sync/setup/SyncAccounts.java sync/setup/SyncAuthenticatorService.java sync/stage/AndroidBrowserBookmarksServerSyncStage.java sync/stage/AndroidBrowserHistoryServerSyncStage.java sync/stage/CheckPreconditionsStage.java sync/stage/CompletedStage.java sync/stage/EnsureClusterURLStage.java sync/stage/EnsureKeysStage.java sync/stage/FennecTabsServerSyncStage.java sync/stage/FetchInfoCollectionsStage.java sync/stage/FetchMetaGlobalStage.java sync/stage/FormHistoryServerSyncStage.java sync/stage/GlobalSyncStage.java sync/stage/NoSuchStageException.java sync/stage/NoSyncIDException.java sync/stage/ServerSyncStage.java sync/stage/SyncClientsEngineStage.java sync/StubActivity.java sync/syncadapter/SyncAdapter.java sync/syncadapter/SyncService.java sync/SyncConfiguration.java sync/SyncConfigurationException.java sync/SyncException.java sync/synchronizer/ConcurrentRecordConsumer.java sync/synchronizer/RecordConsumer.java sync/synchronizer/RecordsChannel.java sync/synchronizer/RecordsChannelDelegate.java sync/synchronizer/RecordsConsumerDelegate.java sync/synchronizer/SerialRecordConsumer.java sync/synchronizer/SessionNotBegunException.java sync/synchronizer/Synchronizer.java sync/synchronizer/SynchronizerDelegate.java sync/synchronizer/SynchronizerSession.java sync/synchronizer/SynchronizerSessionDelegate.java sync/synchronizer/UnbundleError.java sync/synchronizer/UnexpectedSessionException.java sync/SynchronizerConfiguration.java sync/SynchronizerConfigurations.java sync/ThreadPool.java sync/UnexpectedJSONException.java sync/UnknownSynchronizerConfigurationVersionException.java sync/Utils.java
+sync/AlreadySyncingException.java sync/CollectionKeys.java sync/CommandProcessor.java sync/CommandRunner.java sync/CredentialsSource.java sync/crypto/CryptoException.java sync/crypto/CryptoInfo.java sync/crypto/HKDF.java sync/crypto/HMACVerificationException.java sync/crypto/KeyBundle.java sync/crypto/MissingCryptoInputException.java sync/crypto/NoKeyBundleException.java sync/CryptoRecord.java sync/DelayedWorkTracker.java sync/delegates/ClientsDataDelegate.java sync/delegates/FreshStartDelegate.java sync/delegates/GlobalSessionCallback.java sync/delegates/InfoCollectionsDelegate.java sync/delegates/KeyUploadDelegate.java sync/delegates/MetaGlobalDelegate.java sync/delegates/WipeServerDelegate.java sync/ExtendedJSONObject.java sync/GlobalSession.java sync/HTTPFailureException.java sync/InfoCollections.java sync/jpake/BigIntegerHelper.java sync/jpake/Gx3OrGx4IsZeroOrOneException.java sync/jpake/IncorrectZkpException.java sync/jpake/JPakeClient.java sync/jpake/JPakeCrypto.java sync/jpake/JPakeJson.java sync/jpake/JPakeNoActivePairingException.java sync/jpake/JPakeNumGenerator.java sync/jpake/JPakeNumGeneratorRandom.java sync/jpake/JPakeParty.java sync/jpake/JPakeRequest.java sync/jpake/JPakeRequestDelegate.java sync/jpake/JPakeResponse.java sync/jpake/stage/CompleteStage.java sync/jpake/stage/ComputeFinalStage.java sync/jpake/stage/ComputeKeyVerificationStage.java sync/jpake/stage/ComputeStepOneStage.java sync/jpake/stage/ComputeStepTwoStage.java sync/jpake/stage/DecryptDataStage.java sync/jpake/stage/DeleteChannel.java sync/jpake/stage/GetChannelStage.java sync/jpake/stage/GetRequestStage.java sync/jpake/stage/JPakeStage.java sync/jpake/stage/PutRequestStage.java sync/jpake/stage/VerifyPairingStage.java sync/jpake/Zkp.java sync/KeyBundleProvider.java sync/Logger.java sync/MetaGlobal.java sync/MetaGlobalException.java sync/MetaGlobalMissingEnginesException.java sync/MetaGlobalNotSetException.java sync/middleware/Crypto5MiddlewareRepository.java sync/middleware/Crypto5MiddlewareRepositorySession.java sync/middleware/MiddlewareRepository.java sync/middleware/MiddlewareRepositorySession.java sync/net/BaseResource.java sync/net/CompletedEntity.java sync/net/ConnectionMonitorThread.java sync/net/HandleProgressException.java sync/net/HttpResponseObserver.java sync/net/Resource.java sync/net/ResourceDelegate.java sync/net/SyncResourceDelegate.java sync/net/SyncResponse.java sync/net/SyncStorageCollectionRequest.java sync/net/SyncStorageCollectionRequestDelegate.java sync/net/SyncStorageRecordRequest.java sync/net/SyncStorageRequest.java sync/net/SyncStorageRequestDelegate.java sync/net/SyncStorageRequestIncrementalDelegate.java sync/net/SyncStorageResponse.java sync/net/TLSSocketFactory.java sync/net/WBOCollectionRequestDelegate.java sync/net/WBORequestDelegate.java sync/NoCollectionKeysSetException.java sync/NodeAuthenticationException.java sync/NonArrayJSONException.java sync/NonObjectJSONException.java sync/NullClusterURLException.java sync/PrefsSource.java sync/repositories/android/AndroidBrowserBookmarksDataAccessor.java sync/repositories/android/AndroidBrowserBookmarksRepository.java sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java sync/repositories/android/AndroidBrowserHistoryDataAccessor.java sync/repositories/android/AndroidBrowserHistoryDataExtender.java sync/repositories/android/AndroidBrowserHistoryRepository.java sync/repositories/android/AndroidBrowserHistoryRepositorySession.java sync/repositories/android/AndroidBrowserPasswordsDataAccessor.java sync/repositories/android/AndroidBrowserPasswordsRepository.java sync/repositories/android/AndroidBrowserPasswordsRepositorySession.java sync/repositories/android/AndroidBrowserRepository.java sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java sync/repositories/android/AndroidBrowserRepositorySession.java sync/repositories/android/BookmarksDeletionManager.java sync/repositories/android/BrowserContractHelpers.java sync/repositories/android/CachedSQLiteOpenHelper.java sync/repositories/android/ClientsDatabase.java sync/repositories/android/ClientsDatabaseAccessor.java sync/repositories/android/FennecTabsRepository.java sync/repositories/android/FormHistoryRepositorySession.java sync/repositories/android/RepoUtils.java sync/repositories/BookmarkNeedsReparentingException.java sync/repositories/BookmarksRepository.java sync/repositories/ConstrainedServer11Repository.java sync/repositories/delegates/DeferrableRepositorySessionCreationDelegate.java sync/repositories/delegates/DeferredRepositorySessionBeginDelegate.java sync/repositories/delegates/DeferredRepositorySessionFetchRecordsDelegate.java sync/repositories/delegates/DeferredRepositorySessionFinishDelegate.java sync/repositories/delegates/DeferredRepositorySessionStoreDelegate.java sync/repositories/delegates/RepositorySessionBeginDelegate.java sync/repositories/delegates/RepositorySessionCleanDelegate.java sync/repositories/delegates/RepositorySessionCreationDelegate.java sync/repositories/delegates/RepositorySessionFetchRecordsDelegate.java sync/repositories/delegates/RepositorySessionFinishDelegate.java sync/repositories/delegates/RepositorySessionGuidsSinceDelegate.java sync/repositories/delegates/RepositorySessionStoreDelegate.java sync/repositories/delegates/RepositorySessionWipeDelegate.java sync/repositories/domain/BookmarkRecord.java sync/repositories/domain/BookmarkRecordFactory.java sync/repositories/domain/ClientRecord.java sync/repositories/domain/ClientRecordFactory.java sync/repositories/domain/FormHistoryRecord.java sync/repositories/domain/HistoryRecord.java sync/repositories/domain/HistoryRecordFactory.java sync/repositories/domain/PasswordRecord.java sync/repositories/domain/Record.java sync/repositories/domain/TabsRecord.java sync/repositories/HashSetStoreTracker.java sync/repositories/HistoryRepository.java sync/repositories/IdentityRecordFactory.java sync/repositories/InactiveSessionException.java sync/repositories/InvalidBookmarkTypeException.java sync/repositories/InvalidRequestException.java sync/repositories/InvalidSessionTransitionException.java sync/repositories/MultipleRecordsForGuidException.java sync/repositories/NoContentProviderException.java sync/repositories/NoGuidForIdException.java sync/repositories/NoStoreDelegateException.java sync/repositories/NullCursorException.java sync/repositories/ParentNotFoundException.java sync/repositories/ProfileDatabaseException.java sync/repositories/RecordFactory.java sync/repositories/RecordFilter.java sync/repositories/Repository.java sync/repositories/RepositorySession.java sync/repositories/RepositorySessionBundle.java sync/repositories/Server11Repository.java sync/repositories/Server11RepositorySession.java sync/repositories/StoreTracker.java sync/repositories/StoreTrackingRepositorySession.java sync/setup/activities/AccountActivity.java sync/setup/activities/ActivityUtils.java sync/setup/activities/SetupFailureActivity.java sync/setup/activities/SetupSuccessActivity.java sync/setup/activities/SetupSyncActivity.java sync/setup/Constants.java sync/setup/InvalidSyncKeyException.java sync/setup/SyncAccounts.java sync/setup/SyncAuthenticatorService.java sync/stage/AndroidBrowserBookmarksServerSyncStage.java sync/stage/AndroidBrowserHistoryServerSyncStage.java sync/stage/CheckPreconditionsStage.java sync/stage/CompletedStage.java sync/stage/EnsureClusterURLStage.java sync/stage/EnsureKeysStage.java sync/stage/FennecTabsServerSyncStage.java sync/stage/FetchInfoCollectionsStage.java sync/stage/FetchMetaGlobalStage.java sync/stage/FormHistoryServerSyncStage.java sync/stage/GlobalSyncStage.java sync/stage/NoSuchStageException.java sync/stage/NoSyncIDException.java sync/stage/ServerSyncStage.java sync/stage/SyncClientsEngineStage.java sync/StubActivity.java sync/syncadapter/SyncAdapter.java sync/syncadapter/SyncService.java sync/SyncConfiguration.java sync/SyncConfigurationException.java sync/SyncException.java sync/synchronizer/ConcurrentRecordConsumer.java sync/synchronizer/RecordConsumer.java sync/synchronizer/RecordsChannel.java sync/synchronizer/RecordsChannelDelegate.java sync/synchronizer/RecordsConsumerDelegate.java sync/synchronizer/SerialRecordConsumer.java sync/synchronizer/SessionNotBegunException.java sync/synchronizer/Synchronizer.java sync/synchronizer/SynchronizerDelegate.java sync/synchronizer/SynchronizerSession.java sync/synchronizer/SynchronizerSessionDelegate.java sync/synchronizer/UnbundleError.java sync/synchronizer/UnexpectedSessionException.java sync/SynchronizerConfiguration.java sync/SynchronizerConfigurations.java sync/ThreadPool.java sync/UnexpectedJSONException.java sync/UnknownSynchronizerConfigurationVersionException.java sync/Utils.java