Bug 709385 - Passwords repository layer. r=rnewman
authorChenxia Liu <liuche@mozilla.com>
Thu, 12 Apr 2012 00:36:58 -0700
changeset 94813 3e5b3359a4690f2fbe5c29c3a9b2b27932af2574
parent 94812 527a2b7e7f4e530d2e3939cc51dc0cc9c1326c9a
child 94814 7324a63f7855c085662098937df7b617d3209037
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)
reviewersrnewman
bugs709385
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 709385 - Passwords repository layer. r=rnewman
mobile/android/base/sync/GlobalSession.java
mobile/android/base/sync/repositories/InvalidRequestException.java
mobile/android/base/sync/repositories/android/AndroidBrowserPasswordsDataAccessor.java
mobile/android/base/sync/repositories/android/AndroidBrowserPasswordsRepository.java
mobile/android/base/sync/repositories/android/AndroidBrowserPasswordsRepositorySession.java
mobile/android/base/sync/repositories/android/BrowserContractHelpers.java
mobile/android/base/sync/repositories/android/PasswordsRepositorySession.java
mobile/android/base/sync/repositories/android/RepoUtils.java
mobile/android/base/sync/repositories/domain/PasswordRecord.java
mobile/android/base/sync/stage/GlobalSyncStage.java
mobile/android/base/sync/stage/PasswordsServerSyncStage.java
mobile/android/sync/java-sources.mn
--- a/mobile/android/base/sync/GlobalSession.java
+++ b/mobile/android/base/sync/GlobalSession.java
@@ -28,16 +28,17 @@ import org.mozilla.gecko.sync.net.HttpRe
 import org.mozilla.gecko.sync.net.SyncResponse;
 import org.mozilla.gecko.sync.net.SyncStorageRecordRequest;
 import org.mozilla.gecko.sync.net.SyncStorageRequest;
 import org.mozilla.gecko.sync.net.SyncStorageRequestDelegate;
 import org.mozilla.gecko.sync.net.SyncStorageResponse;
 import org.mozilla.gecko.sync.stage.AndroidBrowserBookmarksServerSyncStage;
 import org.mozilla.gecko.sync.stage.FormHistoryServerSyncStage;
 import org.mozilla.gecko.sync.stage.AndroidBrowserHistoryServerSyncStage;
+import org.mozilla.gecko.sync.stage.PasswordsServerSyncStage;
 import org.mozilla.gecko.sync.stage.CheckPreconditionsStage;
 import org.mozilla.gecko.sync.stage.CompletedStage;
 import org.mozilla.gecko.sync.stage.EnsureClusterURLStage;
 import org.mozilla.gecko.sync.stage.EnsureKeysStage;
 import org.mozilla.gecko.sync.stage.FennecTabsServerSyncStage;
 import org.mozilla.gecko.sync.stage.FetchInfoCollectionsStage;
 import org.mozilla.gecko.sync.stage.FetchMetaGlobalStage;
 import org.mozilla.gecko.sync.stage.GlobalSyncStage;
@@ -192,16 +193,17 @@ public class GlobalSession implements Cr
     stages.put(Stage.ensureClusterURL,        new EnsureClusterURLStage());
     stages.put(Stage.fetchInfoCollections,    new FetchInfoCollectionsStage());
     stages.put(Stage.fetchMetaGlobal,         new FetchMetaGlobalStage());
     stages.put(Stage.ensureKeysStage,         new EnsureKeysStage());
     stages.put(Stage.syncClientsEngine,       new SyncClientsEngineStage());
 
     // TODO: more stages.
     stages.put(Stage.syncTabs,                new FennecTabsServerSyncStage());
