Bug 986114 - Part 1: ReadingListProvider and BrowserProvider should share DB accessors. r=nalexander, a=lsblakk
authorRichard Newman <rnewman@mozilla.com>
Fri, 21 Mar 2014 16:00:38 -0700
changeset 192666 e38906f990ee26bd774c533e452b6848245a2b0b
parent 192665 1c429fbf41e55215649726b96c3599db096c3733
child 192667 7a2168f9b7a0f973027b0b0197b6945785edd4cb
push id474
push userasasaki@mozilla.com
push dateMon, 02 Jun 2014 21:01:02 +0000
treeherdermozilla-release@967f4cf1b31c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnalexander, lsblakk
bugs986114
milestone30.0a2
Bug 986114 - Part 1: ReadingListProvider and BrowserProvider should share DB accessors. r=nalexander, a=lsblakk
mobile/android/base/db/AbstractPerProfileDatabaseProvider.java
mobile/android/base/db/AbstractTransactionalProvider.java
mobile/android/base/db/BrowserProvider.java
mobile/android/base/db/PerProfileDatabaseProvider.java
mobile/android/base/db/ReadingListProvider.java
mobile/android/base/db/SharedBrowserDatabaseProvider.java
mobile/android/base/db/TransactionalProvider.java
mobile/android/base/moz.build
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/AbstractPerProfileDatabaseProvider.java
@@ -0,0 +1,79 @@
+/* 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.db;
+
+import org.mozilla.gecko.mozglue.RobocopTarget;
+
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+
+/**
+ * The base class for ContentProviders that wish to use a different DB
+ * for each profile.
+ *
+ * This class has logic shared between ordinary per-profile CPs and
+ * those that wish to share DB connections between CPs.
+ */
+public abstract class AbstractPerProfileDatabaseProvider extends AbstractTransactionalProvider {
+
+    /**
+     * Extend this to provide access to your own map of shared databases. This
+     * is a method so that your subclass doesn't collide with others!
+     */
+    protected abstract PerProfileDatabases<? extends SQLiteOpenHelper> getDatabases();
+
+    /*
+     * Fetches a readable database based on the profile indicated in the
+     * passed URI. If the URI does not contain a profile param, the default profile
+     * is used.
+     *
+     * @param uri content URI optionally indicating the profile of the user
+     * @return    instance of a readable SQLiteDatabase
+     */
+    @Override
+    protected SQLiteDatabase getReadableDatabase(Uri uri) {
+        String profile = null;
+        if (uri != null) {
+            profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
+        }
+
+        return getDatabases().getDatabaseHelperForProfile(profile, isTest(uri)).getReadableDatabase();
+    }
+
+    /*
+     * Fetches a writable database based on the profile indicated in the
+     * passed URI. If the URI does not contain a profile param, the default profile
+     * is used
+     *
+     * @param uri content URI optionally indicating the profile of the user
+     * @return    instance of a writable SQLiteDatabase
+     */
+    @Override
+    protected SQLiteDatabase getWritableDatabase(Uri uri) {
+        String profile = null;
+        if (uri != null) {
+            profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
+        }
+
+        return getDatabases().getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
+    }
+
+    protected SQLiteDatabase getWritableDatabaseForProfile(String profile, boolean isTest) {
+        return getDatabases().getDatabaseHelperForProfile(profile, isTest).getWritableDatabase();
+    }
+
+    /**
+     * This method should ONLY be used for testing purposes.
+     *
+     * @param uri content URI optionally indicating the profile of the user
+     * @return    instance of a writable SQLiteDatabase
+     */
+    @Override
+    @RobocopTarget
+    public SQLiteDatabase getWritableDatabaseForTesting(Uri uri) {
+        return getWritableDatabase(uri);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/AbstractTransactionalProvider.java
@@ -0,0 +1,370 @@
+/* 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.db;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * This abstract class exists to capture some of the transaction-handling
+ * commonalities in Fennec's DB layer.
+ *
+ * In particular, this abstracts DB access, batching, and a particular
+ * transaction approach.
+ *
+ * That approach is: subclasses implement the abstract methods
+ * {@link #insertInTransaction(android.net.Uri, android.content.ContentValues)},
+ * {@link #deleteInTransaction(android.net.Uri, String, String[])}, and
+ * {@link #updateInTransaction(android.net.Uri, android.content.ContentValues, String, String[])}.
+ *
+ * These are all called expecting a transaction to be established, so failed
+ * modifications can be rolled-back, and work batched.
+ *
+ * If no transaction is established, that's not a problem. Transaction nesting
+ * can be avoided by using {@link #beginWrite(SQLiteDatabase)}.
+ *
+ * The decision of when to begin a transaction is left to the subclasses,
+ * primarily to avoid the pattern of a transaction being begun, a read occurring,
+ * and then a write being necessary. This lock upgrade can result in SQLITE_BUSY,
+ * which we don't handle well. Better to avoid starting a transaction too soon!
+ *
+ * You are probably interested in some subclasses:
+ *
+ * * {@link AbstractPerProfileDatabaseProvider} provides a simple abstraction for
+ *   querying databases that are stored in the user's profile directory.
+ * * {@link PerProfileDatabaseProvider} is a simple version that only allows a
+ *   single ContentProvider to access each per-profile database.
+ * * {@link SharedBrowserDatabaseProvider} is an example of a per-profile provider
+ *   that allows for multiple providers to safely work with the same databases.
+ */
+@SuppressWarnings("javadoc")
+public abstract class AbstractTransactionalProvider extends ContentProvider {
+    private static final String LOGTAG = "GeckoTransProvider";
+
+    private static boolean logDebug = Log.isLoggable(LOGTAG, Log.DEBUG);
+    private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
+
+    protected abstract SQLiteDatabase getReadableDatabase(Uri uri);
+    protected abstract SQLiteDatabase getWritableDatabase(Uri uri);
+
+    public abstract SQLiteDatabase getWritableDatabaseForTesting(Uri uri);
+
+    protected abstract Uri insertInTransaction(Uri uri, ContentValues values);
+    protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
+    protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs);
+
+    /**
+     * Track whether we're in a batch operation.
+     *
+     * When we're in a batch operation, individual write steps won't even try
+     * to start a transaction... and neither will they attempt to finish one.
+     *
+     * Set this to <code>Boolean.TRUE</code> when you're entering a batch --
+     * a section of code in which {@link ContentProvider} methods will be
+     * called, but nested transactions should not be started. Callers are
+     * responsible for beginning and ending the enclosing transaction, and
+     * for setting this to <code>Boolean.FALSE</code> when done.
+     *
+     * This is a ThreadLocal separate from `db.inTransaction` because batched
+     * operations start transactions independent of individual ContentProvider
+     * operations. This doesn't work well with the entire concept of this
+     * abstract class -- that is, automatically beginning and ending transactions
+     * for each insert/delete/update operation -- and doing so without
+     * causing arbitrary nesting requires external tracking.
+     *
+     * Note that beginWrite takes a DB argument, but we don't differentiate
+     * between databases in this tracking flag. If your ContentProvider manages
+     * multiple database transactions within the same thread, you'll need to
+     * amend this scheme -- but then, you're already doing some serious wizardry,
+     * so rock on.
+     */
+    final ThreadLocal<Boolean> isInBatchOperation = new ThreadLocal<Boolean>();
+
+    /**
+     * Return true if OS version and database parallelism support indicates
+     * that this provider should bundle writes into transactions.
+     */
+    @SuppressWarnings("static-method")
+    protected boolean shouldUseTransactions() {
+        return Build.VERSION.SDK_INT >= 11;
+    }
+
+    protected static String computeSQLInClause(int items, String field) {
+        final StringBuilder builder = new StringBuilder(field);
+        builder.append(" IN (");
+        int i = 0;
+        for (; i < items - 1; ++i) {
+            builder.append("?, ");
+        }
+        if (i < items) {
+            builder.append("?");
+        }
+        builder.append(")");
+        return builder.toString();
+    }
+
+    private boolean isInBatch() {
+        final Boolean isInBatch = isInBatchOperation.get();
+        if (isInBatch == null) {
+            return false;
+        }
+        return isInBatch.booleanValue();
+    }
+
+    /**
+     * If we're not currently in a transaction, and we should be, start one.
+     */
+    protected void beginWrite(final SQLiteDatabase db) {
+        if (isInBatch()) {
+            trace("Not bothering with an intermediate write transaction: inside batch operation.");
+            return;
+        }
+
+        if (shouldUseTransactions() && !db.inTransaction()) {
+            trace("beginWrite: beginning transaction.");
+            db.beginTransaction();
+        }
+    }
+
+    /**
+     * If we're not in a batch, but we are in a write transaction, mark it as
+     * successful.
+     */
+    protected void markWriteSuccessful(final SQLiteDatabase db) {
+        if (isInBatch()) {
+            trace("Not marking write successful: inside batch operation.");
+            return;
+        }
+
+        if (shouldUseTransactions() && db.inTransaction()) {
+            trace("Marking write transaction successful.");
+            db.setTransactionSuccessful();
+        }
+    }
+
+    /**
+     * If we're not in a batch, but we are in a write transaction,
+     * end it.
+     *
+     * @see PerProfileDatabaseProvider#markWriteSuccessful(SQLiteDatabase)
+     */
+    protected void endWrite(final SQLiteDatabase db) {
+        if (isInBatch()) {
+            trace("Not ending write: inside batch operation.");
+            return;
+        }
+
+        if (shouldUseTransactions() && db.inTransaction()) {
+            trace("endWrite: ending transaction.");
+            db.endTransaction();
+        }
+    }
+
+    protected void beginBatch(final SQLiteDatabase db) {
+        trace("Beginning batch.");
+        isInBatchOperation.set(Boolean.TRUE);
+        db.beginTransaction();
+    }
+
+    protected void markBatchSuccessful(final SQLiteDatabase db) {
+        if (isInBatch()) {
+            trace("Marking batch successful.");
+            db.setTransactionSuccessful();
+            return;
+        }
+        Log.w(LOGTAG, "Unexpectedly asked to mark batch successful, but not in batch!");
+        throw new IllegalStateException("Not in batch.");
+    }
+
+    protected void endBatch(final SQLiteDatabase db) {
+        trace("Ending batch.");
+        db.endTransaction();
+        isInBatchOperation.set(Boolean.FALSE);
+    }
+
+    /**
+     * Turn a single-column cursor of longs into a single SQL "IN" clause.
+     * We can do this without using selection arguments because Long isn't
+     * vulnerable to injection.
+     */
+    protected static String computeSQLInClauseFromLongs(final Cursor cursor, String field) {
+        final StringBuilder builder = new StringBuilder(field);
+        builder.append(" IN (");
+        final int commaLimit = cursor.getCount() - 1;
+        int i = 0;
+        while (cursor.moveToNext()) {
+            builder.append(cursor.getLong(0));
+            if (i++ < commaLimit) {
+                builder.append(", ");
+            }
+        }
+        builder.append(")");
+        return builder.toString();
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        trace("Calling delete on URI: " + uri + ", " + selection + ", " + selectionArgs);
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        int deleted = 0;
+
+        try {
+            deleted = deleteInTransaction(uri, selection, selectionArgs);
+            markWriteSuccessful(db);
+        } finally {
+            endWrite(db);
+        }
+
+        if (deleted > 0) {
+            final boolean shouldSyncToNetwork = !isCallerSync(uri);
+            getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
+        }
+
+        return deleted;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        trace("Calling insert on URI: " + uri);
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        Uri result = null;
+        try {
+            result = insertInTransaction(uri, values);
+            markWriteSuccessful(db);
+        } catch (SQLException sqle) {
+            Log.e(LOGTAG, "exception in DB operation", sqle);
+        } catch (UnsupportedOperationException uoe) {
+            Log.e(LOGTAG, "don't know how to perform that insert", uoe);
+        } finally {
+            endWrite(db);
+        }
+
+        if (result != null) {
+            final boolean shouldSyncToNetwork = !isCallerSync(uri);
+            getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
+        }
+
+        return result;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        trace("Calling update on URI: " + uri + ", " + selection + ", " + selectionArgs);
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+        int updated = 0;
+
+        try {
+            updated = updateInTransaction(uri, values, selection,
+                                          selectionArgs);
+            markWriteSuccessful(db);
+        } finally {
+            endWrite(db);
+        }
+
+        if (updated > 0) {
+            final boolean shouldSyncToNetwork = !isCallerSync(uri);
+            getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
+        }
+
+        return updated;
+    }
+
+    @Override
+    public int bulkInsert(Uri uri, ContentValues[] values) {
+        if (values == null) {
+            return 0;
+        }
+
+        int numValues = values.length;
+        int successes = 0;
+
+        final SQLiteDatabase db = getWritableDatabase(uri);
+
+        debug("bulkInsert: explicitly starting transaction.");
+        beginBatch(db);
+
+        try {
+            for (int i = 0; i < numValues; i++) {
+                insertInTransaction(uri, values[i]);
+                successes++;
+            }
+            trace("Flushing DB bulkinsert...");
+            markBatchSuccessful(db);
+        } finally {
+            debug("bulkInsert: explicitly ending transaction.");
+            endBatch(db);
+        }
+
+        if (successes > 0) {
+            final boolean shouldSyncToNetwork = !isCallerSync(uri);
+            getContext().getContentResolver().notifyChange(uri, null, shouldSyncToNetwork);
+        }
+
+        return successes;
+    }
+
+    /**
+     * Indicates whether a query should include deleted fields
+     * based on the URI.
+     * @param uri query URI
+     */
+    protected static boolean shouldShowDeleted(Uri uri) {
+        String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
+        return !TextUtils.isEmpty(showDeleted);
+    }
+
+    /**
+     * Indicates whether an insertion should be made if a record doesn't
+     * exist, based on the URI.
+     * @param uri query URI
+     */
+    protected static boolean shouldUpdateOrInsert(Uri uri) {
+        String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED);
+        return Boolean.parseBoolean(insertIfNeeded);
+    }
+
+    /**
+     * Indicates whether query is a test based on the URI.
+     * @param uri query URI
+     */
+    protected static boolean isTest(Uri uri) {
+        if (uri == null) {
+            return false;
+        }
+        String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
+        return !TextUtils.isEmpty(isTest);
+    }
+
+    /**
+     * Return true of the query is from Firefox Sync.
+     * @param uri query URI
+     */
+    protected static boolean isCallerSync(Uri uri) {
+        String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
+        return !TextUtils.isEmpty(isSync);
+    }
+
+    protected static void trace(String message) {
+        if (logVerbose) {
+            Log.v(LOGTAG, message);
+        }
+    }
+
+    protected static void debug(String message) {
+        if (logDebug) {
+            Log.d(LOGTAG, message);
+        }
+    }
+}
\ No newline at end of file
--- a/mobile/android/base/db/BrowserProvider.java
+++ b/mobile/android/base/db/BrowserProvider.java
@@ -14,38 +14,36 @@ import org.mozilla.gecko.db.BrowserContr
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserContract.CommonColumns;
 import org.mozilla.gecko.db.BrowserContract.FaviconColumns;
 import org.mozilla.gecko.db.BrowserContract.Favicons;
 import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserContract.Schema;
 import org.mozilla.gecko.db.BrowserContract.SyncColumns;
 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
