Bug 720934 - Part 2: Collected Android Sync 0.4 code drop (Bug 722945, Bug 709348). a=mobile
authorRichard Newman <rnewman@mozilla.com>
Fri, 03 Feb 2012 13:09:29 -0800
changeset 86124 dab232f3a4f317f7819ffc60b1ac201e4beef1d1
parent 86123 5a918bbf0cd6fa740dc1672af1b983dac60aece5
child 86125 8d922468a7fe0ec0988f905cbc3cfbedb80ee915
push id21994
push userrnewman@mozilla.com
push dateFri, 03 Feb 2012 21:10:34 +0000
treeherdermozilla-central@2ce9638b93e2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmobile
bugs720934, 722945, 709348
milestone13.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 720934 - Part 2: Collected Android Sync 0.4 code drop (Bug 722945, Bug 709348). a=mobile Includes: Bug 722945 - Transactional behavior for store (don't return stored records in subsequent fetch). Bug 709348 - Refactor and improve reconciling and storing of records.
mobile/android/base/sync/Utils.java
mobile/android/base/sync/net/SyncStorageCollectionRequest.java
mobile/android/base/sync/net/SyncStorageRequestDelegate.java
mobile/android/base/sync/repositories/HashSetStoreTracker.java
mobile/android/base/sync/repositories/RecordFilter.java
mobile/android/base/sync/repositories/RepositorySession.java
mobile/android/base/sync/repositories/StoreTracker.java
mobile/android/base/sync/repositories/StoreTrackingRepositorySession.java
mobile/android/base/sync/repositories/android/AndroidBrowserBookmarksRepository.java
mobile/android/base/sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java
mobile/android/base/sync/repositories/android/AndroidBrowserHistoryDataAccessor.java
mobile/android/base/sync/repositories/android/AndroidBrowserHistoryRepositorySession.java
mobile/android/base/sync/repositories/android/AndroidBrowserPasswordsRepositorySession.java
mobile/android/base/sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java
mobile/android/base/sync/repositories/android/AndroidBrowserRepositorySession.java
mobile/android/base/sync/repositories/domain/BookmarkRecord.java
mobile/android/base/sync/synchronizer/ConcurrentRecordConsumer.java
mobile/android/sync/android-xml-resources.mn
mobile/android/sync/java-sources.mn
--- a/mobile/android/base/sync/Utils.java
+++ b/mobile/android/base/sync/Utils.java
@@ -71,16 +71,39 @@ public class Utils {
     if (LOG_TO_STDOUT) {
       for (String string : s) {
         System.out.print(string);
       }
       System.out.println("");
     }
   }
 
+  public static void error(String logTag, String message) {
+    logToStdout(logTag, " :: ERROR: ", message);
+    Log.i(logTag, message);
+  }
+
+  public static void info(String logTag, String message) {
+    logToStdout(logTag, " :: INFO: ", message);
+    Log.i(logTag, message);
+  }
+
+  public static void debug(String logTag, String message) {
+    logToStdout(logTag, " :: DEBUG: ", message);
+    Log.d(logTag, message);
+  }
+
+  public static void trace(String logTag, String message) {
+    if (!ENABLE_TRACE_LOGGING) {
+      return;
+    }
+    logToStdout(logTag, " :: TRACE: ", message);
+    Log.d(logTag, message);
+  }
+
   public static String generateGuid() {
     byte[] encodedBytes = Base64.encodeBase64(generateRandomBytes(9), false);
     return new String(encodedBytes).replace("+", "-").replace("/", "_");
   }
 
   private static byte[] generateRandomBytes(int length) {
     byte[] bytes = new byte[length];
     Random random = new Random(System.nanoTime());
--- a/mobile/android/base/sync/net/SyncStorageCollectionRequest.java
+++ b/mobile/android/base/sync/net/SyncStorageCollectionRequest.java
@@ -93,16 +93,22 @@ public class SyncStorageCollectionReques
       Header contentType = entity.getContentType();
       System.out.println("content type is " + contentType.getValue());
       if (!contentType.getValue().startsWith("application/newlines")) {
         // Not incremental!
         super.handleHttpResponse(response);
         return;
       }
 
+      // TODO: at this point we can access X-Weave-Timestamp, compare
+      // that to our local timestamp, and compute an estimate of clock
+      // skew. We can provide this to the incremental delegate, which
+      // will allow it to seamlessly correct timestamps on the records
+      // it processes. Bug 721887.
+
       // Line-by-line processing, then invoke success.
       SyncStorageCollectionRequestDelegate delegate = (SyncStorageCollectionRequestDelegate) this.request.delegate;
       InputStream content = null;
       BufferedReader br = null;
       try {
         content = entity.getContent();
         int bufSize = 1024 * 1024;         // 1MB. TODO: lift and consider.
         br = new BufferedReader(new InputStreamReader(content), bufSize);
--- a/mobile/android/base/sync/net/SyncStorageRequestDelegate.java
+++ b/mobile/android/base/sync/net/SyncStorageRequestDelegate.java
@@ -35,12 +35,16 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 package org.mozilla.gecko.sync.net;
 
 public interface SyncStorageRequestDelegate {
   String credentials();
   String ifUnmodifiedSince();
+
+  // TODO: at this point we can access X-Weave-Timestamp, compare
+  // that to our local timestamp, and compute an estimate of clock
+  // skew. Bug 721887.
   void handleRequestSuccess(SyncStorageResponse response);
   void handleRequestFailure(SyncStorageResponse response);
   void handleRequestError(Exception ex);
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/sync/repositories/HashSetStoreTracker.java
@@ -0,0 +1,55 @@
+/* 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;
+
+import java.util.HashSet;
+
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+public class HashSetStoreTracker implements StoreTracker {
+
+  // Guarded by `this`.
+  // Used to store GUIDs that were not locally modified but
+  // have been modified by a call to `store`, and thus
+  // should not be returned by a subsequent fetch.
+  private HashSet<String> guids;
+
+  public HashSetStoreTracker() {
+    guids = new HashSet<String>();
+  }
+
+  @Override
+  public String toString() {
+    return "#<Tracker: " + guids.size() + " guids tracked.>";
+  }
+
+  @Override
+  public synchronized boolean trackRecordForExclusion(String guid) {
+    return (guid != null) && guids.add(guid);
+  }
+
+  @Override
+  public synchronized boolean isTrackedForExclusion(String guid) {
+    return (guid != null) && guids.contains(guid);
+  }
+
+  @Override
+  public synchronized boolean untrackStoredForExclusion(String guid) {
+    return (guid != null) && guids.remove(guid);
+  }
+
+  @Override
+  public RecordFilter getFilter() {
+    if (guids.size() == 0) {
+      return null;
+    }
+    return new RecordFilter() {
+      @Override
+      public boolean excludeRecord(Record r) {
+        return isTrackedForExclusion(r.guid);
+      }
+    };
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/sync/repositories/RecordFilter.java
@@ -0,0 +1,11 @@
+/* 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;
+
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+public interface RecordFilter {
+  public boolean excludeRecord(Record r);
+}
--- a/mobile/android/base/sync/repositories/RepositorySession.java
+++ b/mobile/android/base/sync/repositories/RepositorySession.java
@@ -36,16 +36,17 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 package org.mozilla.gecko.sync.repositories;
 
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
+import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
 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.RepositorySessionStoreDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
 import org.mozilla.gecko.sync.repositories.domain.Record;
 
@@ -69,16 +70,25 @@ public abstract class RepositorySession 
   public enum SessionStatus {
     UNSTARTED,
     ACTIVE,
     ABORTED,
     DONE
   }
 
   private static final String LOG_TAG = "RepositorySession";
+
+  private static void error(String message) {
+    Utils.error(LOG_TAG, message);
+  }
+
+  protected static void trace(String message) {
+    Utils.trace(LOG_TAG, message);
+  }
+
   protected SessionStatus status = SessionStatus.UNSTARTED;
   protected Repository repository;
   protected RepositorySessionStoreDelegate delegate;
 
   /**
    * A queue of Runnables which call out into delegates.
    */
   protected ExecutorService delegateQueue  = Executors.newSingleThreadExecutor();
@@ -158,21 +168,16 @@ public abstract class RepositorySession 
       try {
         this.lastSyncTimestamp = bundle.getLong("timestamp");
       } catch (Exception e) {
         // Defaults to 0 above.
       }
     }
   }
 
-  private static void error(String msg) {
-    System.err.println("ERROR: " + msg);
-    Log.e(LOG_TAG, msg);
-  }
-
   /**
    * Synchronously perform the shared work of beginning. Throws on failure.
    * @throws InvalidSessionTransitionException
    *
    */
   protected void sharedBegin() throws InvalidSessionTransitionException {
     if (this.status == SessionStatus.UNSTARTED) {
       this.status = SessionStatus.ACTIVE;
@@ -246,9 +251,96 @@ public abstract class RepositorySession 
   }
 
   public void abort() {
     // TODO: do something here.
     status = SessionStatus.ABORTED;
     storeWorkQueue.shutdown();
     delegateQueue.shutdown();
   }
+
+  /**
+   * Produce a record that is some combination of the remote and local records
+   * provided.
+   *
+   * The returned record must be produced without mutating either remoteRecord
+   * or localRecord. It is acceptable to return either remoteRecord or localRecord
+   * if no modifications are to be propagated.
+   *
+   * The returned record *should* have the local androidID and the remote GUID,
+   * and some optional merge of data from the two records.
+   *
+   * This method can be called with records that are identical, or differ in
+   * any regard.
+   *
+   * This method will not be called if:
+   *
+   * * either record is marked as deleted, or
+   * * there is no local mapping for a new remote record.
+   *
+   * Otherwise, it will be called precisely once.
+   *
+   * Side-effects (e.g., for transactional storage) can be hooked in here.
+   *
+   * @param remoteRecord
+   *        The record retrieved from upstream, already adjusted for clock skew.
+   * @param localRecord
+   *        The record retrieved from local storage.
+   * @param lastRemoteRetrieval
+   *        The timestamp of the last retrieved set of remote records, adjusted for
+   *        clock skew.
+   * @param lastLocalRetrieval
+   *        The timestamp of the last retrieved set of local records.
+   * @return
+   *        A Record instance to apply, or null to apply nothing.
+   */
+  protected Record reconcileRecords(final Record remoteRecord,
+                                    final Record localRecord,
+                                    final long lastRemoteRetrieval,
+                                    final long lastLocalRetrieval) {
+    Log.d(LOG_TAG, "Reconciling remote " + remoteRecord.guid + " against local " + localRecord.guid);
+
+    if (localRecord.equalPayloads(remoteRecord)) {
+      if (remoteRecord.lastModified > localRecord.lastModified) {
+        Log.d(LOG_TAG, "Records are equal. No record application needed.");
+        return null;
+      }
+
+      // Local wins.
+      return null;
+    }
+
+    // TODO: Decide what to do based on:
+    // * Which of the two records is modified;
+    // * Whether they are equal or congruent;
+    // * The modified times of each record (interpreted through the lens of clock skew);
+    // * ...
+    boolean localIsMoreRecent = localRecord.lastModified > remoteRecord.lastModified;
+    Log.d(LOG_TAG, "Local record is more recent? " + localIsMoreRecent);
+    Record donor = localIsMoreRecent ? localRecord : remoteRecord;
+
+    // Modify the local record to match the remote record's GUID and values.
+    // Preserve the local Android ID, and merge data where possible.
+    // It sure would be nice if copyWithIDs didn't give a shit about androidID, mm?
+    Record out = donor.copyWithIDs(remoteRecord.guid, localRecord.androidID);
+
+    // We don't want to upload the record if the remote record was
+    // applied without changes.
+    // This logic will become more complicated as reconciling becomes smarter.
+    if (!localIsMoreRecent) {
+      trackRecord(out);
+    }
+    return out;
+  }
+
+  /**
+   * Depending on the RepositorySession implementation, track
+   * that a record — most likely a brand-new record that has been
+   * applied unmodified — should be tracked so as to not be uploaded
+   * redundantly.
+   *
+   * The default implementation does nothing.
+   *
+   * @param record
+   */
+  protected synchronized void trackRecord(Record record) {
+  }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/sync/repositories/StoreTracker.java
@@ -0,0 +1,78 @@
+/* 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;
+
+/**
+ * Our hacky version of transactional semantics. The goal is to prevent
+ * the following situation:
+ *
+ * * AAA is not modified locally.
+ * * A modified AAA is downloaded during the storing phase. Its local
+ *   timestamp is advanced.
+ * * The direction of syncing changes, and AAA is now uploaded to the server.
+ *
+ * The following situation should still be supported:
+ *
+ * * AAA is not modified locally.
+ * * A modified AAA is downloaded and merged with the local AAA.
+ * * The merged AAA is uploaded to the server.
+ *
+ * As should:
+ *
+ * * AAA is modified locally.
+ * * A modified AAA is downloaded, and discarded or merged.
+ * * The current version of AAA is uploaded to the server.
+ *
+ * We achieve this by tracking GUIDs during the storing phase. If we
+ * apply a record such that the local copy is substantially the same
+ * as the record we just downloaded, we add it to a list of records
+ * to avoid uploading. The definition of "substantially the same"
+ * depends on the particular repository. The only consideration is "do we
+ * want to upload this record in this sync?".
+ *
+ * Note that items are removed from this list when a fetch that
+ * considers them for upload completes successfully. The entire list
+ * is discarded when the session is completed.
+ *
+ * This interface exposes methods to:
+ *
+ * * During a store, recording that a record has been stored, and should
+ *   thus not be returned in subsequent fetches;
+ * * During a fetch, checking whether a record should be returned.
+ *
+ * In the future this might also grow self-persistence.
+ *
+ * See also RepositorySession.trackRecord.
+ *
+ * @author rnewman
+ *
+ */
+public interface StoreTracker {
+
+  /**
+   * @param guid
+   *        The GUID of the item to track.
+   * @return
+   *        Whether the GUID was a newly tracked value.
+   */
+  public boolean trackRecordForExclusion(String guid);
+
+  /**
+   * @param guid
+   *        The GUID of the item to check.
+   * @return
+   *        true if the item is already tracked.
+   */
+  public boolean isTrackedForExclusion(String guid);
+
+  /**
+  *
+  * @param guid
+  * @return true if the specified GUID was removed from the tracked set.
+  */
+  public boolean untrackStoredForExclusion(String guid);
+
+  public RecordFilter getFilter();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/sync/repositories/StoreTrackingRepositorySession.java
@@ -0,0 +1,62 @@
+/* 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;
+
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
+import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFinishDelegate;
+import org.mozilla.gecko.sync.repositories.domain.Record;
+
+import android.util.Log;
+
+public abstract class StoreTrackingRepositorySession extends RepositorySession {
+  private static final String LOG_TAG = "StoreTrackingRepositorySession";
+  protected StoreTracker storeTracker;
+
+  protected static StoreTracker createStoreTracker() {
+    return new HashSetStoreTracker();
+  }
+
+  public StoreTrackingRepositorySession(Repository repository) {
+    super(repository);
+  }
+
+  @Override
+  public void begin(RepositorySessionBeginDelegate delegate) {
+    RepositorySessionBeginDelegate deferredDelegate = delegate.deferredBeginDelegate(delegateQueue);
+    try {
+      super.sharedBegin();
+    } catch (InvalidSessionTransitionException e) {
+      deferredDelegate.onBeginFailed(e);
+      return;
+    }
+    // Or do this in your own subclass.
+    storeTracker = createStoreTracker();
+    deferredDelegate.onBeginSucceeded(this);
+  }
+
+  @Override
+  protected synchronized void trackRecord(Record record) {
+    if (this.storeTracker == null) {
+      throw new IllegalStateException("Store tracker not yet initialized!");
+    }
+
+    Log.d(LOG_TAG, "Tracking record " + record.guid +
+                   " (" + record.lastModified + ") to avoid re-upload.");
+    // Future: we care about the timestamp…
+    this.storeTracker.trackRecordForExclusion(record.guid);
+  }
+
+  @Override
+  public void abort(RepositorySessionFinishDelegate delegate) {
+    this.storeTracker = null;
+    super.abort(delegate);
+  }
+
+  @Override
+  public void finish(RepositorySessionFinishDelegate delegate) {
+    this.storeTracker = null;
+    super.finish(delegate);
+  }
+}
\ No newline at end of file
--- a/mobile/android/base/sync/repositories/android/AndroidBrowserBookmarksRepository.java
+++ b/mobile/android/base/sync/repositories/android/AndroidBrowserBookmarksRepository.java
@@ -40,16 +40,17 @@ package org.mozilla.gecko.sync.repositor
 
 import org.mozilla.gecko.sync.repositories.BookmarksRepository;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
 
 import android.content.Context;
 
 public class AndroidBrowserBookmarksRepository extends AndroidBrowserRepository implements BookmarksRepository {
 
+  @Override
   protected void sessionCreator(RepositorySessionCreationDelegate delegate, Context context) {
     AndroidBrowserBookmarksRepositorySession session = new AndroidBrowserBookmarksRepositorySession(AndroidBrowserBookmarksRepository.this, context);
     delegate.onSessionCreated(session);
   }
 
   @Override
   protected AndroidBrowserRepositoryDataAccessor getDataAccessor(Context context) {
     return new AndroidBrowserBookmarksDataAccessor(context);
--- a/mobile/android/base/sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java
+++ b/mobile/android/base/sync/repositories/android/AndroidBrowserBookmarksRepositorySession.java
@@ -62,22 +62,16 @@ public class AndroidBrowserBookmarksRepo
   private HashMap<String, Long> guidToID = new HashMap<String, Long>();
   private HashMap<Long, String> idToGuid = new HashMap<Long, String>();
 
   private HashMap<String, ArrayList<String>> missingParentToChildren = new HashMap<String, ArrayList<String>>();
   private HashMap<String, JSONArray> parentToChildArray = new HashMap<String, JSONArray>();
   private AndroidBrowserBookmarksDataAccessor dataAccessor;
   private int needsReparenting = 0;
 
-  private static void trace(String string) {
-    if (Utils.ENABLE_TRACE_LOGGING) {
-      Log.d(LOG_TAG, string);
-    }
-  }
-
   /**
    * Return true if the provided record GUID should be skipped
    * in child lists or fetch results.
    *
    * @param recordGUID
    * @return
    */
   public static boolean forbiddenGUID(String recordGUID) {
@@ -308,20 +302,19 @@ public class AndroidBrowserBookmarksRepo
             " bookmark(s) have been placed in unsorted bookmarks and not been reparented.");
 
       // TODO: handling of failed reparenting.
       // E.g., delegate.onFinishFailed(new BookmarkNeedsReparentingException(null));
     }
     super.finish(delegate);
   };
 
-  // TODO this code is yucky, cleanup or comment or something
+  @Override
   @SuppressWarnings("unchecked")
-  @Override
-  protected long insert(Record record) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
+  protected Record prepareRecord(Record record) {
     BookmarkRecord bmk = (BookmarkRecord) record;
     
     // Check if parent exists
     if (guidToID.containsKey(bmk.parentID)) {
       bmk.androidParentID = guidToID.get(bmk.parentID);
       JSONArray children = parentToChildArray.get(bmk.parentID);
       if (children != null) {
         if (!children.contains(bmk.guid)) {
@@ -350,45 +343,49 @@ public class AndroidBrowserBookmarksRepo
                      " (" + bmk.parentID + ", " + bmk.parentName +
                      ", " + bmk.pos + ")");
     } else {
       Log.d(LOG_TAG, "Inserting bookmark " + bmk.guid + ", " + bmk.title + ", " +
                      bmk.bookmarkURI + " with parent " + bmk.androidParentID +
                      " (" + bmk.parentID + ", " + bmk.parentName +
                      ", " + bmk.pos + ")");
     }
-    long id = RepoUtils.getAndroidIdFromUri(dbHelper.insert(bmk));
-    Log.d(LOG_TAG, "Inserted as " + id);
+    return bmk;
+  }
 
-    putRecordToGuidMap(buildRecordString(bmk), bmk.guid);
-    bmk.androidID = id;
+  @Override
+  @SuppressWarnings("unchecked")
+  protected void updateBookkeeping(Record record) throws NoGuidForIdException,
+                                                         NullCursorException,
+                                                         ParentNotFoundException {
+    super.updateBookkeeping(record);
+    BookmarkRecord bmk = (BookmarkRecord) record;
 
     // If record is folder, update maps and re-parent children if necessary
     if (bmk.type.equalsIgnoreCase(AndroidBrowserBookmarksDataAccessor.TYPE_FOLDER)) {
-      guidToID.put(bmk.guid, id);
-      idToGuid.put(id, bmk.guid);
+      guidToID.put(bmk.guid, bmk.androidID);
+      idToGuid.put(bmk.androidID, bmk.guid);
 
       JSONArray childArray = bmk.children;
 
       // Re-parent.
       if (missingParentToChildren.containsKey(bmk.guid)) {
         for (String child : missingParentToChildren.get(bmk.guid)) {
           long position;
           if (!bmk.children.contains(child)) {
             childArray.add(child);
           }
           position = childArray.indexOf(child);
-          dataAccessor.updateParentAndPosition(child, id, position);
+          dataAccessor.updateParentAndPosition(child, bmk.androidID, position);
           needsReparenting--;
         }
         missingParentToChildren.remove(bmk.guid);
       }
       parentToChildArray.put(bmk.guid, childArray);
     }
-    return id;
   }
 
   @Override
   protected String buildRecordString(Record record) {
     BookmarkRecord bmk = (BookmarkRecord) record;
     return bmk.title + bmk.bookmarkURI + bmk.type + bmk.parentName;
   }
 }
--- a/mobile/android/base/sync/repositories/android/AndroidBrowserHistoryDataAccessor.java
+++ b/mobile/android/base/sync/repositories/android/AndroidBrowserHistoryDataAccessor.java
@@ -97,18 +97,28 @@ public class AndroidBrowserHistoryDataAc
   
   @Override
   public Uri insert(Record record) {
     HistoryRecord rec = (HistoryRecord) record;
     Log.d(LOG_TAG, "Storing visits for " + record.guid);
     dataExtender.store(record.guid, rec.visits);
     Log.d(LOG_TAG, "Storing record " + record.guid);
     return super.insert(record);
-  }  
-  
+  }
+
+  @Override
+  public void update(String oldGUID, Record newRecord) {
+    HistoryRecord rec = (HistoryRecord) newRecord;
+    String newGUID = newRecord.guid;
+    Log.d(LOG_TAG, "Storing visits for " + newGUID + ", replacing " + oldGUID);
+    dataExtender.delete(oldGUID);
+    dataExtender.store(newGUID, rec.visits);
+    super.update(oldGUID, newRecord);
+  }
+
   @Override
   protected void delete(String guid) {
     Log.d(LOG_TAG, "Deleting record " + guid);
     super.delete(guid);
     dataExtender.delete(guid);
   }
 
 }