+    stages.put(Stage.syncPasswords,           new PasswordsServerSyncStage());
     stages.put(Stage.syncBookmarks,           new AndroidBrowserBookmarksServerSyncStage());
     stages.put(Stage.syncHistory,             new AndroidBrowserHistoryServerSyncStage());
     stages.put(Stage.syncFormHistory,         new FormHistoryServerSyncStage());
     stages.put(Stage.completed,               new CompletedStage());
   }
 
   protected GlobalSyncStage getStageByName(Stage next) throws NoSuchStageException {
     GlobalSyncStage stage = stages.get(next);
--- a/mobile/android/base/sync/repositories/InvalidRequestException.java
+++ b/mobile/android/base/sync/repositories/InvalidRequestException.java
@@ -41,10 +41,9 @@ import org.mozilla.gecko.sync.SyncExcept
 
 public class InvalidRequestException extends SyncException {
 
   private static final long serialVersionUID = 4502951350743608243L;
 
   public InvalidRequestException(Exception ex) {
     super(ex);
   }
-
 }
deleted file mode 100644
--- a/mobile/android/base/sync/repositories/android/AndroidBrowserPasswordsDataAccessor.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/* 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 org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.sync.repositories.domain.PasswordRecord;
-import org.mozilla.gecko.sync.repositories.domain.Record;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.net.Uri;
-
-public class AndroidBrowserPasswordsDataAccessor extends AndroidBrowserRepositoryDataAccessor {
-
-  public AndroidBrowserPasswordsDataAccessor(Context context) {
-    super(context);
-  }
-
-  @Override
-  protected ContentValues getContentValues(Record record) {
-    PasswordRecord rec = (PasswordRecord) record;
-
-    ContentValues cv = new ContentValues();
-    cv.put(BrowserContract.Passwords.GUID,            rec.guid);
-    cv.put(BrowserContract.Passwords.HOSTNAME,        rec.hostname);
-    cv.put(BrowserContract.Passwords.HTTP_REALM,      rec.httpRealm);
-    cv.put(BrowserContract.Passwords.FORM_SUBMIT_URL, rec.formSubmitURL);
-    cv.put(BrowserContract.Passwords.USERNAME_FIELD,  rec.usernameField);
-    cv.put(BrowserContract.Passwords.PASSWORD_FIELD,  rec.passwordField);
-    
-    // TODO Do encryption of username/password here. Bug 711636
-    cv.put(BrowserContract.Passwords.ENC_TYPE,           rec.encType);
-    cv.put(BrowserContract.Passwords.ENCRYPTED_USERNAME, rec.username);
-    cv.put(BrowserContract.Passwords.ENCRYPTED_PASSWORD, rec.password);
-    
-    cv.put(BrowserContract.Passwords.TIMES_USED,     rec.timesUsed);
-    cv.put(BrowserContract.Passwords.TIME_LAST_USED, rec.timeLastUsed);
-    return cv;
-  }
-
-  @Override
-  protected Uri getUri() {
-    return BrowserContractHelpers.PASSWORDS_CONTENT_URI;
-  }
-
-  @Override
-  protected String[] getAllColumns() {
-    return BrowserContractHelpers.PasswordColumns;
-  }
-}
deleted file mode 100644
--- a/mobile/android/base/sync/repositories/android/AndroidBrowserPasswordsRepository.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Android Sync Client.
- *
- * The Initial Developer of the Original Code is
- * the Mozilla Foundation.
- * Portions created by the Initial Developer are Copyright (C) 2011
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- * Jason Voll <jvoll@mozilla.com>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
-
-package org.mozilla.gecko.sync.repositories.android;
-
-import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
-
-import android.content.Context;
-
-public class AndroidBrowserPasswordsRepository extends AndroidBrowserRepository {
-
-  @Override
-  protected AndroidBrowserRepositoryDataAccessor getDataAccessor(Context context) {
-    return new AndroidBrowserPasswordsDataAccessor(context);
-  }
-
-  @Override
-  protected void sessionCreator(RepositorySessionCreationDelegate delegate,
-      Context context) {
-    AndroidBrowserPasswordsRepositorySession session = new AndroidBrowserPasswordsRepositorySession(AndroidBrowserPasswordsRepository.this, context);
-    delegate.onSessionCreated(session);
-  }
-
-}
deleted file mode 100644
--- a/mobile/android/base/sync/repositories/android/AndroidBrowserPasswordsRepositorySession.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/* 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 org.mozilla.gecko.sync.repositories.Repository;
-import org.mozilla.gecko.sync.repositories.domain.PasswordRecord;
-import org.mozilla.gecko.sync.repositories.domain.Record;
-
-import android.content.Context;
-import android.database.Cursor;
-
-public class AndroidBrowserPasswordsRepositorySession extends
-    AndroidBrowserRepositorySession {
-
-  public AndroidBrowserPasswordsRepositorySession(Repository repository, Context context) {
-    super(repository);
-    dbHelper = new AndroidBrowserPasswordsDataAccessor(context);
-  }
-
-  @Override
-  protected Record retrieveDuringStore(Cursor cur) {
-    return RepoUtils.passwordFromMirrorCursor(cur);
-  }
-
-  @Override
-  protected Record retrieveDuringFetch(Cursor cur) {
-    return RepoUtils.passwordFromMirrorCursor(cur);
-  }
-
-  @Override
-  protected String buildRecordString(Record record) {
-    PasswordRecord rec = (PasswordRecord) record;
-    return rec.hostname + rec.formSubmitURL + rec.httpRealm + rec.username;
-  }
-
-  @Override
-  protected Record prepareRecord(Record record) {
-    return record;
-  }
-}
--- a/mobile/android/base/sync/repositories/android/BrowserContractHelpers.java
+++ b/mobile/android/base/sync/repositories/android/BrowserContractHelpers.java
@@ -8,59 +8,55 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
 import org.mozilla.gecko.db.BrowserContract;
 
 import android.net.Uri;
 
 public class BrowserContractHelpers extends BrowserContract {
-  protected static Uri withSync(Uri u) {
-    return u.buildUpon()
-            .appendQueryParameter(PARAM_IS_SYNC, "true")
-            .build();
-  }
 
   protected static Uri withSyncAndDeleted(Uri u) {
     return u.buildUpon()
             .appendQueryParameter(PARAM_IS_SYNC, "true")
             .appendQueryParameter(PARAM_SHOW_DELETED, "true")
             .build();
   }
+  protected static Uri withSync(Uri u) {
+    return u.buildUpon()
+        .appendQueryParameter(PARAM_IS_SYNC, "true")
+        .build();
+  }
 
   public static final Uri IMAGES_CONTENT_URI               = withSyncAndDeleted(Images.CONTENT_URI);
   public static final Uri BOOKMARKS_CONTENT_URI            = withSyncAndDeleted(Bookmarks.CONTENT_URI);
   public static final Uri BOOKMARKS_PARENTS_CONTENT_URI    = withSyncAndDeleted(Bookmarks.PARENTS_CONTENT_URI);
   public static final Uri BOOKMARKS_POSITIONS_CONTENT_URI  = withSyncAndDeleted(Bookmarks.POSITIONS_CONTENT_URI);
   public static final Uri HISTORY_CONTENT_URI              = withSyncAndDeleted(History.CONTENT_URI);
   public static final Uri SCHEMA_CONTENT_URI               = withSyncAndDeleted(Schema.CONTENT_URI);
-
-  public static final Uri PASSWORDS_CONTENT_URI            = null;
-  /*
   public static final Uri PASSWORDS_CONTENT_URI            = withSyncAndDeleted(Passwords.CONTENT_URI);
-   */
+  public static final Uri DELETED_PASSWORDS_CONTENT_URI    = withSyncAndDeleted(DeletedPasswords.CONTENT_URI);
   public static final Uri FORM_HISTORY_CONTENT_URI         = withSync(FormHistory.CONTENT_URI);
   public static final Uri DELETED_FORM_HISTORY_CONTENT_URI = withSync(DeletedFormHistory.CONTENT_URI);
 
   public static final String[] PasswordColumns = new String[] {
-    CommonColumns._ID,
-    SyncColumns.GUID,
-    SyncColumns.DATE_CREATED,
-    SyncColumns.DATE_MODIFIED,
-    SyncColumns.IS_DELETED,
+    Passwords.ID,
     Passwords.HOSTNAME,
     Passwords.HTTP_REALM,
     Passwords.FORM_SUBMIT_URL,
     Passwords.USERNAME_FIELD,
     Passwords.PASSWORD_FIELD,
     Passwords.ENCRYPTED_USERNAME,
     Passwords.ENCRYPTED_PASSWORD,
     Passwords.ENC_TYPE,
+    Passwords.TIME_CREATED,
     Passwords.TIME_LAST_USED,
-    Passwords.TIMES_USED
+    Passwords.TIME_PASSWORD_CHANGED,
+    Passwords.TIMES_USED,
+    Passwords.GUID
   };
 
   public static final String[] HistoryColumns = new String[] {
     CommonColumns._ID,
     SyncColumns.GUID,
     SyncColumns.DATE_CREATED,
     SyncColumns.DATE_MODIFIED,
     SyncColumns.IS_DELETED,
@@ -81,31 +77,30 @@ public class BrowserContractHelpers exte
     Bookmarks.TYPE,
     Bookmarks.PARENT,
     Bookmarks.POSITION,
     Bookmarks.TAGS,
     Bookmarks.DESCRIPTION,
     Bookmarks.KEYWORD
   };
 
-
   public static final String[] FormHistoryColumns = new String[] {
     FormHistory.ID,
     FormHistory.GUID,
     FormHistory.FIELD_NAME,
     FormHistory.VALUE,
     FormHistory.TIMES_USED,
     FormHistory.FIRST_USED,
     FormHistory.LAST_USED
   };
 
   public static final String[] DeletedColumns = new String[] {
-    DeletedFormHistory.ID,
-    DeletedFormHistory.GUID,
-    DeletedFormHistory.TIME_DELETED
+    BrowserContract.DeletedColumns.ID,
+    BrowserContract.DeletedColumns.GUID,
+    BrowserContract.DeletedColumns.TIME_DELETED
   };
 
   // Mapping from Sync types to Fennec types.
   public static final String[] BOOKMARK_TYPE_CODE_TO_STRING = {
     // Observe omissions: "microsummary", "item".
     "folder", "bookmark", "separator", "livemark", "query"
   };
   private static final int MAX_BOOKMARK_TYPE_CODE = BOOKMARK_TYPE_CODE_TO_STRING.length - 1;
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/sync/repositories/android/PasswordsRepositorySession.java
@@ -0,0 +1,737 @@
+/* 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.ArrayList;
+import java.util.List;
+
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserContract.DeletedColumns;
+import org.mozilla.gecko.db.BrowserContract.DeletedPasswords;
+import org.mozilla.gecko.db.BrowserContract.Passwords;
+import org.mozilla.gecko.sync.Logger;
+import org.mozilla.gecko.sync.repositories.InactiveSessionException;
+import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
+import org.mozilla.gecko.sync.repositories.NullCursorException;
+import org.mozilla.gecko.sync.repositories.RecordFilter;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession;
+import org.mozilla.gecko.sync.repositories.android.RepoUtils.QueryHelper;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
+import org.mozilla.gecko.sync.repositories.domain.PasswordRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import android.content.ContentProviderClient;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+
+public class PasswordsRepositorySession extends
+    StoreTrackingRepositorySession {
+
+  public static class PasswordsRepository extends Repository {
+    @Override
+    public void createSession(RepositorySessionCreationDelegate delegate,
+        Context context) {
+      PasswordsRepositorySession session = new PasswordsRepositorySession(PasswordsRepository.this, context);
+      final RepositorySessionCreationDelegate deferredCreationDelegate = delegate.deferredCreationDelegate();
+      deferredCreationDelegate.onSessionCreated(session);
+    }
+  }
+
+  private static final String LOG_TAG = "PasswordsRepoSession";
+  private static String COLLECTION = "passwords";
+
+  private RepoUtils.QueryHelper passwordsHelper;
+  private RepoUtils.QueryHelper deletedPasswordsHelper;
+  private ContentProviderClient passwordsProvider;
+
+  private final Context context;
+
+  public PasswordsRepositorySession(Repository repository, Context context) {
+    super(repository);
+    this.context = context;
+    this.passwordsHelper        = new QueryHelper(context, BrowserContractHelpers.PASSWORDS_CONTENT_URI, LOG_TAG);
+    this.deletedPasswordsHelper = new QueryHelper(context, BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, LOG_TAG);
+    this.passwordsProvider      = context.getContentResolver().acquireContentProviderClient(BrowserContract.PASSWORDS_AUTHORITY_URI);
+    dumpDbs();
+  }
+
+  private static final String[] GUID_COLS = new String[] { Passwords.GUID };
+  private static final String[] DELETED_GUID_COLS = new String[] { DeletedColumns.GUID };
+
+  private static final String WHERE_GUID_IS = Passwords.GUID + " = ?";
+  private static final String WHERE_DELETED_GUID_IS = DeletedPasswords.GUID + " = ?";
+
+  @Override
+  public void guidsSince(final long timestamp, final RepositorySessionGuidsSinceDelegate delegate) {
+    final Runnable guidsSinceRunnable = new Runnable() {
+      @Override
+      public void run() {
+
+        if (!isActive()) {
+          delegate.onGuidsSinceFailed(new InactiveSessionException(null));
+          return;
+        }
+
+        // Checks succeeded, now get GUIDs.
+        final List<String> guids = new ArrayList<String>();
+        try {
+          Logger.debug(LOG_TAG, "Fetching guidsSince from data table.");
+          final Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".getGUIDsSince", GUID_COLS, dateModifiedWhere(timestamp), null, null);
+          try {
+            if (data.moveToFirst()) {
+              while (!data.isAfterLast()) {
+                guids.add(RepoUtils.getStringFromCursor(data, Passwords.GUID));
+                data.moveToNext();
+              }
+            }
+          } finally {
+            data.close();
+          }
+
+          // Fetch guids from deleted table.
+          Logger.debug(LOG_TAG, "Fetching guidsSince from deleted table.");
+          final Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".getGUIDsSince", DELETED_GUID_COLS, dateModifiedWhereDeleted(timestamp), null, null);
+          try {
+            if (deleted.moveToFirst()) {
+              while (!deleted.isAfterLast()) {
+                guids.add(RepoUtils.getStringFromCursor(deleted, DeletedColumns.GUID));
+                deleted.moveToNext();
+              }
+            }
+          } finally {
+            deleted.close();
+          }
+        } catch (Exception e) {
+          Logger.error(LOG_TAG, "Exception in fetch.");
+          delegate.onGuidsSinceFailed(e);
+          return;
+        }
+        String[] guidStrings = new String[guids.size()];
+        delegate.onGuidsSinceSucceeded(guids.toArray(guidStrings));
+      }
+    };
+
+    delegateQueue.execute(guidsSinceRunnable);
+  }
+
+  @Override
+  public void fetchSince(final long timestamp, final RepositorySessionFetchRecordsDelegate delegate) {
+    final RecordFilter filter = this.storeTracker.getFilter();
+    final Runnable fetchSinceRunnable = new Runnable() {
+      @Override
+      public void run() {
+        if (!isActive()) {
+          delegate.onFetchFailed(new InactiveSessionException(null), null);
+          return;
+        }
+
+        final long end = now();
+        try {
+          // Fetch from data table.
+          Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".fetchSince",
+                                                  getAllColumns(),
+                                                  dateModifiedWhere(timestamp),
+                                                  null, null);
+          if (!fetchAndCloseCursorDeleted(data, false, filter, delegate)) {
+            return;
+          }
+
+          // Fetch from deleted table.
+          Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".fetchSince",
+                                                            getAllDeletedColumns(),
+                                                            dateModifiedWhereDeleted(timestamp),
+                                                            null, null);
+          if (!fetchAndCloseCursorDeleted(deleted, true, filter, delegate)) {
+            return;
+          }
+
+          // Success!
+          try {
+            delegate.onFetchCompleted(end);
+          } catch (Exception e) {
+            Logger.error(LOG_TAG, "Delegate fetch completed callback failed.", e);
+            // Don't call failure callback.
+            return;
+          }
+        } catch (Exception e) {
+          Logger.error(LOG_TAG, "Exception in fetch.");
+          delegate.onFetchFailed(e, null);
+        }
+      }
+    };
+
+    delegateQueue.execute(fetchSinceRunnable);
+  }
+
+  @Override
+  public void fetch(final String[] guids, final RepositorySessionFetchRecordsDelegate delegate) {
+    if (guids == null || guids.length < 1) {
+      Logger.error(LOG_TAG, "No guids to be fetched.");
+      final long end = now();
+      delegateQueue.execute(new Runnable() {
+        @Override
+        public void run() {
+          delegate.onFetchCompleted(end);
+        }
+      });
+      return;
+    }
+
+    // Checks succeeded, now fetch.
+    final RecordFilter filter = this.storeTracker.getFilter();
+    final Runnable fetchRunnable = new Runnable() {
+      @Override
+      public void run() {
+        if (!isActive()) {
+          delegate.onFetchFailed(new InactiveSessionException(null), null);
+          return;
+        }
+
+        final long end = now();
+        final String where = RepoUtils.computeSQLInClause(guids.length, "guid");
+        Logger.trace(LOG_TAG, "Fetch guids where: " + where);
+
+        try {
+          // Fetch records from data table.
+          Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".fetch",
+                                                  getAllColumns(),
+                                                  where, guids, null);
+          if (!fetchAndCloseCursorDeleted(data, false, filter, delegate)) {
+            return;
+          }
+
+          // Fetch records from deleted table.
+          Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".fetch",
+                                                            getAllDeletedColumns(),
+                                                            where, guids, null);
+          if (!fetchAndCloseCursorDeleted(deleted, true, filter, delegate)) {
+            return;
+          }
+
+          delegate.onFetchCompleted(end);
+
+        } catch (Exception e) {
+          Logger.error(LOG_TAG, "Exception in fetch.");
+          delegate.onFetchFailed(e, null);
+        }
+      }
+    };
+
+    delegateQueue.execute(fetchRunnable);
+  }
+
+  @Override
+  public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) {
+    fetchSince(0, delegate);
+  }
+
+  @Override
+  public void store(final Record record) throws NoStoreDelegateException {
+    if (delegate == null) {
+      Logger.error(LOG_TAG, "No store delegate.");
+      throw new NoStoreDelegateException();
+    }
+    if (record == null) {
+      Logger.error(LOG_TAG, "Record sent to store was null.");
+      throw new IllegalArgumentException("Null record passed to PasswordsRepositorySession.store().");
+    }
+    if (!(record instanceof PasswordRecord)) {
+      Logger.error(LOG_TAG, "Can't store anything but a PasswordRecord.");
+      throw new IllegalArgumentException("Non-PasswordRecord passed to PasswordsRepositorySession.store().");
+    }
+
+    final PasswordRecord remoteRecord = (PasswordRecord) record;
+
+    final Runnable storeRunnable = new Runnable() {
+      @Override
+      public void run() {
+        if (!isActive()) {
+          Logger.warn(LOG_TAG, "RepositorySession is inactive. Store failing.");
+          delegate.onRecordStoreFailed(new InactiveSessionException(null));
+          return;
+        }
+
+        final String guid = remoteRecord.guid;
+        if (guid == null) {
+          delegate.onRecordStoreFailed(new RuntimeException("Can't store record with null GUID."));
+          return;
+        }
+
+        PasswordRecord existingRecord;
+        try {
+          existingRecord = retrieveByGUID(guid);
+        } catch (NullCursorException e) {
+          // Indicates a serious problem.
+          delegate.onRecordStoreFailed(e);
+          return;
+        } catch (RemoteException e) {
+          delegate.onRecordStoreFailed(e);
+          return;
+        }
+
+        long lastLocalRetrieval  = 0;      // lastSyncTimestamp?
+        long lastRemoteRetrieval = 0;      // TODO: adjust for clock skew.
+        boolean remotelyModified = remoteRecord.lastModified > lastRemoteRetrieval;
+
+        // Check deleted state first.
+        if (remoteRecord.deleted) {
+          if (existingRecord == null) {
+            // Do nothing, record does not exist anyways.
+            Logger.info(LOG_TAG, "Incoming record " + remoteRecord.guid + " is deleted, and no local version.");
+            return;
+          }
+
+          if (existingRecord.deleted) {
+            // Record is already tracked as deleted. Delete from local.
+            storeRecordDeletion(existingRecord); // different from ABRepoSess.
+            Logger.info(LOG_TAG, "Incoming record " + remoteRecord.guid + " and local are both deleted.");
+            return;
+          }
+
+          // Which one wins?
+          if (!remotelyModified) {
+            trace("Ignoring deleted record from the past.");
+            return;
+          }
+
+          boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval;
+          if (!locallyModified) {
+            trace("Remote modified, local not. Deleting.");
+            storeRecordDeletion(remoteRecord);
+            return;
+          }
+
+          trace("Both local and remote records have been modified.");
+          if (remoteRecord.lastModified > existingRecord.lastModified) {
+            trace("Remote is newer, and deleted. Deleting local.");
+            storeRecordDeletion(remoteRecord);
+            return;
+          }
+
+          trace("Remote is older, local is not deleted. Ignoring.");
+          if (!locallyModified) {
+            Logger.warn(LOG_TAG, "Inconsistency: old remote record is deleted, but local record not modified!");
+            // Ensure that this is tracked for upload.
+          }
+          return;
+        }
+        // End deletion logic.
+
+        // Now we're processing a non-deleted incoming record.
+        if (existingRecord == null) {
+          trace("Looking up match for record " + remoteRecord.guid);
+          existingRecord = findExistingRecord(remoteRecord);
+        }
+
+        if (existingRecord == null) {
+          // The record is new.
+          trace("No match. Inserting.");
+          Logger.debug(LOG_TAG, "Didn't find matching record. Inserting.");
+          Record inserted = null;
+          try {
+            inserted = insert(remoteRecord);
+          } catch (RemoteException e) {
+            Logger.debug(LOG_TAG, "Record insert caused a RemoteException.");
+            delegate.onRecordStoreFailed(e);
+            return;
+          }
+          trackRecord(inserted);
+          delegate.onRecordStoreSucceeded(inserted);
+          return;
+        }
+
+        // We found a local dupe.
+        trace("Incoming record " + remoteRecord.guid + " dupes to local record " + existingRecord.guid);
+        Logger.debug(LOG_TAG, "remote " + remoteRecord + " dupes to " + existingRecord);
+        Record toStore = reconcileRecords(remoteRecord, existingRecord, lastRemoteRetrieval, lastLocalRetrieval);
+
+        if (toStore == null) {
+          Logger.debug(LOG_TAG, "Reconciling returned null. Not inserting a record.");
+          return;
+        }
+
+        // TODO: pass in timestamps?
+        Logger.debug(LOG_TAG, "Replacing " + existingRecord.guid + " with record " + toStore.guid);
+        Logger.debug(LOG_TAG, "existing: " + existingRecord);
+        Logger.debug(LOG_TAG, "toStore: " + toStore);
+        Record replaced = null;
+        try {
+          replaced = replace(existingRecord, toStore);
+        } catch (RemoteException e) {
+          Logger.debug(LOG_TAG, "Record replace caused a RemoteException.");
+          delegate.onRecordStoreFailed(e);
+          return;
+        }
+
+        // Note that we don't track records here; deciding that is the job
+        // of reconcileRecords.
+        Logger.debug(LOG_TAG, "Calling delegate callback with guid " + replaced.guid +
+                              "(" + replaced.androidID + ")");
+        delegate.onRecordStoreSucceeded(replaced);
+        return;
+      }
+    };
+    storeWorkQueue.execute(storeRunnable);
+  }
+
+  @Override
+  public void wipe(final RepositorySessionWipeDelegate delegate) {
+    Logger.info(LOG_TAG, "Wiping " + BrowserContractHelpers.PASSWORDS_CONTENT_URI + ", " + BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI);
+
+    Runnable wipeRunnable = new Runnable() {
+      @Override
+      public void run() {
+        if (!isActive()) {
+          delegate.onWipeFailed(new InactiveSessionException(null));
+          return;
+        }
+
+        // Wipe both data and deleted.
+        try {
+          context.getContentResolver().delete(BrowserContractHelpers.PASSWORDS_CONTENT_URI, null, null);
+          context.getContentResolver().delete(BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, null, null);
+        } catch (Exception e) {
+          delegate.onWipeFailed(e);
+          return;
+        }
+        delegate.onWipeSucceeded();
+      }
+    };
+    storeWorkQueue.execute(wipeRunnable);
+  }
+
+  @Override
+  public void abort() {
+    passwordsProvider.release();
+    super.abort();
+  }
+
+  @Override
+  public void finish(final RepositorySessionFinishDelegate delegate) throws InactiveSessionException {
+    passwordsProvider.release();
+    super.finish(delegate);
+  }
+
+  public void deleteGUID(String guid) throws RemoteException {
+    final String[] args = new String[] { guid };
+
+    int deleted = passwordsProvider.delete(BrowserContractHelpers.PASSWORDS_CONTENT_URI, WHERE_GUID_IS, args) +
+                  passwordsProvider.delete(BrowserContractHelpers.DELETED_PASSWORDS_CONTENT_URI, WHERE_DELETED_GUID_IS, args);
+    if (deleted == 1) {
+      return;
+    }
+    Logger.warn(LOG_TAG, "Unexpectedly deleted " + deleted + " rows for guid " + guid);
+  }
+
+  /**
+   * Insert record and return the record with its updated androidId set.
+   * @param record
+   * @return
+   * @throws RemoteException
+   */
+  public PasswordRecord insert(PasswordRecord record) throws RemoteException {
+    record.timePasswordChanged = now();
+    // TODO: are these necessary for Fennec autocomplete?
+    // record.timesUsed = 1;
+    // record.timeLastUsed = now();
+    ContentValues cv = getContentValues(record);
+    Uri insertedUri = passwordsProvider.insert(BrowserContractHelpers.PASSWORDS_CONTENT_URI, cv);
+    record.androidID = RepoUtils.getAndroidIdFromUri(insertedUri);
+    return record;
+  }
+
+  public Record replace(Record origRecord, Record newRecord) throws RemoteException {
+    PasswordRecord newPasswordRecord = (PasswordRecord) newRecord;
+    PasswordRecord origPasswordRecord = (PasswordRecord) origRecord;
+    propagateTimes(newPasswordRecord, origPasswordRecord);
+    ContentValues cv = getContentValues(newPasswordRecord);
+
+    final String[] args = new String[] { origRecord.guid };
+
+    int updated = context.getContentResolver().update(BrowserContractHelpers.PASSWORDS_CONTENT_URI, cv, WHERE_GUID_IS, args);
+    if (updated != 1) {
+      Logger.warn(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + origPasswordRecord.guid);
+    }
+    return newRecord;
+  }
+
+  // When replacing a record, propagate the times.
+  private static void propagateTimes(PasswordRecord toRecord, PasswordRecord fromRecord) {
+    toRecord.timePasswordChanged = now();
+    toRecord.timeCreated  = fromRecord.timeCreated;
+    toRecord.timeLastUsed = fromRecord.timeLastUsed;
+    toRecord.timesUsed    = fromRecord.timesUsed;
+  }
+
+  private static String[] getAllColumns() {
+    return BrowserContractHelpers.PasswordColumns;
+  }
+
+  private static String[] getAllDeletedColumns() {
+    return BrowserContractHelpers.DeletedColumns;
+  }
+
+  /**
+   * Constructs the DB query string for entry age for deleted records.
+   *
+   * @param timestamp
+   * @return String DB query string for dates to fetch.
+   */
+  private static String dateModifiedWhereDeleted(long timestamp) {
+    return DeletedColumns.TIME_DELETED + " >= " + Long.toString(timestamp);
+  }
+
+  /**
+   * Constructs the DB query string for entry age for (undeleted) records.
+   *
+   * @param timestamp
+   * @return String DB query string for dates to fetch.
+   */
+  private static String dateModifiedWhere(long timestamp) {
+    return Passwords.TIME_PASSWORD_CHANGED + " >= " + Long.toString(timestamp);
+  }
+
+
+  /**
+   * Fetch from the cursor with the given parameters, invoking
+   * delegate callbacks and closing the cursor.
+   * Returns true on success, false if failure was signaled.
+   *
+   * @param cursor
+            fetch* cursor.
+   * @param deleted
+   *        true if using deleted table, false when using data table.
+   * @param delegate
+   *        FetchRecordsDelegate to process records.
+   */
+  private static boolean fetchAndCloseCursorDeleted(final Cursor cursor,
+                                                    final boolean deleted,
+                                                    final RecordFilter filter,
+                                                    final RepositorySessionFetchRecordsDelegate delegate) {
+    if (cursor == null) {
+      return true;
+    }
+
+    try {
+      while (cursor.moveToNext()) {
+        Record r = deleted ? deletedPasswordRecordFromCursor(cursor) : passwordRecordFromCursor(cursor);
+        if (r != null) {
+          if (filter == null || !filter.excludeRecord(r)) {
+            Logger.debug(LOG_TAG, "Fetched record " + r);
+            delegate.onFetchedRecord(r);
+          } else {
+            Logger.debug(LOG_TAG, "Skipping filtered record " + r.guid);
+          }
+        }
+      }
+    } catch (Exception e) {
+      Logger.error(LOG_TAG, "Exception in fetch.");
+      delegate.onFetchFailed(e, null);
+      return false;
+    } finally {
+      cursor.close();
+    }
+
+    return true;
+  }
+
+  private PasswordRecord retrieveByGUID(String guid) throws NullCursorException, RemoteException {
+    final String[] guidArg = new String[] { guid };
+
+    // Check data table.
+    final Cursor data = passwordsHelper.safeQuery(passwordsProvider, ".store", BrowserContractHelpers.PasswordColumns, WHERE_GUID_IS, guidArg, null);
+    try {
+      if (data.moveToFirst()) {
+        return passwordRecordFromCursor(data);
+      }
+    } finally {
+      data.close();
+    }
+
+    // Check deleted table.
+    final Cursor deleted = deletedPasswordsHelper.safeQuery(passwordsProvider, ".retrieveByGuid", BrowserContractHelpers.DeletedColumns, WHERE_DELETED_GUID_IS, guidArg, null);
+    try {
+      if (deleted.moveToFirst()) {
+        return deletedPasswordRecordFromCursor(deleted);
+      }
+    } finally {
+      deleted.close();
+    }
+
+    return null;
+  }
+
+  // TODO: Bug 738347 - SQLiteBridge does not check for nulls in ContentValues.
+  // Passwords.HTTP_REALM + " = ? AND " +
+  private static final String WHERE_RECORD_DATA =
+    Passwords.HOSTNAME        + " = ? AND " +
+    Passwords.FORM_SUBMIT_URL + " = ? AND " +
+    Passwords.USERNAME_FIELD  + " = ? AND " +
+    Passwords.PASSWORD_FIELD  + " = ?";
+
+  private PasswordRecord findExistingRecord(PasswordRecord record) {
+    PasswordRecord foundRecord = null;
+    Cursor cursor = null;
+    // Only check the data table.
+    // We can't encrypt username directly for query, so run a more general query and then filter.
+    final String[] whereArgs = new String[] {
+      record.hostname,
+      record.formSubmitURL,
+      record.usernameField,
+      record.passwordField
+
+      // TODO: Bug 738347 - SQLiteBridge does not check for nulls in ContentValues.
+      // record.httpRealm
+    };
+
+    try {
+      cursor = passwordsHelper.safeQuery(passwordsProvider, ".findRecord", getAllColumns(), WHERE_RECORD_DATA, whereArgs, null);
+      while (cursor.moveToNext()) {
+        foundRecord = passwordRecordFromCursor(cursor);
+
+        // We don't directly query for username because the
+        // username/password values are encrypted in the db.
+        // We don't have the keys for encrypting our query,
+        // so we run a more general query and then filter
+        // the returned records for a matching username.
+        Logger.trace(LOG_TAG, "Checking incoming [" + record.encryptedUsername + "] to [" + foundRecord.encryptedUsername + "]");
+        if (record.encryptedUsername.equals(foundRecord.encryptedUsername)) {
+          Logger.trace(LOG_TAG, "Found matching record: " + foundRecord);
+          return foundRecord;
+        }
+      }
+    } catch (RemoteException e) {
+      Logger.error(LOG_TAG, "Remote exception in findExistingRecord.");
+      delegate.onRecordStoreFailed(e);
+    } catch (NullCursorException e) {
+      Logger.error(LOG_TAG, "Null cursor in findExistingRecord.");
+      delegate.onRecordStoreFailed(e);
+    } finally {
+      if (cursor != null) {
+        cursor.close();
+      }
+    }
+    Logger.debug(LOG_TAG, "No matching records, returning null.");
+    return null;
+  }
+
+  private void storeRecordDeletion(Record record) {
+    try {
+      deleteGUID(record.guid);
+    } catch (RemoteException e) {
+      Logger.error(LOG_TAG, "RemoteException in password delete.");
+      delegate.onRecordStoreFailed(e);
+      return;
+    }
+    delegate.onRecordStoreSucceeded(record);
+  }
+
+  /**
+   * Make a PasswordRecord from a Cursor.
+   * @param cur
+   *        Cursor from query.
+   * @param deleted
+   *        true if creating a deleted Record, false if otherwise.
+   * @return
+   *        PasswordRecord populated from Cursor.
+   */
+  private static PasswordRecord passwordRecordFromCursor(Cursor cur) {
+    if (cur.isAfterLast()) {
+      return null;
+    }
+    String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.GUID);
+    long lastModified = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_PASSWORD_CHANGED);
+
+    PasswordRecord rec = new PasswordRecord(guid, COLLECTION, lastModified, false);
+    rec.id = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ID);
+    rec.hostname = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.HOSTNAME);
+    rec.httpRealm = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.HTTP_REALM);
+    rec.formSubmitURL = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.FORM_SUBMIT_URL);
+    rec.usernameField = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.USERNAME_FIELD);
+    rec.passwordField = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.PASSWORD_FIELD);
+    rec.encType = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ENC_TYPE);
+
+    // TODO decryption of username/password here (Bug 711636)
+    rec.encryptedUsername = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ENCRYPTED_USERNAME);
+    rec.encryptedPassword = RepoUtils.getStringFromCursor(cur, BrowserContract.Passwords.ENCRYPTED_PASSWORD);
+
+    rec.timeCreated = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_CREATED);
+    rec.timeLastUsed = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_LAST_USED);
+    rec.timePasswordChanged = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIME_PASSWORD_CHANGED);
+    rec.timesUsed = RepoUtils.getLongFromCursor(cur, BrowserContract.Passwords.TIMES_USED);
+    return rec;
+  }
+
+  private static PasswordRecord deletedPasswordRecordFromCursor(Cursor cur) {
+    if (cur.isAfterLast()) {
+      return null;
+    }
+    String guid = RepoUtils.getStringFromCursor(cur, DeletedColumns.GUID);
+    long lastModified = RepoUtils.getLongFromCursor(cur, DeletedColumns.TIME_DELETED);
+    PasswordRecord rec = new PasswordRecord(guid, COLLECTION, lastModified, true);
+    rec.androidID = RepoUtils.getLongFromCursor(cur, DeletedColumns.ID);
+    return rec;
+  }
+
+  private static ContentValues getContentValues(Record record) {
+    PasswordRecord rec = (PasswordRecord) record;
+
+    ContentValues cv = new ContentValues();
+    cv.put(BrowserContract.Passwords.GUID,            rec.guid);
+    cv.put(BrowserContract.Passwords.HOSTNAME,        rec.hostname);
+    // TODO: Bug 738347 - SQLiteBridge does not check for nulls in ContentValues.
+    // For now, don't set httpRealm, because it can be null and Fennec SQLite doesn't handle null CV.
+    // cv.put(BrowserContract.Passwords.HTTP_REALM,      rec.httpRealm);
+    cv.put(BrowserContract.Passwords.FORM_SUBMIT_URL, rec.formSubmitURL);
+    cv.put(BrowserContract.Passwords.USERNAME_FIELD,  rec.usernameField);
+    cv.put(BrowserContract.Passwords.PASSWORD_FIELD,  rec.passwordField);
+
+    // TODO Do encryption of username/password here. Bug 711636
+    // TODO: Bug 738347 - SQLiteBridge does not check for nulls in ContentValues.
+    // For now, don't set encType. (same as httpRealm)
+    // cv.put(BrowserContract.Passwords.ENC_TYPE,           rec.encType);
+    cv.put(BrowserContract.Passwords.ENCRYPTED_USERNAME, rec.encryptedUsername);
+    cv.put(BrowserContract.Passwords.ENCRYPTED_PASSWORD, rec.encryptedPassword);
+
+    cv.put(BrowserContract.Passwords.TIME_CREATED,          rec.timeCreated);
+    cv.put(BrowserContract.Passwords.TIME_LAST_USED,        rec.timeLastUsed);
+    cv.put(BrowserContract.Passwords.TIME_PASSWORD_CHANGED, rec.timePasswordChanged);
+    cv.put(BrowserContract.Passwords.TIMES_USED,            rec.timesUsed);
+    return cv;
+  }
+
+  private void dumpDbs() {
+    Cursor cursor = null;
+    try {
+      // Dump passwords.
+      Logger.debug(LOG_TAG, "passwordsProvider: ");
+      cursor = passwordsHelper.safeQuery(passwordsProvider, ".dumpDBs", getAllColumns(), dateModifiedWhere(0), null, null);
+      RepoUtils.dumpCursor(cursor);
+      cursor.close();
+
+      // Dump deleted passwords.
+      Logger.debug(LOG_TAG, "deletedPasswordsProvider: ");
+      cursor = deletedPasswordsHelper.safeQuery(passwordsProvider, ".dumpDBs", getAllDeletedColumns(), dateModifiedWhereDeleted(0), null, null);
+      RepoUtils.dumpCursor(cursor);
+
+    } catch (NullCursorException e) {
+      Logger.debug(LOG_TAG, "NullCursor in dumping DBs.");
+    } catch (RemoteException e) {
+      Logger.debug(LOG_TAG, "RemoteException in dumping DBs.");
+    } finally {
+      if (cursor != null) {
+        cursor.close();
+      }
+    }
+  }
+}
--- a/mobile/android/base/sync/repositories/android/RepoUtils.java
+++ b/mobile/android/base/sync/repositories/android/RepoUtils.java
@@ -7,17 +7,16 @@ package org.mozilla.gecko.sync.repositor
 import org.json.simple.JSONArray;
 import org.json.simple.parser.JSONParser;
 import org.json.simple.parser.ParseException;
 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.ClientRecord;
 import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