-import org.mozilla.gecko.db.BrowserContract.URLColumns;
 import org.mozilla.gecko.sync.Utils;
 
 import android.app.SearchManager;
 import android.content.ContentProviderOperation;
 import android.content.ContentProviderResult;
 import android.content.ContentUris;
 import android.content.ContentValues;
-import android.content.Context;
 import android.content.OperationApplicationException;
 import android.content.UriMatcher;
 import android.database.Cursor;
 import android.database.DatabaseUtils;
 import android.database.MatrixCursor;
 import android.database.SQLException;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
 import android.text.TextUtils;
 import android.util.Log;
 
-public class BrowserProvider extends TransactionalProvider<BrowserDatabaseHelper> {
+public class BrowserProvider extends SharedBrowserDatabaseProvider {
     private static final String LOGTAG = "GeckoBrowserProvider";
 
     // How many records to reposition in a single query.
     // This should be less than the SQLite maximum number of query variables
     // (currently 999) divided by the number of variables used per positioning
     // query (currently 3).
     static final int MAX_POSITION_UPDATES_PER_QUERY = 100;
 
@@ -810,31 +808,16 @@ public class BrowserProvider extends Tra
         Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy,
                 null, sortOrder, limit);
         cursor.setNotificationUri(getContext().getContentResolver(),
                 BrowserContract.AUTHORITY_URI);
 
         return cursor;
     }
 