\ No newline at end of file
--- a/mobile/android/base/sync/repositories/android/AndroidBrowserHistoryRepositorySession.java
+++ b/mobile/android/base/sync/repositories/android/AndroidBrowserHistoryRepositorySession.java
@@ -130,9 +130,14 @@ public class AndroidBrowserHistoryReposi
       // ... and the 1 actual record we have.
       // We still have to fake the visit type: Fennec doesn't track that.
       addVisit(visitsArray, hist.fennecDateVisited * 1000);
     }
 
     hist.visits = visitsArray;
     return hist;
   }
+
+  @Override
+  protected Record prepareRecord(Record record) {
+    return record;
+  }
 }
--- a/mobile/android/base/sync/repositories/android/AndroidBrowserPasswordsRepositorySession.java
+++ b/mobile/android/base/sync/repositories/android/AndroidBrowserPasswordsRepositorySession.java
@@ -58,9 +58,13 @@ public class AndroidBrowserPasswordsRepo
   }
 
   @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/AndroidBrowserRepositoryDataAccessor.java
+++ b/mobile/android/base/sync/repositories/android/AndroidBrowserRepositoryDataAccessor.java
@@ -96,18 +96,29 @@ public abstract class AndroidBrowserRepo
 
     int deleted = context.getContentResolver().delete(getUri(), where, args);
     if (deleted == 1) {
       return;
     }
     Log.w(LOG_TAG, "Unexpectedly deleted " + deleted + " rows for guid " + guid);
   }
 