-import org.mozilla.gecko.sync.repositories.domain.PasswordRecord;
 
 import android.content.ContentProviderClient;
 import android.content.Context;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.net.Uri;
 import android.os.RemoteException;
 
@@ -166,40 +165,16 @@ public class RepoUtils {
       Logger.trace(LOG_TAG, "Returning client record " + rec.guid + " (" + rec.androidID + ")");
       Logger.trace(LOG_TAG, "Client Name:   " + rec.name);
       Logger.trace(LOG_TAG, "Client Type:   " + rec.type);
       Logger.trace(LOG_TAG, "Last Modified: " + rec.lastModified);
       Logger.trace(LOG_TAG, "Deleted:       " + rec.deleted);
     }
   }
 
-  public static PasswordRecord passwordFromMirrorCursor(Cursor cur) {
-    
-    String guid = getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
-    String collection = "passwords";
-    long lastModified = getLongFromCursor(cur, BrowserContract.SyncColumns.DATE_MODIFIED);
-    boolean deleted = getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1 ? true : false;
-    PasswordRecord rec = new PasswordRecord(guid, collection, lastModified, deleted);
-    rec.hostname = getStringFromCursor(cur, BrowserContract.Passwords.HOSTNAME);
-    rec.httpRealm = getStringFromCursor(cur, BrowserContract.Passwords.HTTP_REALM);
-    rec.formSubmitURL = getStringFromCursor(cur, BrowserContract.Passwords.FORM_SUBMIT_URL);
-    rec.usernameField = getStringFromCursor(cur, BrowserContract.Passwords.USERNAME_FIELD);
-    rec.passwordField = getStringFromCursor(cur, BrowserContract.Passwords.PASSWORD_FIELD);
-    rec.encType = getStringFromCursor(cur, BrowserContract.Passwords.ENC_TYPE);
-    
-    // TODO decryption of username/password here (Bug 711636)
-    rec.username = getStringFromCursor(cur, BrowserContract.Passwords.ENCRYPTED_USERNAME);
-    rec.password = getStringFromCursor(cur, BrowserContract.Passwords.ENCRYPTED_PASSWORD);
-    
-    rec.timeLastUsed = getLongFromCursor(cur, BrowserContract.Passwords.TIME_LAST_USED);
-    rec.timesUsed = getLongFromCursor(cur, BrowserContract.Passwords.TIMES_USED);
-    
-    return rec;
-  }
-  
   public static void queryTimeLogger(String methodCallingQuery, long queryStart, long queryEnd) {
     long elapsedTime = queryEnd - queryStart;
     Logger.debug(LOG_TAG, "Query timer: " + methodCallingQuery + " took " + elapsedTime + "ms.");
   }
 
   public static boolean stringsEqual(String a, String b) {
     // Check for nulls
     if (a == b) return true;
--- a/mobile/android/base/sync/repositories/domain/PasswordRecord.java
+++ b/mobile/android/base/sync/repositories/domain/PasswordRecord.java
@@ -1,23 +1,34 @@
 /* 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.domain;
 
 import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Logger;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.repositories.android.RepoUtils;
 
 public class PasswordRecord extends Record {
+  private static final String LOG_TAG = "PasswordRecord";
 
   public static final String COLLECTION_NAME = "passwords";
   public static long PASSWORDS_TTL = -1; // Never expire passwords.
 
+  // Payload strings.
+  public static final String PAYLOAD_HOSTNAME = "hostname";
+  public static final String PAYLOAD_FORM_SUBMIT_URL = "formSubmitURL";
+  public static final String PAYLOAD_HTTP_REALM = "httpRealm";
+  public static final String PAYLOAD_USERNAME = "username";
+  public static final String PAYLOAD_PASSWORD = "password";
+  public static final String PAYLOAD_USERNAME_FIELD = "usernameField";
+  public static final String PAYLOAD_PASSWORD_FIELD = "passwordField";
+
   public PasswordRecord(String guid, String collection, long lastModified, boolean deleted) {
     super(guid, collection, lastModified, deleted);
     this.ttl = PASSWORDS_TTL;
   }
   public PasswordRecord(String guid, String collection, long lastModified) {
     this(guid, collection, lastModified, false);
   }
   public PasswordRecord(String guid, String collection) {
@@ -25,95 +36,150 @@ public class PasswordRecord extends Reco
   }
   public PasswordRecord(String guid) {
     this(guid, COLLECTION_NAME, 0, false);
   }
   public PasswordRecord() {
     this(Utils.generateGuid(), COLLECTION_NAME, 0, false);
   }
 
+  public String id;
   public String hostname;
   public String formSubmitURL;
   public String httpRealm;
   // TODO these are encrypted in the passwords content provider,
   // need to figure out what we need to do here.
-  public String username;
-  public String password;
   public String usernameField;
   public String passwordField;
+  public String encryptedUsername;
+  public String encryptedPassword;
   public String encType;
+
+  public long   timeCreated;
   public long   timeLastUsed;
+  public long   timePasswordChanged;
   public long   timesUsed;
 
 
   @Override
   public Record copyWithIDs(String guid, long androidID) {
     PasswordRecord out = new PasswordRecord(guid, this.collection, this.lastModified, this.deleted);
     out.androidID = androidID;
     out.sortIndex = this.sortIndex;
     out.ttl       = this.ttl;
 
-    // Copy HistoryRecord fields.
+    // Copy PasswordRecord fields.
+    out.id            = this.id;
     out.hostname      = this.hostname;
     out.formSubmitURL = this.formSubmitURL;
     out.httpRealm     = this.httpRealm;
-    out.username      = this.username;
-    out.password      = this.password;
-    out.usernameField = this.usernameField;
-    out.passwordField = this.passwordField;
-    out.encType       = this.encType;
-    out.timeLastUsed  = this.timeLastUsed;
-    out.timesUsed     = this.timesUsed;
+
+    out.usernameField       = this.usernameField;
+    out.passwordField       = this.passwordField;
+    out.encryptedUsername   = this.encryptedUsername;
+    out.encryptedPassword   = this.encryptedPassword;
+    out.encType             = this.encType;
+
+    out.timeCreated         = this.timeCreated;
+    out.timeLastUsed        = this.timeLastUsed;
+    out.timePasswordChanged = this.timePasswordChanged;
+    out.timesUsed           = this.timesUsed;
 
     return out;
   }
 
   @Override
   public void initFromPayload(ExtendedJSONObject payload) {
-    // TODO: implement.
+    this.hostname = payload.getString(PAYLOAD_HOSTNAME);
+    this.formSubmitURL = payload.getString(PAYLOAD_FORM_SUBMIT_URL);
+    this.httpRealm = payload.getString(PAYLOAD_HTTP_REALM);
+    this.encryptedUsername = payload.getString(PAYLOAD_USERNAME);
+    this.encryptedPassword = payload.getString(PAYLOAD_PASSWORD);
+    this.usernameField = payload.getString(PAYLOAD_USERNAME_FIELD);
+    this.passwordField = payload.getString(PAYLOAD_PASSWORD_FIELD);
   }
 
   @Override
   public void populatePayload(ExtendedJSONObject payload) {
-    // TODO: implement.
+    putPayload(payload, PAYLOAD_HOSTNAME, this.hostname);
+    putPayload(payload, PAYLOAD_FORM_SUBMIT_URL, this.formSubmitURL);
+    putPayload(payload, PAYLOAD_HTTP_REALM, this.httpRealm);
+    putPayload(payload, PAYLOAD_USERNAME, this.encryptedUsername);
+    putPayload(payload, PAYLOAD_PASSWORD, this.encryptedPassword);
+    putPayload(payload, PAYLOAD_USERNAME_FIELD, this.usernameField);
+    putPayload(payload, PAYLOAD_PASSWORD_FIELD, this.passwordField);
   }
-  
+
   @Override
   public boolean congruentWith(Object o) {
     if (o == null || !(o instanceof PasswordRecord)) {
       return false;
     }
     PasswordRecord other = (PasswordRecord) o;
     if (!super.congruentWith(other)) {
       return false;
     }
     return RepoUtils.stringsEqual(this.hostname, other.hostname)
         && RepoUtils.stringsEqual(this.formSubmitURL, other.formSubmitURL)
-        && RepoUtils.stringsEqual(this.httpRealm, other.httpRealm)
-        && RepoUtils.stringsEqual(this.username, other.username)
-        && RepoUtils.stringsEqual(this.password, other.password)
+        // Bug 738347 - SQLiteBridge does not check for nulls in ContentValues.
+        // && RepoUtils.stringsEqual(this.httpRealm, other.httpRealm)
+        // && RepoUtils.stringsEqual(this.encType, other.encType)
         && RepoUtils.stringsEqual(this.usernameField, other.usernameField)
         && RepoUtils.stringsEqual(this.passwordField, other.passwordField)
-        && RepoUtils.stringsEqual(this.encType, other.encType);
+        && RepoUtils.stringsEqual(this.encryptedUsername, other.encryptedUsername)
+        && RepoUtils.stringsEqual(this.encryptedPassword, other.encryptedPassword);
   }
 
   @Override
   public boolean equalPayloads(Object o) {
     if (o == null || !(o instanceof PasswordRecord)) {
       return false;
     }
+
     PasswordRecord other = (PasswordRecord) o;
-    if (!super.equalPayloads(other)) {
+    Logger.debug("PasswordRecord", "thisRecord:" + this.toString());
+    Logger.debug("PasswordRecord", "otherRecord:" + o.toString());
+
+    if (this.deleted) {
+      if (other.deleted) {
+        // Deleted records are equal if their guids match.
+        return RepoUtils.stringsEqual(this.guid, other.guid);
+      }
+      // One record is deleted, the other is not. Not equal.
       return false;
     }
+
+    if (!super.equalPayloads(other)) {
+      Logger.debug(LOG_TAG, "super.equalPayloads returned false.");
+      return false;
+    }
+
     return RepoUtils.stringsEqual(this.hostname, other.hostname)
         && RepoUtils.stringsEqual(this.formSubmitURL, other.formSubmitURL)
-        && RepoUtils.stringsEqual(this.httpRealm, other.httpRealm)
-        && RepoUtils.stringsEqual(this.username, other.username)
-        && RepoUtils.stringsEqual(this.password, other.password)
+        // Bug 738347 - SQLiteBridge does not check for nulls in ContentValues.
+        // && RepoUtils.stringsEqual(this.httpRealm, other.httpRealm)
+        // && RepoUtils.stringsEqual(this.encType, other.encType)
         && RepoUtils.stringsEqual(this.usernameField, other.usernameField)
         && RepoUtils.stringsEqual(this.passwordField, other.passwordField)
-        && RepoUtils.stringsEqual(this.encType, other.encType)
-        && (this.timeLastUsed == other.timeLastUsed)
-        && (this.timesUsed == other.timesUsed);
+        && RepoUtils.stringsEqual(this.encryptedUsername, other.encryptedUsername)
+        && RepoUtils.stringsEqual(this.encryptedPassword, other.encryptedPassword);
+        // Desktop sync never sets timeCreated so this isn't relevant for sync records.
+  }
+
+  @Override
+  public String toString() {
+    return "PasswordRecord {"
+        + "lastModified: " + this.lastModified + ", "
+        + "hostname: " + this.hostname + ", "
+        + "formSubmitURL: " + this.formSubmitURL + ", "
+        + "httpRealm: " + this.httpRealm + ", "
+        + "usernameField: " + this.usernameField + ", "
+        + "passwordField: " + this.passwordField + ", "
+        + "encryptedUsername: " + this.encryptedUsername + ", "
+        + "encryptedPassword: " + this.encryptedPassword + ", "
+        + "encType: " + this.encType + ", "
+        + "timeCreated: " + this.timeCreated + ", "
+        + "timeLastUsed: " + this.timeLastUsed + ", "
+        + "timePasswordChanged: " + this.timePasswordChanged + ", "
+        + "timesUsed: " + this.timesUsed;
   }
 
 }
--- a/mobile/android/base/sync/stage/GlobalSyncStage.java
+++ b/mobile/android/base/sync/stage/GlobalSyncStage.java
@@ -20,15 +20,17 @@ public interface GlobalSyncStage {
     */
     syncClientsEngine,
     /*
     processFirstSyncPref,
     processClientCommands,
     updateEnabledEngines,
     */
     syncTabs,
+    syncPasswords,
     syncBookmarks,
     syncHistory,
     syncFormHistory,
     completed,
   }