-    private static int getUrlCount(SQLiteDatabase db, String table, String url) {
-        final Cursor c = db.query(table, new String[] { "COUNT(*)" },
-                                  URLColumns.URL + " = ?", new String[] { url },
-                                  null, null, null);
-        try {
-            if (c.moveToFirst()) {
-                return c.getInt(0);
-            }
-        } finally {
-            c.close();
-        }
-
-        return 0;
-    }
-
     /**
      * Update the positions of bookmarks in batches.
      *
      * Begins and ends its own transactions.
      *
      * @see #updateBookmarkPositionsInTransaction(SQLiteDatabase, String[], int, int)
      */
     int updateBookmarkPositions(Uri uri, String[] guids) {
@@ -1300,17 +1283,17 @@ public class BrowserProvider extends Tra
         // Doing this UPDATE (or the DELETE above) first ensures that the
         // first operation within a new enclosing transaction is a write.
         // The cleanup call below will do a SELECT first, and thus would
         // require the transaction to be upgraded from a reader to a writer.
         // In some cases that upgrade can fail (SQLITE_BUSY), so we avoid
         // it if we can.
         final int updated = db.update(TABLE_HISTORY, values, selection, selectionArgs);
         try {
-            cleanupSomeDeletedRecords(uri, History.CONTENT_URI, TABLE_HISTORY);
+            cleanUpSomeDeletedRecords(uri, TABLE_HISTORY);
         } catch (Exception e) {
             // We don't care.
             Log.e(LOGTAG, "Unable to clean up deleted history records: ", e);
         }
         return updated;
     }
 
     int deleteBookmarks(Uri uri, String selection, String[] selectionArgs) {
@@ -1329,17 +1312,17 @@ public class BrowserProvider extends Tra
         values.put(Bookmarks.IS_DELETED, 1);
 
         // Doing this UPDATE (or the DELETE above) first ensures that the
         // first operation within this transaction is a write.
         // The cleanup call below will do a SELECT first, and thus would
         // require the transaction to be upgraded from a reader to a writer.
         final int updated = updateBookmarks(uri, values, selection, selectionArgs);
         try {
-            cleanupSomeDeletedRecords(uri, Bookmarks.CONTENT_URI, TABLE_BOOKMARKS);
+            cleanUpSomeDeletedRecords(uri, TABLE_BOOKMARKS);
         } catch (Exception e) {
             // We don't care.
             Log.e(LOGTAG, "Unable to clean up deleted bookmark records: ", e);
         }
         return updated;
     }
 
     int deleteFavicons(Uri uri, String selection, String[] selectionArgs) {
@@ -1456,20 +1439,9 @@ public class BrowserProvider extends Tra
         endBatch(db);
 
         if (failures) {
             throw new OperationApplicationException();
         }
 
         return results;
     }