+  public void update(String guid, Record newRecord) {
+    String where  = BrowserContract.SyncColumns.GUID + " = ?";
+    String[] args = new String[] { guid };
+    ContentValues cv = getContentValues(newRecord);
+    int updated = context.getContentResolver().update(getUri(), cv, where, args);
+    if (updated != 1) {
+      Log.w(LOG_TAG, "Unexpectedly updated " + updated + " rows for guid " + guid);
+    }
+  }
+
   public Uri insert(Record record) {
     ContentValues cv = getContentValues(record);
+    Log.d(LOG_TAG, "INSERTING: " + cv.getAsString("guid"));
     return context.getContentResolver().insert(getUri(), cv);
   }
 
   /**
    * Fetch all records.
    * The caller is responsible for closing the cursor.
    *
    * @return A cursor. You *must* close this when you're done with it.
--- a/mobile/android/base/sync/repositories/android/AndroidBrowserRepositorySession.java
+++ b/mobile/android/base/sync/repositories/android/AndroidBrowserRepositorySession.java
@@ -46,25 +46,27 @@ import org.mozilla.gecko.sync.repositori
 import org.mozilla.gecko.sync.repositories.InvalidRequestException;
 import org.mozilla.gecko.sync.repositories.InvalidSessionTransitionException;
 import org.mozilla.gecko.sync.repositories.MultipleRecordsForGuidException;
 import org.mozilla.gecko.sync.repositories.NoGuidForIdException;
 import org.mozilla.gecko.sync.repositories.NoStoreDelegateException;
 import org.mozilla.gecko.sync.repositories.NullCursorException;
 import org.mozilla.gecko.sync.repositories.ParentNotFoundException;
 import org.mozilla.gecko.sync.repositories.ProfileDatabaseException;
+import org.mozilla.gecko.sync.repositories.RecordFilter;
 import org.mozilla.gecko.sync.repositories.Repository;
-import org.mozilla.gecko.sync.repositories.RepositorySession;
+import org.mozilla.gecko.sync.repositories.StoreTrackingRepositorySession;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionBeginDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionFetchRecordsDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionGuidsSinceDelegate;
 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionWipeDelegate;
 import org.mozilla.gecko.sync.repositories.domain.Record;
 
 import android.database.Cursor;
+import android.net.Uri;
 import android.util.Log;
 
 /**
  * You'll notice that all delegate calls *either*:
  *
  * - request a deferred delegate with the appropriate work queue, then
  *   make the appropriate call, or
  * - create a Runnable which makes the appropriate call, and pushes it
@@ -79,20 +81,20 @@ import android.util.Log;
  * don't do neither unless you know what you're doing!
  *
  * Similarly, all store calls go through the appropriate store queue. This
  * ensures that store() and storeDone() consequences occur before-after.
  *
  * @author rnewman
  *
  */
