Bug 709385 - Passwords repository layer. r=rnewman
authorChenxia Liu <liuche@mozilla.com>
Thu, 12 Apr 2012 00:36:58 -0700
changeset 91500 3e5b3359a4690f2fbe5c29c3a9b2b27932af2574
parent 91499 527a2b7e7f4e530d2e3939cc51dc0cc9c1326c9a
child 91501 7324a63f7855c085662098937df7b617d3209037
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersrnewman
bugs709385
milestone14.0a1
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