-
-    @Override
-    protected BrowserDatabaseHelper createDatabaseHelper(
-            Context context, String databasePath) {
-         return new BrowserDatabaseHelper(context, databasePath);
-    }
-
-    @Override
-    protected String getDatabaseName() {
-        return BrowserDatabaseHelper.DATABASE_NAME;
-    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/PerProfileDatabaseProvider.java
@@ -0,0 +1,50 @@
+/* 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.db;
+
+import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteOpenHelper;
+
+/**
+ * Abstract class containing methods needed to make a SQLite-based content
+ * provider with a database helper of type T, where one database helper is
+ * held per profile.
+ */
+public abstract class PerProfileDatabaseProvider<T extends SQLiteOpenHelper> extends AbstractPerProfileDatabaseProvider {
+    private PerProfileDatabases<T> databases;
+
+    @Override
+    protected PerProfileDatabases<T> getDatabases() {
+        return databases;
+    }
+
+    protected abstract String getDatabaseName();
+
+    /**
+     * Creates and returns an instance of the appropriate DB helper.
+     *
+     * @param  context       to use to create the database helper
+     * @param  databasePath  path to the DB file
+     * @return               instance of the database helper
+     */
+    protected abstract T createDatabaseHelper(Context context, String databasePath);
+
+    @Override
+    public boolean onCreate() {
+        synchronized (this) {
+            databases = new PerProfileDatabases<T>(
+                getContext(), getDatabaseName(), new DatabaseHelperFactory<T>() {
+                    @Override
+                    public T makeDatabaseHelper(Context context, String databasePath) {
+                        return createDatabaseHelper(context, databasePath);
+                    }
+                });
+        }
+
+        return true;
+    }
+}
--- a/mobile/android/base/db/ReadingListProvider.java
+++ b/mobile/android/base/db/ReadingListProvider.java
@@ -4,27 +4,24 @@
 
 package org.mozilla.gecko.db;
 
 import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
 import org.mozilla.gecko.sync.Utils;
 
 import android.content.ContentUris;
 import android.content.ContentValues;
-import android.content.Context;
 import android.content.UriMatcher;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.net.Uri;
 import android.text.TextUtils;
 
-public class ReadingListProvider extends TransactionalProvider<BrowserDatabaseHelper> {
-    private static final String LOGTAG = "GeckoReadingListProv";
-
+public class ReadingListProvider extends SharedBrowserDatabaseProvider {
     static final String TABLE_READING_LIST = ReadingListItems.TABLE_NAME;
 
     static final int ITEMS = 101;
     static final int ITEMS_ID = 102;
     static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
 
     static {
         URI_MATCHER.addURI(BrowserContract.READING_LIST_AUTHORITY, "items", ITEMS);
@@ -98,17 +95,17 @@ public class ReadingListProvider extends
         if (isCallerSync(uri)) {
             return db.delete(TABLE_READING_LIST, selection, selectionArgs);
         }
 
         debug("Marking item entry as deleted for URI: " + uri);
         ContentValues values = new ContentValues();
         values.put(ReadingListItems.IS_DELETED, 1);
 
-        cleanupSomeDeletedRecords(uri, ReadingListItems.CONTENT_URI, TABLE_READING_LIST);
+        cleanUpSomeDeletedRecords(uri, TABLE_READING_LIST);
         return updateItems(uri, values, selection, selectionArgs);
     }
 
     @Override
     @SuppressWarnings("fallthrough")
     public int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
         trace("Calling update in transaction on URI: " + uri);
 
@@ -242,20 +239,9 @@ public class ReadingListProvider extends
             case ITEMS_ID:
                 trace("URI is ITEMS_ID: " + uri);
                 return ReadingListItems.CONTENT_ITEM_TYPE;
         }
 
         debug("URI has unrecognized type: " + uri);
         return null;
     }
-
-    @Override
-    protected BrowserDatabaseHelper createDatabaseHelper(Context context,
-            String databasePath) {
-        return new BrowserDatabaseHelper(context, databasePath);
-    }
-
-    @Override
-    protected String getDatabaseName() {
-        return BrowserDatabaseHelper.DATABASE_NAME;
-    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/SharedBrowserDatabaseProvider.java
@@ -0,0 +1,115 @@
+/* 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.db;
+
+import org.mozilla.gecko.db.BrowserContract.CommonColumns;
+import org.mozilla.gecko.db.BrowserContract.SyncColumns;
+import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+import android.util.Log;
+
+/**
+ * A ContentProvider subclass that provides per-profile browser.db access
+ * that can be safely shared between multiple providers.
+ *
+ * If multiple ContentProvider classes wish to share a database, it's
+ * vitally important that they use the same SQLiteOpenHelpers for access.
+ *
+ * Failure to do so can cause accidental concurrent writes, with the result
+ * being unexpected SQLITE_BUSY errors.
+ *
+ * This class provides a static {@link PerProfileDatabases} instance, lazily
+ * initialized within {@link SharedBrowserDatabaseProvider#onCreate()}.
+ */
+public abstract class SharedBrowserDatabaseProvider extends AbstractPerProfileDatabaseProvider {
+    private static final String LOGTAG = SharedBrowserDatabaseProvider.class.getSimpleName();
+
+    private static PerProfileDatabases<BrowserDatabaseHelper> databases;
+
+    @Override
+    protected PerProfileDatabases<BrowserDatabaseHelper> getDatabases() {
+        return databases;
+    }
+
+    @Override
+    public boolean onCreate() {
+        // If necessary, do the shared DB work.
+        synchronized (SharedBrowserDatabaseProvider.class) {
+            if (databases != null) {
+                return true;
+            }
+
+            final DatabaseHelperFactory<BrowserDatabaseHelper> helperFactory = new DatabaseHelperFactory<BrowserDatabaseHelper>() {
+                @Override
+                public BrowserDatabaseHelper makeDatabaseHelper(Context context, String databasePath) {
+                    return new BrowserDatabaseHelper(context, databasePath);
+                }
+            };
+
+            databases = new PerProfileDatabases<BrowserDatabaseHelper>(getContext(), BrowserDatabaseHelper.DATABASE_NAME, helperFactory);
+        }
+
+        return true;
+    }
+
+    /**
+     * Clean up some deleted records from the specified table.
+     *
+     * If called in an existing transaction, it is the caller's responsibility
+     * to ensure that the transaction is already upgraded to a writer, because
+     * this method issues a read followed by a write, and thus is potentially
+     * vulnerable to an unhandled SQLITE_BUSY failure during the upgrade.
+     *
+     * If not called in an existing transaction, no new explicit transaction
+     * will be begun.
+     */
+    protected void cleanUpSomeDeletedRecords(Uri fromUri, String tableName) {
+        Log.d(LOGTAG, "Cleaning up deleted records from " + tableName);
+
+        // We clean up records marked as deleted that are older than a
+        // predefined max age. It's important not be too greedy here and
+        // remove only a few old deleted records at a time.
+
+        // we cleanup records marked as deleted that are older than a
+        // predefined max age. It's important not be too greedy here and
+        // remove only a few old deleted records at a time.
+
+        // Maximum age of deleted records to be cleaned up (20 days in ms)
+        final long MAX_AGE_OF_DELETED_RECORDS = 86400000 * 20;
+
+        // Number of records marked as deleted to be removed
+        final long DELETED_RECORDS_PURGE_LIMIT = 5;
+
+        // Android SQLite doesn't have LIMIT on DELETE. Instead, query for the
+        // IDs of matching rows, then delete them in one go.
+        final long now = System.currentTimeMillis();
+        final String selection = SyncColumns.IS_DELETED + " = 1 AND " +
+                SyncColumns.DATE_MODIFIED + " <= " +
+                (now - MAX_AGE_OF_DELETED_RECORDS);
+
+        final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
+        final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri));
+        final String[] ids;
+        final String limit = Long.toString(DELETED_RECORDS_PURGE_LIMIT, 10);
+        final Cursor cursor = db.query(tableName, new String[] { CommonColumns._ID }, selection, null, null, null, null, limit);
+        try {
+            ids = new String[cursor.getCount()];
+            int i = 0;
+            while (cursor.moveToNext()) {
+                ids[i++] = Long.toString(cursor.getLong(0), 10);
+            }
+        } finally {
+            cursor.close();
+        }
+
+        final String inClause = computeSQLInClause(ids.length,
+                CommonColumns._ID);
+        db.delete(tableName, inClause, ids);
+    }
+}
deleted file mode 100644
--- a/mobile/android/base/db/TransactionalProvider.java
+++ /dev/null
@@ -1,513 +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.db;
-
-import org.mozilla.gecko.db.BrowserContract.CommonColumns;
-import org.mozilla.gecko.db.BrowserContract.SyncColumns;
-import org.mozilla.gecko.db.PerProfileDatabases.DatabaseHelperFactory;
-import org.mozilla.gecko.mozglue.RobocopTarget;
-
-import android.content.ContentProvider;
-import android.content.ContentUris;
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-import android.net.Uri;
-import android.os.Build;
-import android.text.TextUtils;
-import android.util.Log;
-
-/*
- * Abstract class containing methods needed to make a SQLite-based content provider with a
- * database helper of type T. Abstract methods insertInTransaction, deleteInTransaction and
- * updateInTransaction all called within a DB transaction so failed modifications can be rolled-back.
- */
-public abstract class TransactionalProvider<T extends SQLiteOpenHelper> extends ContentProvider {
-    private static final String LOGTAG = "GeckoTransProvider";
-    protected Context mContext;
-    protected PerProfileDatabases<T> mDatabases;
-
-    /*
-     * Returns the name of the database file. Used to get a path
-     * to the DB file.
-     *
-     * @return name of the database file
-     */
-    abstract protected String getDatabaseName();
-
-    /*
-     * Creates and returns an instance of a DB helper. Given a
-     * context and a path to the DB file
-     *
-     * @param  context       to use to create the database helper
-     * @param  databasePath  path to the DB  file
-     * @return               instance of the database helper
-     */
-    abstract protected T createDatabaseHelper(Context context, String databasePath);
-
-    /*
-     * Inserts an item into the database within a DB transaction.
-     *
-     * @param uri    query URI
-     * @param values column values to be inserted
-     * @return       a URI for the newly inserted item
-     */
-    abstract protected Uri insertInTransaction(Uri uri, ContentValues values);
-
-    /*
-     * Deletes items from the database within a DB transaction.
-     *
-     * @param uri            Query URI.
-     * @param selection      An optional filter to match rows to delete.
-     * @param selectionArgs  An array of arguments to substitute into the selection.
-     *
-     * @return number of rows impacted by the deletion.
-     */
-    abstract protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
-
-    /*
-     * Updates the database within a DB transaction.
-     *
-     * @param uri            Query URI.
-     * @param values         A set of column_name/value pairs to add to the database.
-     * @param selection      An optional filter to match rows to update.
-     * @param selectionArgs  An array of arguments to substitute into the selection.
-     *
-     * @return               number of rows impacted by the update.
-     */
-    abstract protected int updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs);
-
-    /*
-     * Fetches a readable database based on the profile indicated in the
-     * passed URI. If the URI does not contain a profile param, the default profile
-     * is used.
-     *
-     * @param uri content URI optionally indicating the profile of the user
-     * @return    instance of a readable SQLiteDatabase
-     */
-    protected SQLiteDatabase getReadableDatabase(Uri uri) {
-        String profile = null;
-        if (uri != null) {
-            profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
-        }
-
-        return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).getReadableDatabase();
-    }
-
-    /*
-     * Fetches a writeable database based on the profile indicated in the
-     * passed URI. If the URI does not contain a profile param, the default profile
-     * is used
-     *
-     * @param uri content URI optionally indicating the profile of the user
-     * @return    instance of a writeable SQLiteDatabase
-     */
-    protected SQLiteDatabase getWritableDatabase(Uri uri) {
-        String profile = null;
-        if (uri != null) {
-            profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE);
-        }
-
-        return mDatabases.getDatabaseHelperForProfile(profile, isTest(uri)).getWritableDatabase();
-    }
-
-    /**
-     * Public version of {@link #getWritableDatabase(Uri) getWritableDatabase}.
-     * This method should ONLY be used for testing purposes.
-     *
-     * @param uri content URI optionally indicating the profile of the user
-     * @return    instance of a writeable SQLiteDatabase
-     */
-    @RobocopTarget
-    public SQLiteDatabase getWritableDatabaseForTesting(Uri uri) {
-        return getWritableDatabase(uri);
-    }
-
-    /**
-     * Return true of the query is from Firefox Sync.
-     * @param uri query URI
-     */
-    public static boolean isCallerSync(Uri uri) {
-        String isSync = uri.getQueryParameter(BrowserContract.PARAM_IS_SYNC);
-        return !TextUtils.isEmpty(isSync);
-    }
-
-    /**
-     * Indicates whether a query should include deleted fields
-     * based on the URI.
-     * @param uri query URI
-     */
-    public static boolean shouldShowDeleted(Uri uri) {
-        String showDeleted = uri.getQueryParameter(BrowserContract.PARAM_SHOW_DELETED);
-        return !TextUtils.isEmpty(showDeleted);
-    }
-
-    /**
-     * Indicates whether an insertion should be made if a record doesn't
-     * exist, based on the URI.
-     * @param uri query URI
-     */
-    public static boolean shouldUpdateOrInsert(Uri uri) {
-        String insertIfNeeded = uri.getQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED);
-        return Boolean.parseBoolean(insertIfNeeded);
-    }
-
-    /**
-     * Indicates whether query is a test based on the URI.
-     * @param uri query URI
-     */
-    public static boolean isTest(Uri uri) {
-        String isTest = uri.getQueryParameter(BrowserContract.PARAM_IS_TEST);
-        return !TextUtils.isEmpty(isTest);
-    }
-
-    protected SQLiteDatabase getWritableDatabaseForProfile(String profile, boolean isTest) {
-        return mDatabases.getDatabaseHelperForProfile(profile, isTest).getWritableDatabase();
-    }
-
-    @Override
-    public boolean onCreate() {
-        synchronized (this) {
-            mContext = getContext();
-            mDatabases = new PerProfileDatabases<T>(
-                getContext(), getDatabaseName(), new DatabaseHelperFactory<T>() {
-                    @Override
-                    public T makeDatabaseHelper(Context context, String databasePath) {
-                        return createDatabaseHelper(context, databasePath);
-                    }
-                });
-        }
-
-        return true;
-    }
-
-    /**
-     * Return true if OS version and database parallelism support indicates
-     * that this provider should bundle writes into transactions.
-     */
-    @SuppressWarnings("static-method")
-    protected boolean shouldUseTransactions() {
-        return Build.VERSION.SDK_INT >= 11;
-    }
-
-    /**
-     * Track whether we're in a batch operation.
-     *
-     * When we're in a batch operation, individual write steps won't even try
-     * to start a transaction... and neither will they attempt to finish one.
-     *
-     * Set this to <code>Boolean.TRUE</code> when you're entering a batch --
-     * a section of code in which {@link ContentProvider} methods will be
-     * called, but nested transactions should not be started. Callers are
-     * responsible for beginning and ending the enclosing transaction, and
-     * for setting this to <code>Boolean.FALSE</code> when done.
-     *
-     * This is a ThreadLocal separate from `db.inTransaction` because batched
-     * operations start transactions independent of individual ContentProvider
-     * operations. This doesn't work well with the entire concept of this
-     * abstract class -- that is, automatically beginning and ending transactions
-     * for each insert/delete/update operation -- and doing so without
-     * causing arbitrary nesting requires external tracking.
-     *
-     * Note that beginWrite takes a DB argument, but we don't differentiate
-     * between databases in this tracking flag. If your ContentProvider manages
-     * multiple database transactions within the same thread, you'll need to
-     * amend this scheme -- but then, you're already doing some serious wizardry,
-     * so rock on.
-     */
-    final ThreadLocal<Boolean> isInBatchOperation = new ThreadLocal<Boolean>();
-
-    private boolean isInBatch() {
-        final Boolean isInBatch = isInBatchOperation.get();
-        if (isInBatch == null) {
-            return false;
-        }
-        return isInBatch.booleanValue();
-    }
-
-    /**
-     * If we're not currently in a transaction, and we should be, start one.
-     */
-    protected void beginWrite(final SQLiteDatabase db) {
-        if (isInBatch()) {
-            trace("Not bothering with an intermediate write transaction: inside batch operation.");
-            return;
-        }
-
-        if (shouldUseTransactions() && !db.inTransaction()) {
-            trace("beginWrite: beginning transaction.");
-            db.beginTransaction();
-        }
-    }
-
-    /**
-     * If we're not in a batch, but we are in a write transaction, mark it as
-     * successful.
-     */
-    protected void markWriteSuccessful(final SQLiteDatabase db) {
-        if (isInBatch()) {
-            trace("Not marking write successful: inside batch operation.");
-            return;
-        }
-
-        if (shouldUseTransactions() && db.inTransaction()) {
-            trace("Marking write transaction successful.");
-            db.setTransactionSuccessful();
-        }
-    }
-
-    /**
-     * If we're not in a batch, but we are in a write transaction,
-     * end it.
-     *
-     * @see TransactionalProvider#markWriteSuccessful(SQLiteDatabase)
-     */
-    protected void endWrite(final SQLiteDatabase db) {
-        if (isInBatch()) {
-            trace("Not ending write: inside batch operation.");
-            return;
-        }
-
-        if (shouldUseTransactions() && db.inTransaction()) {
-            trace("endWrite: ending transaction.");
-            db.endTransaction();
-        }
-    }
-
-    protected void beginBatch(final SQLiteDatabase db) {
-        trace("Beginning batch.");
-        isInBatchOperation.set(Boolean.TRUE);
-        db.beginTransaction();
-    }
-
-    protected void markBatchSuccessful(final SQLiteDatabase db) {
-        if (isInBatch()) {
-            trace("Marking batch successful.");
-            db.setTransactionSuccessful();
-            return;
-        }
-        Log.w(LOGTAG, "Unexpectedly asked to mark batch successful, but not in batch!");
-        throw new IllegalStateException("Not in batch.");
-    }
-
-    protected void endBatch(final SQLiteDatabase db) {
-        trace("Ending batch.");
-        db.endTransaction();
-        isInBatchOperation.set(Boolean.FALSE);
-    }
-
-    /*
-     * This utility is replicated from RepoUtils, which is managed by android-sync.
-     */
-    protected static String computeSQLInClause(int items, String field) {
-        final StringBuilder builder = new StringBuilder(field);
-        builder.append(" IN (");
-        int i = 0;
-        for (; i < items - 1; ++i) {
-            builder.append("?, ");
-        }
-        if (i < items) {
-            builder.append("?");
-        }
-        builder.append(")");
-        return builder.toString();
-    }
-
-    /**
-     * Turn a single-column cursor of longs into a single SQL "IN" clause.
-     * We can do this without using selection arguments because Long isn't
-     * vulnerable to injection.
-     */
-    protected static String computeSQLInClauseFromLongs(final Cursor cursor, String field) {
-        final StringBuilder builder = new StringBuilder(field);
-        builder.append(" IN (");
-        final int commaLimit = cursor.getCount() - 1;
-        int i = 0;
-        while (cursor.moveToNext()) {
-            builder.append(cursor.getLong(0));
-            if (i++ < commaLimit) {
-                builder.append(", ");
-            }
-        }
-        builder.append(")");
-        return builder.toString();
-    }
-
-    @Override
-    public int delete(Uri uri, String selection, String[] selectionArgs) {
-        trace("Calling delete on URI: " + uri + ", " + selection + ", " + selectionArgs);
-
-        final SQLiteDatabase db = getWritableDatabase(uri);
-        int deleted = 0;
-
-        try {
-            deleted = deleteInTransaction(uri, selection, selectionArgs);
-            markWriteSuccessful(db);
-        } finally {
-            endWrite(db);
-        }
-
-        if (deleted > 0) {
-            getContext().getContentResolver().notifyChange(uri, null);
-        }
-
-        return deleted;
-    }
-
-    @Override
-    public Uri insert(Uri uri, ContentValues values) {
-        trace("Calling insert on URI: " + uri);
-
-        final SQLiteDatabase db = getWritableDatabase(uri);
-        Uri result = null;
-        try {
-            result = insertInTransaction(uri, values);
-            markWriteSuccessful(db);
-        } catch (SQLException sqle) {
-            Log.e(LOGTAG, "exception in DB operation", sqle);
-        } catch (UnsupportedOperationException uoe) {
-            Log.e(LOGTAG, "don't know how to perform that insert", uoe);
-        } finally {
-            endWrite(db);
-        }
-
-        if (result != null) {
-            getContext().getContentResolver().notifyChange(uri, null);
-        }
-
-        return result;
-    }
-
-
-    @Override
-    public int update(Uri uri, ContentValues values, String selection,
-            String[] selectionArgs) {
-        trace("Calling update on URI: " + uri + ", " + selection + ", " + selectionArgs);
-
-        final SQLiteDatabase db = getWritableDatabase(uri);
-        int updated = 0;
-
-        try {
-            updated = updateInTransaction(uri, values, selection,
-                                          selectionArgs);
-            markWriteSuccessful(db);
-        } finally {
-            endWrite(db);
-        }
-
-        if (updated > 0) {
-            getContext().getContentResolver().notifyChange(uri, null);
-        }
-
-        return updated;
-    }
-
-    @Override
-    public int bulkInsert(Uri uri, ContentValues[] values) {
-        if (values == null) {
-            return 0;
-        }
-
-        int numValues = values.length;
-        int successes = 0;
-
-        final SQLiteDatabase db = getWritableDatabase(uri);
-
-        debug("bulkInsert: explicitly starting transaction.");
-        beginBatch(db);
-
-        try {
-            for (int i = 0; i < numValues; i++) {
-                insertInTransaction(uri, values[i]);
-                successes++;
-            }
-            trace("Flushing DB bulkinsert...");
-            markBatchSuccessful(db);
-        } finally {
-            debug("bulkInsert: explicitly ending transaction.");
-            endBatch(db);
-        }
-
-        if (successes > 0) {
-            mContext.getContentResolver().notifyChange(uri, null);
-        }
-
-        return successes;
-    }
-
-    /**
-     * Clean up some deleted records from the specified table.
-     *
-     * If called in an existing transaction, it is the caller's responsibility
-     * to ensure that the transaction is already upgraded to a writer, because
-     * this method issues a read followed by a write, and thus is potentially
-     * vulnerable to an unhandled SQLITE_BUSY failure during the upgrade.
-     *
-     * If not called in an existing transaction, no new explicit transaction
-     * will be begun.
-     */
-    protected void cleanupSomeDeletedRecords(Uri fromUri, Uri targetUri, String tableName) {
-        Log.d(LOGTAG, "Cleaning up deleted records from " + tableName);
-
-        // We clean up records marked as deleted that are older than a
-        // predefined max age. It's important not be too greedy here and
-        // remove only a few old deleted records at a time.
-
-        // we cleanup records marked as deleted that are older than a
-        // predefined max age. It's important not be too greedy here and
-        // remove only a few old deleted records at a time.
-
-        // Maximum age of deleted records to be cleaned up (20 days in ms)
-        final long MAX_AGE_OF_DELETED_RECORDS = 86400000 * 20;
-
-        // Number of records marked as deleted to be removed
-        final long DELETED_RECORDS_PURGE_LIMIT = 5;
-
-        // Android SQLite doesn't have LIMIT on DELETE. Instead, query for the
-        // IDs of matching rows, then delete them in one go.
-        final long now = System.currentTimeMillis();
-        final String selection = SyncColumns.IS_DELETED + " = 1 AND " +
-                                 SyncColumns.DATE_MODIFIED + " <= " +
-                                 (now - MAX_AGE_OF_DELETED_RECORDS);
-
-        final String profile = fromUri.getQueryParameter(BrowserContract.PARAM_PROFILE);
-        final SQLiteDatabase db = getWritableDatabaseForProfile(profile, isTest(fromUri));
-        final String[] ids;
-        final String limit = Long.toString(DELETED_RECORDS_PURGE_LIMIT, 10);
-        final Cursor cursor = db.query(tableName, new String[] { CommonColumns._ID }, selection, null, null, null, null, limit);
-        try {
-            ids = new String[cursor.getCount()];
-            int i = 0;
-            while (cursor.moveToNext()) {
-                ids[i++] = Long.toString(cursor.getLong(0), 10);
-            }
-        } finally {
-            cursor.close();
-        }
-
-        final String inClause = computeSQLInClause(ids.length,
-                                                   CommonColumns._ID);
-        db.delete(tableName, inClause, ids);
-    }
-
-    // Calculate these once, at initialization. isLoggable is too expensive to
-    // have in-line in each log call.
-    private static boolean logDebug  = Log.isLoggable(LOGTAG, Log.DEBUG);
-    private static boolean logVerbose = Log.isLoggable(LOGTAG, Log.VERBOSE);
-    protected static void trace(String message) {
-        if (logVerbose) {
-            Log.v(LOGTAG, message);
-        }
-    }
-
-    protected static void debug(String message) {
-        if (logDebug) {
-            Log.d(LOGTAG, message);
-        }
-    }
-}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -111,30 +111,33 @@ gbjar.sources += [
     'ANRReporter.java',
     'AppNotificationClient.java',
     'BaseGeckoInterface.java',
     'BrowserApp.java',
     'ContactService.java',
     'ContextGetter.java',
     'CustomEditText.java',
     'DataReportingNotification.java',
+    'db/AbstractPerProfileDatabaseProvider.java',
+    'db/AbstractTransactionalProvider.java',
     'db/BrowserContract.java',
     'db/BrowserDatabaseHelper.java',
     'db/BrowserDB.java',
     'db/BrowserProvider.java',
     'db/DBUtils.java',
     'db/FormHistoryProvider.java',
     'db/HomeProvider.java',
     'db/LocalBrowserDB.java',
     'db/PasswordsProvider.java',
+    'db/PerProfileDatabaseProvider.java',
     'db/PerProfileDatabases.java',
     'db/ReadingListProvider.java',
+    'db/SharedBrowserDatabaseProvider.java',
     'db/SQLiteBridgeContentProvider.java',
     'db/TabsProvider.java',
-    'db/TransactionalProvider.java',
     'Distribution.java',
     'DoorHangerPopup.java',
     'DynamicToolbar.java',
     'EditBookmarkDialog.java',
     'EventDispatcher.java',
     'favicons/cache/FaviconCache.java',
     'favicons/cache/FaviconCacheElement.java',
     'favicons/cache/FaviconsForURL.java',