-public abstract class AndroidBrowserRepositorySession extends RepositorySession {
+public abstract class AndroidBrowserRepositorySession extends StoreTrackingRepositorySession {
 
   protected AndroidBrowserRepositoryDataAccessor dbHelper;
-  protected static final String LOG_TAG = "AndroidBrowserRepositorySession";
+  public static final String LOG_TAG = "AndroidBrowserRepositorySession";
   private HashMap<String, String> recordToGuid;
 
   public AndroidBrowserRepositorySession(Repository repository) {
     super(repository);
   }
 
   /**
    * Override this.
@@ -145,25 +147,27 @@ public abstract class AndroidBrowserRepo
       return;
     } catch (NullCursorException e) {
       deferredDelegate.onBeginFailed(e);
       return;
     } catch (Exception e) {
       deferredDelegate.onBeginFailed(e);
       return;
     }
+    storeTracker = createStoreTracker();
     deferredDelegate.onBeginSucceeded(this);
   }
 
   protected abstract String buildRecordString(Record record);
 
   protected void checkDatabase() throws ProfileDatabaseException, NullCursorException {
-    Log.i(LOG_TAG, "Checking database.");
+    Utils.info(LOG_TAG, "BEGIN: checking database.");
     try {
       dbHelper.fetch(new String[] { "none" }).close();
+      Utils.info(LOG_TAG, "END: checking database.");
     } catch (NullPointerException e) {
       throw new ProfileDatabaseException(e);
     }
   }
 
   @Override
   public void guidsSince(long timestamp, RepositorySessionGuidsSinceDelegate delegate) {
     GuidsSinceRunnable command = new GuidsSinceRunnable(timestamp, delegate);
@@ -219,40 +223,44 @@ public abstract class AndroidBrowserRepo
       guids.toArray(guidsArray);
       delegate.onGuidsSinceSucceeded(guidsArray);
     }
   }
 
   @Override
   public void fetch(String[] guids,
                     RepositorySessionFetchRecordsDelegate delegate) {
-    FetchRunnable command = new FetchRunnable(guids, now(), delegate);
+    FetchRunnable command = new FetchRunnable(guids, now(), null, delegate);
     delegateQueue.execute(command);
   }
 
   abstract class FetchingRunnable implements Runnable {
     protected RepositorySessionFetchRecordsDelegate delegate;
 
     public FetchingRunnable(RepositorySessionFetchRecordsDelegate delegate) {
       this.delegate = delegate;
     }
 
-    protected void fetchFromCursor(Cursor cursor, long end) {
+    protected void fetchFromCursor(Cursor cursor, RecordFilter filter, long end) {
       Log.d(LOG_TAG, "Fetch from cursor:");
       try {
         try {
           if (!cursor.moveToFirst()) {
             delegate.onFetchCompleted(end);
             return;
           }
           while (!cursor.isAfterLast()) {
             Log.d(LOG_TAG, "... one more record.");
-            Record r = transformRecord(recordFromMirrorCursor(cursor));
+            Record r = recordFromMirrorCursor(cursor);
             if (r != null) {
-              delegate.onFetchedRecord(r);
+              if (filter == null || !filter.excludeRecord(r)) {
+                delegate.onFetchedRecord(transformRecord(r));
+              } else {
+                Log.d(LOG_TAG, "Filter says to skip record.");
+              }
             }
             cursor.moveToNext();
           }
           delegate.onFetchCompleted(end);
         } catch (NoGuidForIdException e) {
           Log.w(LOG_TAG, "No GUID for ID.", e);
           delegate.onFetchFailed(e, null);
         } catch (Exception e) {
@@ -265,23 +273,26 @@ public abstract class AndroidBrowserRepo
         cursor.close();
       }
     }
   }
 
   class FetchRunnable extends FetchingRunnable {
     private String[] guids;
     private long     end;
+    private RecordFilter filter;
 
     public FetchRunnable(String[] guids,
                          long end,
+                         RecordFilter filter,
                          RepositorySessionFetchRecordsDelegate delegate) {
       super(delegate);
-      this.guids = guids;
-      this.end   = end;
+      this.guids  = guids;
+      this.end    = end;
+      this.filter = filter;
     }
 
     @Override
     public void run() {
       if (!isActive()) {
         delegate.onFetchFailed(new InactiveSessionException(null), null);
         return;
       }
@@ -289,74 +300,72 @@ public abstract class AndroidBrowserRepo
       if (guids == null || guids.length < 1) {
         Log.e(LOG_TAG, "No guids sent to fetch");
         delegate.onFetchFailed(new InvalidRequestException(null), null);
         return;
       }
 
       try {
         Cursor cursor = dbHelper.fetch(guids);
-        this.fetchFromCursor(cursor, end);
+        this.fetchFromCursor(cursor, filter, end);
       } catch (NullCursorException e) {
         delegate.onFetchFailed(e, null);
       }
     }
   }
 
   @Override
   public void fetchSince(long timestamp,
                          RepositorySessionFetchRecordsDelegate delegate) {
+    if (this.storeTracker == null) {
+      throw new IllegalStateException("Store tracker not yet initialized!");
+    }
+
     Log.i(LOG_TAG, "Running fetchSince(" + timestamp + ").");
-    FetchSinceRunnable command = new FetchSinceRunnable(timestamp, now(), delegate);
+    FetchSinceRunnable command = new FetchSinceRunnable(timestamp, now(), this.storeTracker.getFilter(), delegate);
     delegateQueue.execute(command);
   }
 
   class FetchSinceRunnable extends FetchingRunnable {
     private long since;
     private long end;
+    private RecordFilter filter;
 
     public FetchSinceRunnable(long since,
                               long end,
+                              RecordFilter filter,
                               RepositorySessionFetchRecordsDelegate delegate) {
       super(delegate);
-      this.since = since;
-      this.end   = end;
+      this.since  = since;
+      this.end    = end;
+      this.filter = filter;
     }
 
     @Override
     public void run() {
       if (!isActive()) {
         delegate.onFetchFailed(new InactiveSessionException(null), null);
         return;
       }
 
       try {
         Cursor cursor = dbHelper.fetchSince(since);
-        this.fetchFromCursor(cursor, end);
+        this.fetchFromCursor(cursor, filter, end);
       } catch (NullCursorException e) {
         delegate.onFetchFailed(e, null);
         return;
       }
     }
   }
 
   @Override
   public void fetchAll(RepositorySessionFetchRecordsDelegate delegate) {
     this.fetchSince(0, delegate);
   }
 
-  private void trace(String m) {
-    if (Utils.ENABLE_TRACE_LOGGING) {
-      if (Utils.LOG_TO_STDOUT) {
-        System.out.println(LOG_TAG + "::TRACE " + m);
-      }
-      Log.d(LOG_TAG, m);
-    }
-  }
-
   @Override
   public void store(final Record record) throws NoStoreDelegateException {
     if (delegate == null) {
       throw new NoStoreDelegateException();
     }
     if (record == null) {
       Log.e(LOG_TAG, "Record sent to store was null");
       throw new IllegalArgumentException("Null record passed to AndroidBrowserRepositorySession.store().");
@@ -381,44 +390,111 @@ public abstract class AndroidBrowserRepo
         if (!checkRecordType(record)) {
           Log.d(LOG_TAG, "Ignoring record " + record.guid + " due to unknown record type.");
 
           // Don't throw: we don't want to abort the entire sync when we get a livemark!
           // delegate.onRecordStoreFailed(new InvalidBookmarkTypeException(null));
           return;
         }
 
-        // TODO:
-        // TODO: rnewman 2012-01-13: read and improve this code.
-        // TODO:
+
+        // TODO: lift these into the session.
+        // Temporary: this matches prior syncing semantics, in which only
+        // the relationship between the local and remote record is considered.
+        // In the future we'll track these two timestamps and use them to
+        // determine which records have changed, and thus process incoming
+        // records more efficiently.
+        long lastLocalRetrieval  = 0;      // lastSyncTimestamp?
+        long lastRemoteRetrieval = 0;      // TODO: adjust for clock skew.
+        boolean remotelyModified = record.lastModified > lastRemoteRetrieval;
+
         Record existingRecord;
         try {
-          existingRecord = findExistingRecord(record);
+          // GUID matching only: deleted records don't have a payload with which to search.
+          existingRecord = recordForGUID(record.guid);
+          if (record.deleted) {
+            if (existingRecord == null) {
+              // We're done. Don't bother with a callback. That can change later
+              // if we want it to.
+              trace("Incoming record " + record.guid + " is deleted, and no local version. Bye!");
+              return;
+            }
+
+            if (existingRecord.deleted) {
+              trace("Local record already deleted. Bye!");
+              return;
+            }
 
-          // If the record is new and not deleted, store it
-          if (existingRecord == null && !record.deleted) {
-            record.androidID = insert(record);
-          } else if (existingRecord != null) {
+            // Which one wins?
+            if (!remotelyModified) {
+              trace("Ignoring deleted record from the past.");
+              return;
+            }
 
-            // If the incoming record is marked deleted and
-            // our existing record has a newer timestamp, then
-            // discard the incoming record.
-            if (record.deleted && existingRecord.lastModified > record.lastModified) {
-              delegate.onRecordStoreSucceeded(record);
+            boolean locallyModified = existingRecord.lastModified > lastLocalRetrieval;
+            if (!locallyModified) {
+              trace("Remote modified, local not. Deleting.");
+              storeRecordDeletion(record);
+              return;
+            }
+
+            trace("Both local and remote records have been modified.");
+            if (record.lastModified > existingRecord.lastModified) {
+              trace("Remote is newer, and deleted. Deleting local.");
+              storeRecordDeletion(record);
               return;
             }
-            // Now's a great time to do expensive additions.
-            existingRecord = transformRecord(existingRecord);
-            dbHelper.delete(existingRecord);
-            if (!record.deleted) {
-              // Record exists already, need to figure out what to store.
-              Record store = reconcileRecords(existingRecord, record);
-              record.androidID = insert(store);
+
+            trace("Remote is older, local is not deleted. Ignoring.");
+            if (!locallyModified) {
+              Log.w(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 " + record.guid);
+            existingRecord = findExistingRecord(record);
+          }
+
+          if (existingRecord == null) {
+            // The record is new.
+            trace("No match. Inserting.");
+            Record inserted = insert(record);
+            trackRecord(inserted);
+            delegate.onRecordStoreSucceeded(inserted);
+            return;
+          }
+
+          // We found a local dupe.
+          trace("Incoming record " + record.guid + " dupes to local record " + existingRecord.guid);
+
+          // Populate more expensive fields prior to reconciling.
+          existingRecord = transformRecord(existingRecord);
+          Record toStore = reconcileRecords(record, existingRecord, lastRemoteRetrieval, lastLocalRetrieval);
+
+          if (toStore == null) {
+            Log.d(LOG_TAG, "Reconciling returned null. Not inserting a record.");
+            return;
+          }
+
+          // TODO: pass in timestamps?
+          Log.d(LOG_TAG, "Replacing " + existingRecord.guid + " with record " + toStore.guid);
+          Record replaced = replace(toStore, existingRecord);
+
+          // Note that we don't track records here; deciding that is the job
+          // of reconcileRecords.
+          Log.d(LOG_TAG, "Calling delegate callback with guid " + replaced.guid +
+                         "(" + replaced.androidID + ")");
+          delegate.onRecordStoreSucceeded(replaced);
+          return;
+
         } catch (MultipleRecordsForGuidException e) {
           Log.e(LOG_TAG, "Multiple records returned for given guid: " + record.guid);
           delegate.onRecordStoreFailed(e);
           return;
         } catch (NoGuidForIdException e) {
           Log.e(LOG_TAG, "Store failed for " + record.guid, e);
           delegate.onRecordStoreFailed(e);
           return;
@@ -426,27 +502,48 @@ public abstract class AndroidBrowserRepo
           Log.e(LOG_TAG, "Store failed for " + record.guid, e);
           delegate.onRecordStoreFailed(e);
           return;
         } catch (Exception e) {
           Log.e(LOG_TAG, "Store failed for " + record.guid, e);
           delegate.onRecordStoreFailed(e);
           return;
         }
-
-        // Invoke callback with result.
-        delegate.onRecordStoreSucceeded(record);
       }
     };
     storeWorkQueue.execute(command);
   }
-  
-  protected long insert(Record record) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
-    putRecordToGuidMap(buildRecordString(record), record.guid);
-    return RepoUtils.getAndroidIdFromUri(dbHelper.insert(record));
+
+  protected void storeRecordDeletion(final Record record) {
+    // TODO: we ought to mark the record as deleted rather than deleting it,
+    // in order to support syncing to multiple destinations. Bug 722607.
+    dbHelper.delete(record);      // TODO: mm?
+    delegate.onRecordStoreSucceeded(record);
+  }
+
+  protected Record insert(Record record) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
+    Record toStore = prepareRecord(record);
+    Uri recordURI = dbHelper.insert(toStore);
+    long id = RepoUtils.getAndroidIdFromUri(recordURI);
+    Log.d(LOG_TAG, "Inserted as " + id);
+
+    toStore.androidID = id;
+    updateBookkeeping(toStore);
+    Log.d(LOG_TAG, "insert() returning record " + toStore.guid);
+    return toStore;
+  }
+
+  protected Record replace(Record newRecord, Record existingRecord) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
+    Record toStore = prepareRecord(newRecord);
+
+    // newRecord should already have suitable androidID and guid.
+    dbHelper.update(existingRecord.guid, toStore);
+    updateBookkeeping(toStore);
+    Log.d(LOG_TAG, "replace() returning record " + toStore.guid);
+    return toStore;
   }
 
   protected Record recordForGUID(String guid) throws
                                              NoGuidForIdException,
                                              NullCursorException,
                                              ParentNotFoundException,
                                              MultipleRecordsForGuidException {
     Cursor cursor = dbHelper.fetch(new String[] { guid });
@@ -465,31 +562,33 @@ public abstract class AndroidBrowserRepo
 
       // More than one. Oh dear.
       throw (new MultipleRecordsForGuidException(null));
     } finally {
       cursor.close();
     }
   }
 
-  // Check if record already exists locally.
+  /**
+   * Attempt to find an equivalent record through some means other than GUID.
+   *
+   * @param record
+   *        The record for which to search.
+   * @return
+   *        An equivalent Record object, or null if none is found.
+   *
+   * @throws MultipleRecordsForGuidException
+   * @throws NoGuidForIdException
+   * @throws NullCursorException
+   * @throws ParentNotFoundException
+   */
   protected Record findExistingRecord(Record record) throws MultipleRecordsForGuidException,
     NoGuidForIdException, NullCursorException, ParentNotFoundException {
 
-    Log.d(LOG_TAG, "Finding existing record for GUID " + record.guid);
-    Record r = recordForGUID(record.guid);
-
-    // One result. (Multiple throws an exception.)
-    if (r != null) {
-      Log.d(LOG_TAG, "Found one by GUID.");
-      return r;
-    }
-
-    // Empty result.
-    // Check to see if record exists but with a different guid.
+    Log.d(LOG_TAG, "Finding existing record for incoming record with GUID " + record.guid);
     String recordString = buildRecordString(record);
     Log.d(LOG_TAG, "Searching with record string " + recordString);
     String guid = getRecordToGuidMap().get(recordString);
     if (guid != null) {
       Log.d(LOG_TAG, "Found one. Returning computed record.");
       return recordForGUID(guid);
     }
     Log.d(LOG_TAG, "findExistingRecord failed to find one for " + record.guid);
@@ -499,59 +598,48 @@ public abstract class AndroidBrowserRepo
   public HashMap<String, String> getRecordToGuidMap() throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
     if (recordToGuid == null) {
       createRecordToGuidMap();
     }
     return recordToGuid;
   }
 
   private void createRecordToGuidMap() throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
+    Utils.info(LOG_TAG, "BEGIN: creating record -> GUID map.");
     recordToGuid = new HashMap<String, String>();
     Cursor cur = dbHelper.fetchAll();
     try {
       if (!cur.moveToFirst()) {
         return;
       }
       while (!cur.isAfterLast()) {
         Record record = recordFromMirrorCursor(cur);
         if (record != null) {
           recordToGuid.put(buildRecordString(record), record.guid);
         }
         cur.moveToNext();
       }
     } finally {
       cur.close();
     }
+    Utils.info(LOG_TAG, "END: creating record -> GUID map.");
   }
 
-  public void putRecordToGuidMap(String guid, String recordString) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
+  public void putRecordToGuidMap(String recordString, String guid) throws NoGuidForIdException, NullCursorException, ParentNotFoundException {
     if (recordToGuid == null) {
       createRecordToGuidMap();
     }
-    recordToGuid.put(guid, recordString);
+    recordToGuid.put(recordString, guid);
   }
 
-  protected Record reconcileRecords(Record local, Record remote) {
-    Log.i(LOG_TAG, "Reconciling " + local.guid + " against " + remote.guid);
-
-    // Determine which record is newer since this is the one we will take in case of conflict.
-    // Yes, clock drift. *sigh*
-    Record newer;
-    if (local.lastModified > remote.lastModified) {
-      newer = local;
-    } else {
-      newer = remote;
-    }
-
-    if (newer.guid != remote.guid) {
-      newer.guid = remote.guid;
-    }
-    newer.androidID = local.androidID;
-
-    return newer;
+  protected abstract Record prepareRecord(Record record);
+  protected void updateBookkeeping(Record record) throws NoGuidForIdException,
+                                                 NullCursorException,
+                                                 ParentNotFoundException {
+    putRecordToGuidMap(buildRecordString(record), record.guid);
   }
 
   // Wipe method and thread.
   @Override
   public void wipe(RepositorySessionWipeDelegate delegate) {
     Runnable command = new WipeRunnable(delegate);
     storeWorkQueue.execute(command);
   }
--- a/mobile/android/base/sync/repositories/domain/BookmarkRecord.java
+++ b/mobile/android/base/sync/repositories/domain/BookmarkRecord.java
@@ -219,19 +219,17 @@ public class BookmarkRecord extends Reco
     }
     if (isFolder()) {
       rec.payload.put("children", this.children);
     }
     return rec;
   }
 
   private void trace(String s) {
-    if (Utils.ENABLE_TRACE_LOGGING) {
-      Log.d(LOG_TAG, s);
-    }
+    Utils.trace(LOG_TAG, s);
   }
 
   @Override
   public boolean equalPayloads(Object o) {
     trace("Calling BookmarkRecord.equalPayloads.");
     if (o == null || !(o instanceof BookmarkRecord)) {
       return false;
     }
--- a/mobile/android/base/sync/synchronizer/ConcurrentRecordConsumer.java
+++ b/mobile/android/base/sync/synchronizer/ConcurrentRecordConsumer.java
@@ -60,31 +60,25 @@ class ConcurrentRecordConsumer extends R
   protected boolean allRecordsQueued = false;
   private long counter = 0;
 
   public ConcurrentRecordConsumer(RecordsConsumerDelegate delegate) {
     this.delegate = delegate;
   }
 
   private static void info(String message) {
-    Utils.logToStdout(LOG_TAG, "::INFO: ", message);
-    Log.i(LOG_TAG, message);
+    Utils.info(LOG_TAG, message);
   }
 
   private static void debug(String message) {
-    Utils.logToStdout(LOG_TAG, ":: DEBUG: ", message);
-    Log.d(LOG_TAG, message);
+    Utils.debug(LOG_TAG, message);
   }
 
   private static void trace(String message) {
-    if (!Utils.ENABLE_TRACE_LOGGING) {
-      return;
-    }
-    Utils.logToStdout(LOG_TAG, ":: TRACE: ", message);
-    Log.d(LOG_TAG, message);
+    Utils.trace(LOG_TAG, message);
   }
 
   private Object monitor = new Object();
   @Override
   public void doNotify() {
     synchronized (monitor) {
       monitor.notify();
     }
@@ -134,17 +128,23 @@ class ConcurrentRecordConsumer extends R
           return;
         }
         debug("run() dropped monitor.");
       }
       // The queue is concurrent-safe.
       while (!delegate.getQueue().isEmpty()) {
         trace("Grabbing record...");
         Record record = delegate.getQueue().remove();
-        delegate.store(record);
+        trace("Storing record... " + delegate);
+        try {
+          delegate.store(record);
+        } catch (Exception e) {
+          // TODO: Bug 709371: track records that failed to apply.
+          Log.e(LOG_TAG, "Caught error in store.", e);
+        }
         trace("Done with record.");
       }
       synchronized (monitor) {
         trace("run() took monitor.");
 
         if (allRecordsQueued) {
           debug("Done with records and no more to come. Notifying consumerIsDone.");
           consumerIsDone();
deleted file mode 100644
--- a/mobile/android/sync/android-xml-resources.mn
+++ /dev/null
@@ -1,2 +0,0 @@
-mobile/android/base/resources/xml/sync_authenticator.xml
-mobile/android/base/resources/xml/sync_syncadapter.xml
--- 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/CredentialsSource.java sync/crypto/CryptoException.java sync/crypto/Cryptographer.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/cryptographer/CryptoStatusBundle.java sync/cryptographer/SyncCryptographer.java sync/CryptoRecord.java sync/DelayedWorkTracker.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/Gx4IsOneException.java sync/jpake/IncorrectZkpException.java sync/jpake/JPakeClient.java sync/jpake/JPakeCrypto.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/JPakeUtils.java sync/jpake/Zkp.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/net/BaseResource.java sync/net/CompletedEntity.java sync/net/HandleProgressException.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/NoCollectionKeysSetException.java sync/NonArrayJSONException.java sync/NonObjectJSONException.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/BrowserContract.java sync/repositories/android/PasswordColumns.java sync/repositories/android/RepoUtils.java sync/repositories/BookmarkNeedsReparentingException.java sync/repositories/BookmarksRepository.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/HistoryRecord.java sync/repositories/domain/HistoryRecordFactory.java sync/repositories/domain/PasswordRecord.java sync/repositories/domain/Record.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/NoGuidForIdException.java sync/repositories/NoStoreDelegateException.java sync/repositories/NullCursorException.java sync/repositories/ParentNotFoundException.java sync/repositories/ProfileDatabaseException.java sync/repositories/RecordFactory.java sync/repositories/Repository.java sync/repositories/RepositorySession.java sync/repositories/RepositorySessionBundle.java sync/repositories/Server11Repository.java sync/repositories/Server11RepositorySession.java sync/setup/activities/AccountActivity.java sync/setup/activities/SetupFailureActivity.java sync/setup/activities/SetupSuccessActivity.java sync/setup/activities/SetupSyncActivity.java sync/setup/Constants.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/FetchInfoCollectionsStage.java sync/stage/FetchMetaGlobalStage.java sync/stage/GlobalSyncStage.java sync/stage/NoSuchStageException.java sync/stage/NoSyncIDException.java sync/stage/ServerSyncStage.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/CredentialsSource.java sync/crypto/CryptoException.java sync/crypto/Cryptographer.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/cryptographer/CryptoStatusBundle.java sync/cryptographer/SyncCryptographer.java sync/CryptoRecord.java sync/DelayedWorkTracker.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/Gx4IsOneException.java sync/jpake/IncorrectZkpException.java sync/jpake/JPakeClient.java sync/jpake/JPakeCrypto.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/JPakeUtils.java sync/jpake/Zkp.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/net/BaseResource.java sync/net/CompletedEntity.java sync/net/HandleProgressException.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/NoCollectionKeysSetException.java sync/NonArrayJSONException.java sync/NonObjectJSONException.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/BrowserContract.java sync/repositories/android/PasswordColumns.java sync/repositories/android/RepoUtils.java sync/repositories/BookmarkNeedsReparentingException.java sync/repositories/BookmarksRepository.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/HistoryRecord.java sync/repositories/domain/HistoryRecordFactory.java sync/repositories/domain/PasswordRecord.java sync/repositories/domain/Record.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/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/SetupFailureActivity.java sync/setup/activities/SetupSuccessActivity.java sync/setup/activities/SetupSyncActivity.java sync/setup/Constants.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/FetchInfoCollectionsStage.java sync/stage/FetchMetaGlobalStage.java sync/stage/GlobalSyncStage.java sync/stage/NoSuchStageException.java sync/stage/NoSyncIDException.java sync/stage/ServerSyncStage.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