+
   public void execute(GlobalSession session) throws NoSuchStageException;
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/sync/stage/PasswordsServerSyncStage.java
@@ -0,0 +1,45 @@
+/* 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.stage;
+
+import org.mozilla.gecko.sync.CryptoRecord;
+import org.mozilla.gecko.sync.repositories.RecordFactory;
+import org.mozilla.gecko.sync.repositories.Repository;
+import org.mozilla.gecko.sync.repositories.android.PasswordsRepositorySession;
+import org.mozilla.gecko.sync.repositories.domain.PasswordRecord;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+public class PasswordsServerSyncStage extends ServerSyncStage {
+
+  @Override
+  protected String getCollection() {
+    return "passwords";
+  }
+  @Override
+  protected String getEngineName() {
+    return "passwords";
+  }
+
+  @Override
+  protected Repository getLocalRepository() {
+    return new PasswordsRepositorySession.PasswordsRepository();
+  }
+
+  @Override
+  protected RecordFactory getRecordFactory() {
+    return new PasswordRecordFactory();
+  }
+
+  public class PasswordRecordFactory extends RecordFactory {
+
+    @Override
+    public Record createRecord(Record record) {
+      PasswordRecord r = new PasswordRecord();
+      r.initFromEnvelope((CryptoRecord) record);
+      return r;
+    }
+  }
+
+}
--- 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/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
+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/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/PasswordsRepositorySession.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/PasswordsServerSyncStage.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