Bug 1077590 - Part 1: make all per-profile DB access go through a profile. r=wesj
authorRichard Newman <rnewman@mozilla.com>
Sun, 11 Jan 2015 20:45:09 -0800
changeset 223425 3fc5da45d29e8bc823395b289c82a69000dffb1a
parent 223424 48f0e0b28a4676b04c86b661210598d9877bf57b
child 223426 d4af48565428e37024733380717d992cddcd47a0
push id10785
push userrnewman@mozilla.com
push dateTue, 13 Jan 2015 04:58:30 +0000
treeherderfx-team@fbdcb7d48dd9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerswesj
bugs1077590
milestone38.0a1
Bug 1077590 - Part 1: make all per-profile DB access go through a profile. r=wesj * * * Bug 1077590 - Review comments.
mobile/android/base/BrowserApp.java
mobile/android/base/EditBookmarkDialog.java
mobile/android/base/GeckoApp.java
mobile/android/base/GeckoAppShell.java
mobile/android/base/GeckoApplication.java
mobile/android/base/GeckoProfile.java
mobile/android/base/GeckoView.java
mobile/android/base/GlobalHistory.java
mobile/android/base/MemoryMonitor.java
mobile/android/base/PrivateTab.java
mobile/android/base/ReadingListHelper.java
mobile/android/base/RemoteClientsDialogFragment.java
mobile/android/base/RemoteTabsExpandableListAdapter.java
mobile/android/base/Tab.java
mobile/android/base/Tabs.java
mobile/android/base/TabsAccessor.java
mobile/android/base/ThumbnailHelper.java
mobile/android/base/db/BrowserDB.java
mobile/android/base/db/BrowserProvider.java
mobile/android/base/db/DBUtils.java
mobile/android/base/db/LocalBrowserDB.java
mobile/android/base/db/LocalSearches.java
mobile/android/base/db/LocalTabsAccessor.java
mobile/android/base/db/LocalURLMetadata.java
mobile/android/base/db/RemoteClient.java
mobile/android/base/db/RemoteTab.java
mobile/android/base/db/Searches.java
mobile/android/base/db/StubBrowserDB.java
mobile/android/base/db/TabsAccessor.java
mobile/android/base/db/URLMetadata.java
mobile/android/base/db/URLMetadataTable.java
mobile/android/base/favicons/Favicons.java
mobile/android/base/favicons/LoadFaviconTask.java
mobile/android/base/gfx/BitmapUtils.java
mobile/android/base/home/BookmarksPanel.java
mobile/android/base/home/HistoryPanel.java
mobile/android/base/home/HomeFragment.java
mobile/android/base/home/PinSiteDialog.java
mobile/android/base/home/ReadingListPanel.java
mobile/android/base/home/RemoteTabsExpandableListFragment.java
mobile/android/base/home/SearchLoader.java
mobile/android/base/home/TopSitesPanel.java
mobile/android/base/moz.build
mobile/android/base/tests/DatabaseHelper.java
mobile/android/base/tests/testClearPrivateData.java
mobile/android/base/tests/testDistribution.java
mobile/android/base/tests/testFilterOpenTab.java
mobile/android/base/tests/testPrivateBrowsing.java
mobile/android/base/tests/testThumbnails.java
mobile/android/base/toolbar/BrowserToolbar.java
mobile/android/base/webapp/WebappImpl.java
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -13,30 +13,30 @@ import java.lang.reflect.Method;
 import java.net.URLEncoder;
 import java.util.EnumSet;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Vector;
 
 import android.support.v4.app.Fragment;
+
 import org.json.JSONException;
 import org.json.JSONObject;
-
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.DynamicToolbar.PinReason;
 import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.Tabs.TabEvents;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.TransitionsTracker;
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.db.BrowserContract.Combined;
-import org.mozilla.gecko.db.BrowserContract.SearchHistory;
 import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.LocalBrowserDB;
 import org.mozilla.gecko.db.SuggestedSites;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.LoadFaviconTask;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
@@ -90,16 +90,17 @@ import org.mozilla.gecko.util.ThreadUtil
 import org.mozilla.gecko.util.UIAsyncTask;
 import org.mozilla.gecko.widget.ButtonToast;
 import org.mozilla.gecko.widget.ButtonToast.ToastListener;
 import org.mozilla.gecko.widget.GeckoActionProvider;
 
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.Configuration;
@@ -614,17 +615,18 @@ public class BrowserApp extends GeckoApp
             "Telemetry:Gather",
             "Updater:Launch",
             "BrowserToolbar:Visibility");
 
         Distribution distribution = Distribution.init(this);
 
         // Init suggested sites engine in BrowserDB.
         final SuggestedSites suggestedSites = new SuggestedSites(appContext, distribution);
-        BrowserDB.setSuggestedSites(suggestedSites);
+        final BrowserDB db = getProfile().getDB();
+        db.setSuggestedSites(suggestedSites);
 
         JavaAddonManager.getInstance().init(appContext);
         mSharedPreferencesHelper = new SharedPreferencesHelper(appContext);
         mOrderedBroadcastHelper = new OrderedBroadcastHelper(appContext);
         mBrowserHealthReporter = new BrowserHealthReporter();
         mReadingListHelper = new ReadingListHelper(appContext);
 
         if (AppConstants.MOZ_ANDROID_BEAM) {
@@ -1517,26 +1519,23 @@ public class BrowserApp extends GeckoApp
 
             // Don't use a transition to settings if we're on a device where that
             // would look bad.
             if (HardwareUtils.IS_KINDLE_DEVICE) {
                 overridePendingTransition(0, 0);
             }
 
         } else if ("Telemetry:Gather".equals(event)) {
-            Telemetry.addToHistogram("PLACES_PAGES_COUNT",
-                    BrowserDB.getCount(getContentResolver(), "history"));
-            Telemetry.addToHistogram("PLACES_BOOKMARKS_COUNT",
-                    BrowserDB.getCount(getContentResolver(), "bookmarks"));
-            Telemetry.addToHistogram("FENNEC_FAVICONS_COUNT",
-                    BrowserDB.getCount(getContentResolver(), "favicons"));
-            Telemetry.addToHistogram("FENNEC_THUMBNAILS_COUNT",
-                    BrowserDB.getCount(getContentResolver(), "thumbnails"));
-            Telemetry.addToHistogram("FENNEC_READING_LIST_COUNT",
-                    BrowserDB.getCount(getContentResolver(), "readinglist"));
+            final BrowserDB db = getProfile().getDB();
+            final ContentResolver cr = getContentResolver();
+            Telemetry.addToHistogram("PLACES_PAGES_COUNT", db.getCount(cr, "history"));
+            Telemetry.addToHistogram("PLACES_BOOKMARKS_COUNT", db.getCount(cr, "bookmarks"));
+            Telemetry.addToHistogram("FENNEC_FAVICONS_COUNT", db.getCount(cr, "favicons"));
+            Telemetry.addToHistogram("FENNEC_THUMBNAILS_COUNT", db.getCount(cr, "thumbnails"));
+            Telemetry.addToHistogram("FENNEC_READING_LIST_COUNT", db.getCount(getContentResolver(), "readinglist"));
             Telemetry.addToHistogram("BROWSER_IS_USER_DEFAULT", (isDefaultBrowser(Intent.ACTION_VIEW) ? 1 : 0));
             if (Versions.feature16Plus) {
                 Telemetry.addToHistogram("BROWSER_IS_ASSIST_DEFAULT", (isDefaultBrowser(Intent.ACTION_ASSIST) ? 1 : 0));
             }
         } else if ("Updater:Launch".equals(event)) {
             handleUpdaterLaunch();
 
         } else if ("BrowserToolbar:Visibility".equals(event)) {
@@ -2020,32 +2019,33 @@ public class BrowserApp extends GeckoApp
         // If the URL doesn't look like a search query, just load it.
         if (!StringUtils.isSearchQuery(url, true)) {
             Tabs.getInstance().loadUrl(url, Tabs.LOADURL_USER_ENTERED);
             Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.ACTIONBAR, "user");
             return;
         }
 
         // Otherwise, check for a bookmark keyword.
+        final BrowserDB db = getProfile().getDB();
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
                 final String keyword;
                 final String keywordSearch;
 
                 final int index = url.indexOf(" ");
                 if (index == -1) {
                     keyword = url;
                     keywordSearch = "";
                 } else {
                     keyword = url.substring(0, index);
                     keywordSearch = url.substring(index + 1);
                 }
 
-                final String keywordUrl = BrowserDB.getUrlForKeyword(getContentResolver(), keyword);
+                final String keywordUrl = db.getUrlForKeyword(getContentResolver(), keyword);
 
                 // If there isn't a bookmark keyword, load the url. This may result in a query
                 // using the default search engine.
                 if (TextUtils.isEmpty(keywordUrl)) {
                     Tabs.getInstance().loadUrl(url, Tabs.LOADURL_USER_ENTERED);
                     Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.ACTIONBAR, "user");
                     return;
                 }
@@ -2085,27 +2085,32 @@ public class BrowserApp extends GeckoApp
     }
 
     /**
      * Store search query in SearchHistoryProvider.
      *
      * @param query
      *        a search query to store. We won't store empty queries.
      */
-    private void storeSearchQuery(String query) {
+    private void storeSearchQuery(final String query) {
         if (TextUtils.isEmpty(query)) {
             return;
         }
 
-        final ContentValues values = new ContentValues();
-        values.put(SearchHistory.QUERY, query);
+        final GeckoProfile profile = getProfile();
+        // Don't bother storing search queries in guest mode
+        if (profile.inGuestMode()) {
+            return;
+        }
+
+        final BrowserDB db = profile.getDB();
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
-                getContentResolver().insert(SearchHistory.CONTENT_URI, values);
+                db.getSearches().insert(getContentResolver(), query);
             }
         });
     }
 
     void filterEditingMode(String searchTerm, AutocompleteHandler handler) {
         if (TextUtils.isEmpty(searchTerm)) {
             hideBrowserSearch();
         } else {
@@ -3144,32 +3149,33 @@ public class BrowserApp extends GeckoApp
     }
 
     private void resetFeedbackLaunchCount() {
         SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE);
         settings.edit().putInt(getPackageName() + ".feedback_launch_count", 0).apply();
     }
 
     private void getLastUrl(final EventCallback callback) {
+        final BrowserDB db = getProfile().getDB();
         (new UIAsyncTask.WithoutParams<String>(ThreadUtils.getBackgroundHandler()) {
             @Override
             public synchronized String doInBackground() {
                 // Get the most recent URL stored in browser history.
-                String url = "";
-                Cursor c = null;
+                final Cursor c = db.getRecentHistory(getContentResolver(), 1);
+                if (c == null) {
+                    return "";
+                }
                 try {
-                    c = BrowserDB.getRecentHistory(getContentResolver(), 1);
                     if (c.moveToFirst()) {
-                        url = c.getString(c.getColumnIndexOrThrow(Combined.URL));
+                        return c.getString(c.getColumnIndexOrThrow(Combined.URL));
                     }
+                    return "";
                 } finally {
-                    if (c != null)
-                        c.close();
+                    c.close();
                 }
-                return url;
             }
 
             @Override
             public void onPostExecute(String url) {
                 callback.sendSuccess(url);
             }
         }).execute();
     }
@@ -3255,16 +3261,30 @@ public class BrowserApp extends GeckoApp
     @Override
     public int getLayout() { return R.layout.gecko_app; }
 
     @Override
     protected String getDefaultProfileName() throws NoMozillaDirectoryException {
         return GeckoProfile.getDefaultProfileName(this);
     }
 
+    // We want a real BrowserDB.
+    @Override
+    protected BrowserDB.Factory getBrowserDBFactory() {
+        return new BrowserDB.Factory() {
+            @Override
+            public BrowserDB get(String profileName, File profileDir) {
+                // Note that we don't use the profile directory -- we
+                // send operations to the ContentProvider, which does
+                // its own thing.
+                return new LocalBrowserDB(profileName);
+            }
+        };
+    }
+
     /**
      * Launch UI that lets the user update Firefox.
      *
      * This depends on the current channel: Release and Beta both direct to the
      * Google Play Store.  If updating is enabled, Aurora, Nightly, and custom
      * builds open about:, which provides an update interface.
      *
      * If updating is not enabled, this simply logs an error.
--- a/mobile/android/base/EditBookmarkDialog.java
+++ b/mobile/android/base/EditBookmarkDialog.java
@@ -1,17 +1,17 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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;
 
+import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
-import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UIAsyncTask;
 
 import android.content.ContentResolver;
 import android.content.Context;
 import android.app.AlertDialog;
 import android.content.DialogInterface;
 import android.database.Cursor;
@@ -136,20 +136,21 @@ public class EditBookmarkDialog {
      *
      * @param url The url of the bookmark to edit. The dialog will look up other information like the id,
      *            current title, or keywords associated with this url. If the url isn't bookmarked, the
      *            dialog will fail silently. If the url is bookmarked multiple times, this will only show
      *            information about the first it finds.
      */
     public void show(final String url) {
         final ContentResolver cr = mContext.getContentResolver();
+        final BrowserDB db = GeckoProfile.get(mContext).getDB();
         (new UIAsyncTask.WithoutParams<Bookmark>(ThreadUtils.getBackgroundHandler()) {
             @Override
             public Bookmark doInBackground() {
-                final Cursor cursor = BrowserDB.getBookmarkForUrl(cr, url);
+                final Cursor cursor = db.getBookmarkForUrl(cr, url);
                 if (cursor == null) {
                     return null;
                 }
 
                 Bookmark bookmark = null;
                 try {
                     cursor.moveToFirst();
                     bookmark = new Bookmark(cursor.getInt(cursor.getColumnIndexOrThrow(Bookmarks._ID)),
@@ -194,25 +195,27 @@ public class EditBookmarkDialog {
 
         final EditText nameText = ((EditText) editView.findViewById(R.id.edit_bookmark_name));
         final EditText locationText = ((EditText) editView.findViewById(R.id.edit_bookmark_location));
         final EditText keywordText = ((EditText) editView.findViewById(R.id.edit_bookmark_keyword));
         nameText.setText(title);
         locationText.setText(url);
         keywordText.setText(keyword);
 
+        final BrowserDB db = GeckoProfile.get(mContext).getDB();
         editPrompt.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() {
             @Override
             public void onClick(DialogInterface dialog, int whichButton) {
                 (new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
                     @Override
                     public Void doInBackground() {
                         String newUrl = locationText.getText().toString().trim();
                         String newKeyword = keywordText.getText().toString().trim();
-                        BrowserDB.updateBookmark(context.getContentResolver(), id, newUrl, nameText.getText().toString(), newKeyword);
+
+                        db.updateBookmark(context.getContentResolver(), id, newUrl, nameText.getText().toString(), newKeyword);
                         return null;
                     }
 
                     @Override
                     public void onPostExecute(Void result) {
                         Toast.makeText(context, R.string.bookmark_updated, Toast.LENGTH_SHORT).show();
                     }
                 }).execute();
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -120,18 +120,26 @@ public abstract class GeckoApp
     ContextGetter,
     GeckoAppShell.GeckoInterface,
     GeckoEventListener,
     GeckoMenu.Callback,
     GeckoMenu.MenuPresenter,
     LocationListener,
     NativeEventListener,
     SensorEventListener,
-    Tabs.OnTabsChangedListener
-{
+    Tabs.OnTabsChangedListener {
+
+    protected GeckoApp() {
+        // We need to do this before any access to the profile; it controls
+        // which database class is used.
+        // We thus need to do this before our GeckoView is inflated, because
+        // GeckoView implicitly accesses the profile.
+        GeckoProfile.setBrowserDBFactory(getBrowserDBFactory());
+    }
+
     private static final String LOGTAG = "GeckoApp";
     private static final int ONE_DAY_MS = 1000*60*60*24;
 
     private static enum StartupAction {
         NORMAL,     /* normal application start */
         URL,        /* launched with a passed URL */
         PREFETCH    /* launched with a passed URL that we prefetch */
     }
@@ -225,16 +233,18 @@ public abstract class GeckoApp
         return this;
     }
 
     @Override
     public SharedPreferences getSharedPreferences() {
         return GeckoSharedPrefs.forApp(this);
     }
 
+    protected abstract BrowserDB.Factory getBrowserDBFactory();
+
     @Override
     public Activity getActivity() {
         return this;
     }
 
     @Override
     public LocationListener getLocationListener() {
         return this;
@@ -518,17 +528,23 @@ public abstract class GeckoApp
     protected void onSaveInstanceState(Bundle outState) {
         super.onSaveInstanceState(outState);
 
         outState.putBoolean(SAVED_STATE_IN_BACKGROUND, isApplicationInBackground());
         outState.putString(SAVED_STATE_PRIVATE_SESSION, mPrivateBrowsingSession);
     }
 
     void handleClearHistory() {
-        BrowserDB.clearHistory(getContentResolver());
+        final BrowserDB db = getProfile().getDB();
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                db.clearHistory(getContentResolver());
+            }
+        });
     }
 
     public void addTab() { }
 
     public void addPrivateTab() { }
 
     public void showNormalTabs() { }
 
@@ -553,20 +569,21 @@ public abstract class GeckoApp
                               final EventCallback callback) {
         if ("Accessibility:Ready".equals(event)) {
             GeckoAccessibility.updateAccessibilitySettings(this);
 
         } else if ("Bookmark:Insert".equals(event)) {
             final String url = message.getString("url");
             final String title = message.getString("title");
             final Context context = this;
+            final BrowserDB db = getProfile().getDB();
             ThreadUtils.postToBackgroundThread(new Runnable() {
                 @Override
                 public void run() {
-                    BrowserDB.addBookmark(getContentResolver(), title, url);
+                    db.addBookmark(getContentResolver(), title, url);
                     ThreadUtils.postToUiThread(new Runnable() {
                         @Override
                         public void run() {
                             Toast.makeText(context, R.string.bookmark_added, Toast.LENGTH_SHORT).show();
                         }
                     });
                 }
             });
@@ -1478,18 +1495,16 @@ public abstract class GeckoApp
         // Start migrating as early as possible, can do this in
         // parallel with Gecko load.
         checkMigrateProfile();
 
         Tabs.registerOnTabsChangedListener(this);
 
         initializeChrome();
 
-        BrowserDB.initialize(getProfile().getName());
-
         // If we are doing a restore, read the session data and send it to Gecko
         if (!mIsRestoringActivity) {
             String restoreMessage = null;
             if (mShouldRestore) {
                 try {
                     // restoreSessionTabs() will create simple tab stubs with the
                     // URL and title for each page, but we also need to restore
                     // session history. restoreSessionTabs() will inject the IDs
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -26,16 +26,17 @@ import java.util.Map;
 import java.util.NoSuchElementException;
 import java.util.Queue;
 import java.util.StringTokenizer;
 import java.util.TreeMap;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentLinkedQueue;
 
 import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.gfx.PanZoomController;
 import org.mozilla.gecko.mozglue.ContextUtils;
 import org.mozilla.gecko.mozglue.GeckoLoader;
@@ -2290,30 +2291,34 @@ public class GeckoAppShell
 
     @WrapElementForJNI(stubName = "CheckURIVisited")
     static void checkUriVisited(String uri) {
         GlobalHistory.getInstance().checkUriVisited(uri);
     }
 
     @WrapElementForJNI(stubName = "MarkURIVisited")
     static void markUriVisited(final String uri) {
+        final Context context = getContext();
+        final BrowserDB db = GeckoProfile.get(context).getDB();
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
-                GlobalHistory.getInstance().add(uri);
+                GlobalHistory.getInstance().add(context, db, uri);
             }
         });
     }
 
     @WrapElementForJNI(stubName = "SetURITitle")
     static void setUriTitle(final String uri, final String title) {
+        final Context context = getContext();
+        final BrowserDB db = GeckoProfile.get(context).getDB();
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
-                GlobalHistory.getInstance().update(uri, title);
+                GlobalHistory.getInstance().update(context.getContentResolver(), db, uri, title);
             }
         });
     }
 
     @WrapElementForJNI
     static void hideProgressDialog() {
         // unused stub
     }
--- a/mobile/android/base/GeckoApplication.java
+++ b/mobile/android/base/GeckoApplication.java
@@ -85,21 +85,21 @@ public class GeckoApplication extends Ap
             // Notify Gecko that we are pausing; the cache service will be
             // shutdown, closing the disk cache cleanly. If the android
             // low memory killer subsequently kills us, the disk cache will
             // be left in a consistent state, avoiding costly cleanup and
             // re-creation. 
             GeckoAppShell.sendEventToGecko(GeckoEvent.createAppBackgroundingEvent());
             mPausedGecko = true;
 
+            final BrowserDB db = GeckoProfile.get(this).getDB();
             ThreadUtils.postToBackgroundThread(new Runnable() {
                 @Override
                 public void run() {
-                    BrowserDB.expireHistory(getContentResolver(),
-                                            BrowserContract.ExpirePriority.NORMAL);
+                    db.expireHistory(getContentResolver(), BrowserContract.ExpirePriority.NORMAL);
                 }
             });
         }
         GeckoConnectivityReceiver.getInstance().stop();
         GeckoNetworkManager.getInstance().stop();
     }
 
     public void onActivityResume(GeckoActivityStatus activity) {
--- a/mobile/android/base/GeckoProfile.java
+++ b/mobile/android/base/GeckoProfile.java
@@ -12,34 +12,44 @@ import java.io.IOException;
 import java.io.OutputStreamWriter;
 import java.nio.charset.Charset;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.Hashtable;
 
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.GeckoProfileDirectories.NoSuchProfileException;
+import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.LocalBrowserDB;
+import org.mozilla.gecko.db.StubBrowserDB;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.mozglue.RobocopTarget;
 import org.mozilla.gecko.util.INIParser;
 import org.mozilla.gecko.util.INISection;
 
 import android.app.Activity;
 import android.content.ContentResolver;
 import android.content.Context;
-import android.content.Intent;
 import android.content.SharedPreferences;
-import android.support.v4.content.LocalBroadcastManager;
 import android.text.TextUtils;
 import android.util.Log;
 
 public final class GeckoProfile {
     private static final String LOGTAG = "GeckoProfile";
 
+    // Only tests should need to do this.
+    // We can default this to AppConstants.RELEASE_BUILD once we fix Bug 1069687.
+    private static volatile boolean sAcceptDirectoryChanges = true;
+
+    @RobocopTarget
+    public static void enableDirectoryChanges() {
+        Log.w(LOGTAG, "Directory changes should only be enabled for tests. And even then it's a bad idea.");
+        sAcceptDirectoryChanges = true;
+    }
+
     // Used to "lock" the guest profile, so that we'll always restart in it
     private static final String LOCK_FILE_NAME = ".active_lock";
     public static final String DEFAULT_PROFILE = "default";
     public static final String GUEST_PROFILE = "guest";
 
     private static final HashMap<String, GeckoProfile> sProfileCache = new HashMap<String, GeckoProfile>();
     private static String sDefaultProfileName;
 
@@ -49,16 +59,18 @@ public final class GeckoProfile {
 
     public static boolean sIsUsingCustomProfile;
 
     private final String mName;
     private final File mMozillaDir;
     private final boolean mIsWebAppProfile;
     private final Context mApplicationContext;
 
+    private final BrowserDB mDB;
+
     /**
      * Access to this member should be synchronized to avoid
      * races during creation -- particularly between getDir and GeckoView#init.
      *
      * Not final because this is lazily computed. 
      */
     private File mProfileDir;
 
@@ -141,49 +153,88 @@ public final class GeckoProfile {
             dir = new File(profilePath);
             if (!dir.exists() || !dir.isDirectory()) {
                 Log.w(LOGTAG, "requested profile directory missing: " + profilePath);
             }
         }
         return get(context, profileName, dir);
     }
 
+    // Extension hook.
+    private static volatile BrowserDB.Factory sDBFactory;
+    public static void setBrowserDBFactory(BrowserDB.Factory factory) {
+        sDBFactory = factory;
+    }
+
     @RobocopTarget
     public static GeckoProfile get(Context context, String profileName, File profileDir) {
+        if (sDBFactory == null) {
+            // We do this so that GeckoView consumers don't need to know anything about BrowserDB.
+            // It's a bit of a broken abstraction, but very tightly coupled, so we work around it
+            // for now. We can't just have GeckoView set this, because then it would collide in
+            // Fennec's use of GeckoView.
+            Log.d(LOGTAG, "Defaulting to StubBrowserDB.");
+            sDBFactory = StubBrowserDB.getFactory();
+        }
+        return GeckoProfile.get(context, profileName, profileDir, sDBFactory);
+    }
+
+    // Note that the profile cache respects only the profile name!
+    // If the directory changes, the returned GeckoProfile instance will be mutated.
+    // If the factory differs, it will be *ignored*.
+    public static GeckoProfile get(Context context, String profileName, File profileDir, BrowserDB.Factory dbFactory) {
+        Log.v(LOGTAG, "Fetching profile: '" + profileName + "', '" + profileDir + "'");
         if (context == null) {
             throw new IllegalArgumentException("context must be non-null");
         }
 
-        // if no profile was passed in, look for the default profile listed in profiles.ini
-        // if that doesn't exist, look for a profile called 'default'
+        // If no profile was passed in, look for the default profile listed in profiles.ini.
+        // If that doesn't exist, look for a profile called 'default'.
         if (TextUtils.isEmpty(profileName) && profileDir == null) {
             try {
                 profileName = GeckoProfile.getDefaultProfileName(context);
             } catch (NoMozillaDirectoryException e) {
                 // We're unable to do anything sane here.
                 throw new RuntimeException(e);
             }
         }
 
-        // actually try to look up the profile
+        // Actually try to look up the profile.
         synchronized (sProfileCache) {
             GeckoProfile profile = sProfileCache.get(profileName);
             if (profile == null) {
                 try {
-                    profile = new GeckoProfile(context, profileName);
+                    profile = new GeckoProfile(context, profileName, profileDir, dbFactory);
                 } catch (NoMozillaDirectoryException e) {
                     // We're unable to do anything sane here.
                     throw new RuntimeException(e);
                 }
+                sProfileCache.put(profileName, profile);
+                return profile;
+            }
+
+            if (profileDir == null) {
+                // Fine.
+                return profile;
+            }
+
+            if (profile.getDir().equals(profileDir)) {
+                // Great! We're consistent.
+                return profile;
+            }
+
+            if (sAcceptDirectoryChanges) {
+                if (AppConstants.RELEASE_BUILD) {
+                    Log.e(LOGTAG, "Release build trying to switch out profile dir. This is an error, but let's do what we can.");
+                }
                 profile.setDir(profileDir);
-                sProfileCache.put(profileName, profile);
-            } else {
-                profile.setDir(profileDir);
+                return profile;
             }
-            return profile;
+
+            throw new IllegalStateException("Refusing to reuse profile with a different directory.");
         }
     }
 
     public static boolean removeProfile(Context context, String profileName) {
         if (profileName == null) {
             Log.w(LOGTAG, "Unable to remove profile: null profile name.");
             return false;
         }
@@ -316,25 +367,37 @@ public final class GeckoProfile {
                 delete(fileDelete);
             }
         }
 
         // Even if this is a dir, it should now be empty and delete should work
         return file.delete();
     }
 
-    private GeckoProfile(Context context, String profileName) throws NoMozillaDirectoryException {
+    private GeckoProfile(Context context, String profileName, File profileDir, BrowserDB.Factory dbFactory) throws NoMozillaDirectoryException {
         if (TextUtils.isEmpty(profileName)) {
             throw new IllegalArgumentException("Unable to create GeckoProfile for empty profile name.");
         }
 
         mApplicationContext = context.getApplicationContext();
         mName = profileName;
         mIsWebAppProfile = profileName.startsWith("webapp");
         mMozillaDir = GeckoProfileDirectories.getMozillaDirectory(context);
+
+        // This apes the behavior of setDir.
+        if (profileDir != null && profileDir.exists() && profileDir.isDirectory()) {
+            mProfileDir = profileDir;
+        }
+
+        // N.B., mProfileDir can be null at this point.
+        mDB = dbFactory.get(profileName, mProfileDir);
+    }
+
+    public BrowserDB getDB() {
+        return mDB;
     }
 
     // Warning, Changing the lock file state from outside apis will cause this to become out of sync
     public boolean locked() {
         if (mLocked != LockState.UNDEFINED) {
             return mLocked == LockState.LOCKED;
         }
 
--- a/mobile/android/base/GeckoView.java
+++ b/mobile/android/base/GeckoView.java
@@ -1,45 +1,42 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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;
 
-import org.mozilla.gecko.db.BrowserDB;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import org.json.JSONException;
+import org.json.JSONObject;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.mozglue.GeckoLoader;
 import org.mozilla.gecko.util.Clipboard;
-import org.mozilla.gecko.util.HardwareUtils;
+import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoEventListener;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSObject;
-
-import org.json.JSONException;
-import org.json.JSONObject;
+import org.mozilla.gecko.util.ThreadUtils;
 
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.res.TypedArray;
 import android.os.Bundle;
-import android.os.Handler;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.View;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
-
 public class GeckoView extends LayerView
     implements ContextGetter {
 
     private static final String DEFAULT_SHARED_PREFERENCES_FILE = "GeckoView";
     private static final String LOGTAG = "GeckoView";
 
     private ChromeDelegate mChromeDelegate;
     private ContentDelegate mContentDelegate;
@@ -147,17 +144,18 @@ public class GeckoView extends LayerView
             }
 
             Clipboard.init(context);
             HardwareUtils.init(context);
 
             // If you want to use GeckoNetworkManager, start it.
 
             GeckoLoader.loadMozGlue(context);
-            BrowserDB.setEnableContentProviders(false);
+
+            final GeckoProfile profile = GeckoProfile.get(context);
          }
 
         if (url != null) {
             GeckoThread.setUri(url);
             GeckoThread.setAction(Intent.ACTION_VIEW);
             GeckoAppShell.sendEventToGecko(GeckoEvent.createURILoadEvent(url));
         }
         GeckoAppShell.setContextGetter(this);
@@ -181,17 +179,16 @@ public class GeckoView extends LayerView
             "Accessibility:Ready",
             "GeckoView:Message");
 
         initializeView(EventDispatcher.getInstance());
 
         if (GeckoThread.checkAndSetLaunchState(GeckoThread.LaunchState.Launching, GeckoThread.LaunchState.Launched)) {
             // This is the first launch, so finish initialization and go.
             GeckoProfile profile = GeckoProfile.get(context).forceCreate();
-            BrowserDB.initialize(profile.getName());
 
             GeckoAppShell.sendEventToGecko(GeckoEvent.createObjectEvent(
                 GeckoEvent.ACTION_OBJECT_LAYER_CLIENT, getLayerClientObject()));
             GeckoThread.createAndStart();
         } else if(GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
             // If Gecko is already running, that means the Activity was
             // destroyed, so we need to re-attach Gecko to this GeckoView.
             connectToGecko();
--- a/mobile/android/base/GlobalHistory.java
+++ b/mobile/android/base/GlobalHistory.java
@@ -9,16 +9,18 @@ import java.lang.ref.SoftReference;
 import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.Queue;
 import java.util.Set;
 
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.util.ThreadUtils;
 
+import android.content.ContentResolver;
+import android.content.Context;
 import android.database.Cursor;
 import android.os.Handler;
 import android.os.SystemClock;
 import android.util.Log;
 
 class GlobalHistory {
     private static final String LOGTAG = "GeckoGlobalHistory";
 
@@ -33,107 +35,125 @@ class GlobalHistory {
     }
 
     // this is the delay between receiving a URI check request and processing it.
     // this allows batching together multiple requests and processing them together,
     // which is more efficient.
     private static final long BATCHING_DELAY_MS = 100;
 
     private final Handler mHandler;                     // a background thread on which we can process requests
-    private final Queue<String> mPendingUris;           // URIs that need to be checked
-    private SoftReference<Set<String>> mVisitedCache;   // cache of the visited URI list
-    private final Runnable mNotifierRunnable;           // off-thread runnable used to check URIs
-    private boolean mProcessing; // = false             // whether or not the runnable is queued/working
+
+    //  Note: These fields are accessed through the NotificationRunnable inner class.
+    final Queue<String> mPendingUris;           // URIs that need to be checked
+    SoftReference<Set<String>> mVisitedCache;   // cache of the visited URI list
+    boolean mProcessing; // = false             // whether or not the runnable is queued/working
+
+    private class NotifierRunnable implements Runnable {
+        private final ContentResolver mContentResolver;
+        private final BrowserDB mDB;
+
+        public NotifierRunnable(final Context context) {
+            mContentResolver = context.getContentResolver();
+            mDB = GeckoProfile.get(context).getDB();
+        }
+
+        @Override
+        public void run() {
+            Set<String> visitedSet = mVisitedCache.get();
+            if (visitedSet == null) {
+                // The cache was wiped. Repopulate it.
+                Log.w(LOGTAG, "Rebuilding visited link set...");
+                final long start = SystemClock.uptimeMillis();
+                final Cursor c = mDB.getAllVisitedHistory(mContentResolver);
+                if (c == null) {
+                    return;
+                }
+
+                try {
+                    visitedSet = new HashSet<String>();
+                    if (c.moveToFirst()) {
+                        do {
+                            visitedSet.add(c.getString(0));
+                        } while (c.moveToNext());
+                    }
+                    mVisitedCache = new SoftReference<Set<String>>(visitedSet);
+                    final long end = SystemClock.uptimeMillis();
+                    final long took = end - start;
+                    Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_BUILD_VISITED_LINK, (int) Math.min(took, Integer.MAX_VALUE));
+                } finally {
+                    c.close();
+                }
+            }
+
+            // This runs on the same handler thread as the checkUriVisited code,
+            // so no synchronization is needed.
+            while (true) {
+                final String uri = mPendingUris.poll();
+                if (uri == null) {
+                    break;
+                }
+
+                if (visitedSet.contains(uri)) {
+                    GeckoAppShell.notifyUriVisited(uri);
+                }
+            }
+
+            mProcessing = false;
+        }
+    };
 
     private GlobalHistory() {
         mHandler = ThreadUtils.getBackgroundHandler();
         mPendingUris = new LinkedList<String>();
         mVisitedCache = new SoftReference<Set<String>>(null);
-        mNotifierRunnable = new Runnable() {
-            @Override
-            public void run() {
-                Set<String> visitedSet = mVisitedCache.get();
-                if (visitedSet == null) {
-                    // The cache was wiped. Repopulate it.
-                    Log.w(LOGTAG, "Rebuilding visited link set...");
-                    final long start = SystemClock.uptimeMillis();
-                    final Cursor c = BrowserDB.getAllVisitedHistory(GeckoAppShell.getContext().getContentResolver());
-                    if (c == null) {
-                        return;
-                    }
-
-                    try {
-                        visitedSet = new HashSet<String>();
-                        if (c.moveToFirst()) {
-                            do {
-                                visitedSet.add(c.getString(0));
-                            } while (c.moveToNext());
-                        }
-                        mVisitedCache = new SoftReference<Set<String>>(visitedSet);
-                        final long end = SystemClock.uptimeMillis();
-                        final long took = end - start;
-                        Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_BUILD_VISITED_LINK, (int) Math.min(took, Integer.MAX_VALUE));
-                    } finally {
-                        c.close();
-                    }
-                }
-
-                // This runs on the same handler thread as the checkUriVisited code,
-                // so no synchronization is needed.
-                while (true) {
-                    final String uri = mPendingUris.poll();
-                    if (uri == null) {
-                        break;
-                    }
-
-                    if (visitedSet.contains(uri)) {
-                        GeckoAppShell.notifyUriVisited(uri);
-                    }
-                }
-                mProcessing = false;
-            }
-        };
     }
 
     public void addToGeckoOnly(String uri) {
         Set<String> visitedSet = mVisitedCache.get();
         if (visitedSet != null) {
             visitedSet.add(uri);
         }
         GeckoAppShell.notifyUriVisited(uri);
     }
 
-    public void add(String uri) {
+    public void add(final Context context, final BrowserDB db, String uri) {
+        ThreadUtils.assertOnBackgroundThread();
         final long start = SystemClock.uptimeMillis();
-        BrowserDB.updateVisitedHistory(GeckoAppShell.getContext().getContentResolver(), uri);
+
+        db.updateVisitedHistory(context.getContentResolver(), uri);
+
         final long end = SystemClock.uptimeMillis();
         final long took = end - start;
         Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_ADD, (int) Math.min(took, Integer.MAX_VALUE));
         addToGeckoOnly(uri);
     }
 
     @SuppressWarnings("static-method")
-    public void update(String uri, String title) {
+    public void update(final ContentResolver cr, final BrowserDB db, String uri, String title) {
+        ThreadUtils.assertOnBackgroundThread();
         final long start = SystemClock.uptimeMillis();
-        BrowserDB.updateHistoryTitle(GeckoAppShell.getContext().getContentResolver(), uri, title);
+
+        db.updateHistoryTitle(cr, uri, title);
+
         final long end = SystemClock.uptimeMillis();
         final long took = end - start;
         Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_UPDATE, (int) Math.min(took, Integer.MAX_VALUE));
     }
 
     public void checkUriVisited(final String uri) {
+        final NotifierRunnable runnable = new NotifierRunnable(GeckoAppShell.getContext());
         mHandler.post(new Runnable() {
             @Override
             public void run() {
                 // this runs on the same handler thread as the processing loop,
                 // so no synchronization needed
                 mPendingUris.add(uri);
                 if (mProcessing) {
                     // there's already a runnable queued up or working away, so
                     // no need to post another
                     return;
                 }
                 mProcessing = true;
-                mHandler.postDelayed(mNotifierRunnable, BATCHING_DELAY_MS);
+                mHandler.postDelayed(runnable, BATCHING_DELAY_MS);
             }
         });
     }
 }
--- a/mobile/android/base/MemoryMonitor.java
+++ b/mobile/android/base/MemoryMonitor.java
@@ -1,23 +1,24 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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;
 
 import org.mozilla.gecko.AppConstants.Versions;
+import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.BroadcastReceiver;
 import android.content.ComponentCallbacks2;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.util.Log;
 
 /**
   * This is a utility class to keep track of how much memory and disk-space pressure
   * the system is under. It receives input from GeckoActivity via the onLowMemory() and
@@ -210,32 +211,44 @@ class MemoryMonitor extends BroadcastRec
 
             // need to keep decrementing
             ThreadUtils.getBackgroundHandler().postDelayed(this, DECREMENT_DELAY);
         }
     }
 
     private static class StorageReducer implements Runnable {
         private final Context mContext;
+        private final BrowserDB mDB;
+
         public StorageReducer(final Context context) {
             this.mContext = context;
+            // Since this may be called while Fennec is in the background, we don't want to risk accidentally
+            // using the wrong context. If the profile we get is a guest profile, use the default profile instead.
+            GeckoProfile profile = GeckoProfile.get(mContext);
+            if (profile.inGuestMode()) {
+                // If it was the guest profile, switch to the default one.
+                profile = GeckoProfile.get(mContext, GeckoProfile.DEFAULT_PROFILE);
+            }
+
+            mDB = profile.getDB();
         }
 
         @Override
         public void run() {
             // this might get run right on startup, if so wait 10 seconds and try again
             if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
                 ThreadUtils.getBackgroundHandler().postDelayed(this, 10000);
                 return;
             }
 
             if (!MemoryMonitor.getInstance().isUnderStoragePressure()) {
                 // Pressure is off, so we can abort.
                 return;
             }
 
-            BrowserDB.expireHistory(mContext.getContentResolver(),
-                                    BrowserContract.ExpirePriority.AGGRESSIVE);
-            BrowserDB.removeThumbnails(mContext.getContentResolver());
+            final ContentResolver cr = mContext.getContentResolver();
+            mDB.expireHistory(cr, BrowserContract.ExpirePriority.AGGRESSIVE);
+            mDB.removeThumbnails(cr);
+
             // TODO: drop or shrink disk caches
         }
     }
 }
--- a/mobile/android/base/PrivateTab.java
+++ b/mobile/android/base/PrivateTab.java
@@ -2,26 +2,28 @@
  * 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;
 
 import android.content.Context;
 
+import org.mozilla.gecko.db.BrowserDB;
+
 public class PrivateTab extends Tab {
     public PrivateTab(Context context, int id, String url, boolean external, int parentId, String title) {
         super(context, id, url, external, parentId, title);
 
         // Init background to background_private to ensure flicker-free
         // private tab creation. Page loads will reset it to white as expected.
         final int bgColor = context.getResources().getColor(R.color.background_private);
         setBackgroundColor(bgColor);
     }
 
     @Override
-    protected void saveThumbnailToDB() {}
+    protected void saveThumbnailToDB(final BrowserDB db) {}
 
     @Override
     public boolean isPrivate() {
         return true;
     }
 }
--- a/mobile/android/base/ReadingListHelper.java
+++ b/mobile/android/base/ReadingListHelper.java
@@ -79,48 +79,50 @@ public final class ReadingListHelper imp
     /**
      * A page can be added to the ReadingList by long-tap of the page-action
      * icon, or by tapping the readinglist-add icon in the ReaderMode banner.
      */
     private void handleAddToList(final JSONObject message) {
         final ContentResolver cr = context.getContentResolver();
         final String url = message.optString("url");
 
+        final BrowserDB db = GeckoProfile.get(context).getDB();
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
-                if (BrowserDB.isReadingListItem(cr, url)) {
+                if (db.isReadingListItem(cr, url)) {
                     showToast(R.string.reading_list_duplicate, Toast.LENGTH_SHORT);
 
                 } else {
                     final ContentValues values = new ContentValues();
                     values.put(ReadingListItems.URL, url);
                     values.put(ReadingListItems.TITLE, message.optString("title"));
                     values.put(ReadingListItems.LENGTH, message.optInt("length"));
                     values.put(ReadingListItems.EXCERPT, message.optString("excerpt"));
                     values.put(ReadingListItems.CONTENT_STATUS, message.optInt("status"));
-                    BrowserDB.addReadingListItem(cr, values);
+                    db.addReadingListItem(cr, values);
 
                     showToast(R.string.reading_list_added, Toast.LENGTH_SHORT);
                 }
             }
         });
 
         GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:Added", url));
     }
 
     /**
      * Gecko (ReaderMode) requests the page favicon to append to the
      * document head for display.
      */
     private void handleReaderModeFaviconRequest(final String url) {
+        final BrowserDB db = GeckoProfile.get(context).getDB();
         (new UIAsyncTask.WithoutParams<String>(ThreadUtils.getBackgroundHandler()) {
             @Override
             public String doInBackground() {
-                return Favicons.getFaviconURLForPageURL(context, url);
+                return Favicons.getFaviconURLForPageURL(db, context.getContentResolver(), url);
             }
 
             @Override
             public void onPostExecute(String faviconUrl) {
                 JSONObject args = new JSONObject();
 
                 if (faviconUrl != null) {
                     try {
@@ -137,36 +139,37 @@ public final class ReadingListHelper imp
         }).execute();
     }
 
     /**
      * A page can be removed from the ReadingList by panel context menu,
      * or by tapping the readinglist-remove icon in the ReaderMode banner.
      */
     private void handleRemoveFromList(final String url) {
+        final BrowserDB db = GeckoProfile.get(context).getDB();
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
-                BrowserDB.removeReadingListItemWithURL(context.getContentResolver(), url);
+                db.removeReadingListItemWithURL(context.getContentResolver(), url);
                 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:Removed", url));
                 showToast(R.string.page_removed, Toast.LENGTH_SHORT);
             }
         });
     }
 
     /**
      * Gecko (ReaderMode) requests the page ReadingList status, to display
      * the proper ReaderMode banner icon (readinglist-add / readinglist-remove).
      */
     private void handleReadingListStatusRequest(final EventCallback callback, final String url) {
+        final BrowserDB db = GeckoProfile.get(context).getDB();
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
-                final int inReadingList =
-                    BrowserDB.isReadingListItem(context.getContentResolver(), url) ? 1 : 0;
+                final int inReadingList = db.isReadingListItem(context.getContentResolver(), url) ? 1 : 0;
 
                 final JSONObject json = new JSONObject();
                 try {
                     json.put("url", url);
                     json.put("inReadingList", inReadingList);
                 } catch (JSONException e) {
                     Log.e(LOGTAG, "JSON error - failed to return inReadingList status", e);
                 }
--- a/mobile/android/base/RemoteClientsDialogFragment.java
+++ b/mobile/android/base/RemoteClientsDialogFragment.java
@@ -3,17 +3,17 @@
  * 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;
 
 import java.util.ArrayList;
 import java.util.List;
 
-import org.mozilla.gecko.TabsAccessor.RemoteClient;
+import org.mozilla.gecko.db.RemoteClient;
 
 import android.app.AlertDialog;
 import android.app.AlertDialog.Builder;
 import android.app.Dialog;
 import android.content.DialogInterface;
 import android.os.Bundle;
 import android.support.v4.app.DialogFragment;
 import android.support.v4.app.Fragment;
--- a/mobile/android/base/RemoteTabsExpandableListAdapter.java
+++ b/mobile/android/base/RemoteTabsExpandableListAdapter.java
@@ -2,18 +2,18 @@
  * 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;
 
 import java.util.ArrayList;
 import java.util.List;
 
-import org.mozilla.gecko.TabsAccessor.RemoteClient;
-import org.mozilla.gecko.TabsAccessor.RemoteTab;
+import org.mozilla.gecko.db.RemoteClient;
+import org.mozilla.gecko.db.RemoteTab;
 import org.mozilla.gecko.home.TwoLinePageRow;
 
 import android.content.Context;
 import android.text.TextUtils;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.BaseExpandableListAdapter;
@@ -42,17 +42,17 @@ public class RemoteTabsExpandableListAda
      * @param groupLayoutId
      * @param childLayoutId
      * @param clients
      *            initial list of clients; can be null.
      */
     public RemoteTabsExpandableListAdapter(int groupLayoutId, int childLayoutId, List<RemoteClient> clients) {
         this.groupLayoutId = groupLayoutId;
         this.childLayoutId = childLayoutId;
-        this.clients = new ArrayList<TabsAccessor.RemoteClient>();
+        this.clients = new ArrayList<RemoteClient>();
         if (clients != null) {
             this.clients.addAll(clients);
         }
     }
 
     public void replaceClients(List<RemoteClient> clients) {
         this.clients.clear();
         if (clients != null) {
@@ -120,17 +120,21 @@ public class RemoteTabsExpandableListAda
 
         // Now update the UI.
         final TextView nameView = (TextView) view.findViewById(R.id.client);
         nameView.setText(client.name);
         nameView.setTextColor(context.getResources().getColor(textColorResId));
 
         final TextView lastModifiedView = (TextView) view.findViewById(R.id.last_synced);
         final long now = System.currentTimeMillis();
-        lastModifiedView.setText(TabsAccessor.getLastSyncedString(context, now, client.lastModified));
+
+        // It's OK to access the DB on the main thread here, as we're just
+        // getting a string.
+        final GeckoProfile profile = GeckoProfile.get(context);
+        lastModifiedView.setText(profile.getDB().getTabsAccessor().getLastSyncedString(context, now, client.lastModified));
 
         // These views exists only in some of our group views: they are present
         // for the home panel groups and not for the tabs panel groups.
         // Therefore, we must handle null.
         final ImageView deviceTypeView = (ImageView) view.findViewById(R.id.device_type);
         if (deviceTypeView != null) {
             deviceTypeView.setImageResource(deviceTypeResId);
         }
--- a/mobile/android/base/Tab.java
+++ b/mobile/android/base/Tab.java
@@ -35,16 +35,17 @@ import android.text.TextUtils;
 import android.util.Log;
 import android.view.View;
 
 public class Tab {
     private static final String LOGTAG = "GeckoTab";
 
     private static Pattern sColorPattern;
     private final int mId;
+    private final BrowserDB mDB;
     private long mLastUsed;
     private String mUrl;
     private String mBaseDomain;
     private String mUserRequested; // The original url requested. May be typed by the user or sent by an extneral app for example.
     private String mTitle;
     private Bitmap mFavicon;
     private String mFaviconUrl;
 
@@ -100,16 +101,17 @@ public class Tab {
         CERT_ERROR,  // Pages with certificate problems
         BLOCKED,     // Pages blocked for phishing or malware warnings
         NET_ERROR,   // All other types of error
         NONE         // Non error pages
     }
 
     public Tab(Context context, int id, String url, boolean external, int parentId, String title) {
         mAppContext = context.getApplicationContext();
+        mDB = GeckoProfile.get(context).getDB();
         mId = id;
         mUrl = url;
         mBaseDomain = "";
         mUserRequested = "";
         mExternal = external;
         mParentId = parentId;
         mTitle = title == null ? "" : title;
         mSiteIdentity = new SiteIdentity();
@@ -221,21 +223,21 @@ public class Tab {
     public void updateThumbnail(final Bitmap b, final ThumbnailHelper.CachePolicy cachePolicy) {
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
                 if (b != null) {
                     try {
                         mThumbnail = new BitmapDrawable(mAppContext.getResources(), b);
                         if (mState == Tab.STATE_SUCCESS && cachePolicy == ThumbnailHelper.CachePolicy.STORE) {
-                            saveThumbnailToDB();
+                            saveThumbnailToDB(mDB);
                         } else {
                             // If the page failed to load, or requested that we not cache info about it, clear any previous
                             // thumbnails we've stored.
-                            clearThumbnailFromDB();
+                            clearThumbnailFromDB(mDB);
                         }
                     } catch (OutOfMemoryError oom) {
                         Log.w(LOGTAG, "Unable to create/scale bitmap.", oom);
                         mThumbnail = null;
                     }
                 } else {
                     mThumbnail = null;
                 }
@@ -295,21 +297,23 @@ public class Tab {
     }
 
     public void setMetadata(JSONObject metadata) {
         if (metadata == null) {
             return;
         }
 
         final ContentResolver cr = mAppContext.getContentResolver();
-        final Map<String, Object> data = URLMetadata.fromJSON(metadata);
+        final URLMetadata urlMetadata = mDB.getURLMetadata();
+
+        final Map<String, Object> data = urlMetadata.fromJSON(metadata);
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
-                URLMetadata.save(cr, mUrl, data);
+                urlMetadata.save(cr, mUrl, data);
             }
         });
     }
 
     public ErrorType getErrorType() {
         return mErrorType;
     }
 
@@ -477,45 +481,45 @@ public class Tab {
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
                 final String url = getURL();
                 if (url == null) {
                     return;
                 }
 
-                mBookmark = BrowserDB.isBookmark(getContentResolver(), url);
+                mBookmark = mDB.isBookmark(getContentResolver(), url);
                 Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.MENU_UPDATED);
             }
         });
     }
 
     public void addBookmark() {
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
                 String url = getURL();
                 if (url == null)
                     return;
 
-                BrowserDB.addBookmark(getContentResolver(), mTitle, url);
+                mDB.addBookmark(getContentResolver(), mTitle, url);
                 Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.BOOKMARK_ADDED);
             }
         });
     }
 
     public void removeBookmark() {
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
                 String url = getURL();
                 if (url == null)
                     return;
 
-                BrowserDB.removeBookmarksWithURL(getContentResolver(), url);
+                mDB.removeBookmarksWithURL(getContentResolver(), url);
                 Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.BOOKMARK_REMOVED);
             }
         });
     }
 
     public void toggleReaderMode() {
         if (AboutPages.isAboutReader(mUrl)) {
             Tabs.getInstance().loadUrl(ReaderModeUtils.getUrlFromAboutReader(mUrl));
@@ -624,58 +628,59 @@ public class Tab {
     }
 
     void handleDocumentStop(boolean success) {
         setState(success ? STATE_SUCCESS : STATE_ERROR);
 
         final String oldURL = getURL();
         final Tab tab = this;
         tab.setLoadProgress(LOAD_PROGRESS_STOP);
+
         ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() {
             @Override
             public void run() {
                 // tab.getURL() may return null
                 if (!TextUtils.equals(oldURL, getURL()))
                     return;
 
-                ThumbnailHelper.getInstance().getAndProcessThumbnailFor(tab);
+                ThumbnailHelper.getInstance().getAndProcessThumbnailFor(tab, mDB);
             }
         }, 500);
     }
 
     void handleContentLoaded() {
         setLoadProgressIfLoading(LOAD_PROGRESS_LOADED);
     }
 
-    protected void saveThumbnailToDB() {
+    protected void saveThumbnailToDB(final BrowserDB db) {
         final BitmapDrawable thumbnail = mThumbnail;
         if (thumbnail == null) {
             return;
         }
 
         try {
             String url = getURL();
             if (url == null) {
                 return;
             }
 
-            BrowserDB.updateThumbnailForUrl(getContentResolver(), url, thumbnail);
+            db.updateThumbnailForUrl(getContentResolver(), url, thumbnail);
         } catch (Exception e) {
             // ignore
         }
     }
 
-    private void clearThumbnailFromDB() {
+    private void clearThumbnailFromDB(final BrowserDB db) {
         try {
             String url = getURL();
             if (url == null)
                 return;
 
             // Passing in a null thumbnail will delete the stored thumbnail for this url
-            BrowserDB.updateThumbnailForUrl(getContentResolver(), url, null);
+            db.updateThumbnailForUrl(getContentResolver(), url, null);
         } catch (Exception e) {
             // ignore
         }
     }
 
     public void addPluginView(View view) {
         mPluginViews.add(view);
     }
--- a/mobile/android/base/Tabs.java
+++ b/mobile/android/base/Tabs.java
@@ -65,26 +65,36 @@ public class Tabs implements GeckoEventL
 
     public static final int INVALID_TAB_ID = -1;
 
     private static final AtomicInteger sTabId = new AtomicInteger(0);
     private volatile boolean mInitialTabsAdded;
 
     private Context mAppContext;
     private ContentObserver mContentObserver;
+    private PersistTabsRunnable mPersistTabsRunnable;
 
-    private final Runnable mPersistTabsRunnable = new Runnable() {
+    private static class PersistTabsRunnable implements Runnable {
+        private final BrowserDB db;
+        private final Context context;
+        private final Iterable<Tab> tabs;
+
+        public PersistTabsRunnable(final Context context, Iterable<Tab> tabsInOrder) {
+            this.context = context;
+            this.db = GeckoProfile.get(context).getDB();
+            this.tabs = tabsInOrder;
+        }
+
         @Override
         public void run() {
             try {
-                final Context context = getAppContext();
                 boolean syncIsSetup = SyncAccounts.syncAccountsExist(context) ||
                                       FirefoxAccounts.firefoxAccountsExist(context);
                 if (syncIsSetup) {
-                    TabsAccessor.persistLocalTabs(getContentResolver(), getTabsInOrder());
+                    db.getTabsAccessor().persistLocalTabs(context.getContentResolver(), tabs);
                 }
             } catch (SecurityException se) {} // will fail without android.permission.GET_ACCOUNTS
         }
     };
 
     private Tabs() {
         EventDispatcher.getInstance().registerGeckoThreadListener(this,
             "Tab:Added",
@@ -128,17 +138,19 @@ public class Tabs implements GeckoEventL
                 persistAllTabs();
             }
         };
 
         // The listener will run on the background thread (see 2nd argument).
         mAccountManager.addOnAccountsUpdatedListener(mAccountListener, ThreadUtils.getBackgroundHandler(), false);
 
         if (mContentObserver != null) {
-            BrowserDB.registerBookmarkObserver(getContentResolver(), mContentObserver);
+            // It's safe to use the db here since we aren't doing any I/O.
+            final GeckoProfile profile = GeckoProfile.get(context);
+            profile.getDB().registerBookmarkObserver(getContentResolver(), mContentObserver);
         }
     }
 
     /**
      * Gets the tab count corresponding to the private state of the selected
      * tab.
      *
      * If the selected tab is a non-private tab, this will return the number of
@@ -175,17 +187,20 @@ public class Tabs implements GeckoEventL
             mContentObserver = new ContentObserver(null) {
                 @Override
                 public void onChange(boolean selfChange) {
                     for (Tab tab : mOrder) {
                         tab.updateBookmark();
                     }
                 }
             };
-            BrowserDB.registerBookmarkObserver(getContentResolver(), mContentObserver);
+
+            // It's safe to use the db here since we aren't doing any I/O.
+            final GeckoProfile profile = GeckoProfile.get(mAppContext);
+            profile.getDB().registerBookmarkObserver(getContentResolver(), mContentObserver);
         }
     }
 
     private Tab addTab(int id, String url, boolean external, int parentId, String title, boolean isPrivate, int tabIndex) {
         final Tab tab = isPrivate ? new PrivateTab(mAppContext, id, url, external, parentId, title) :
                                     new Tab(mAppContext, id, url, external, parentId, title);
         synchronized (this) {
             lazyRegisterBookmarkObserver();
@@ -526,21 +541,22 @@ public class Tabs implements GeckoEventL
 
         } catch (Exception e) {
             Log.w(LOGTAG, "handleMessage threw for " + event, e);
         }
     }
 
     public void refreshThumbnails() {
         final ThumbnailHelper helper = ThumbnailHelper.getInstance();
+        final BrowserDB db = GeckoProfile.get(mAppContext).getDB();
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
                 for (final Tab tab : mOrder) {
-                    helper.getAndProcessThumbnailFor(tab);
+                    helper.getAndProcessThumbnailFor(tab, db);
                 }
             }
         });
     }
 
     public interface OnTabsChangedListener {
         public void onTabChanged(Tab tab, TabEvents msg, Object data);
     }
@@ -627,27 +643,37 @@ public class Tabs implements GeckoEventL
                 break;
             default:
                 break;
         }
     }
 
     // This method persists the current ordered list of tabs in our tabs content provider.
     public void persistAllTabs() {
+        // If there is already a mPersistTabsRunnable in progress, the backgroundThread will hold onto
+        // it and ensure these still happen in the correct order.
+        mPersistTabsRunnable = new PersistTabsRunnable(mAppContext, getTabsInOrder());
         ThreadUtils.postToBackgroundThread(mPersistTabsRunnable);
     }
 
     /**
      * Queues a request to persist tabs after PERSIST_TABS_AFTER_MILLISECONDS
      * milliseconds have elapsed. If any existing requests are already queued then
      * those requests are removed.
      */
     private void queuePersistAllTabs() {
-        Handler backgroundHandler = ThreadUtils.getBackgroundHandler();
-        backgroundHandler.removeCallbacks(mPersistTabsRunnable);
+        final Handler backgroundHandler = ThreadUtils.getBackgroundHandler();
+
+        // Note: Its safe to modify the runnable here because all of the callers are on the same thread.
+        if (mPersistTabsRunnable != null) {
+            backgroundHandler.removeCallbacks(mPersistTabsRunnable);
+            mPersistTabsRunnable = null;
+        }
+
+        mPersistTabsRunnable = new PersistTabsRunnable(mAppContext, getTabsInOrder());
         backgroundHandler.postDelayed(mPersistTabsRunnable, PERSIST_TABS_AFTER_MILLISECONDS);
     }
 
     /**
      * Looks for an open tab with the given URL.
      * @param url       the URL of the tab we're looking for
      *
      * @return first Tab with the given URL, or null if there is no such tab.
--- a/mobile/android/base/ThumbnailHelper.java
+++ b/mobile/android/base/ThumbnailHelper.java
@@ -2,17 +2,16 @@
  * 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;
 
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.gfx.BitmapUtils;
-import org.mozilla.gecko.gfx.IntSize;
 import org.mozilla.gecko.mozglue.DirectBufferAllocator;
 import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI;
 
 import android.graphics.Bitmap;
 import android.util.Log;
 import android.content.res.Resources;
 
 import java.nio.ByteBuffer;
@@ -64,26 +63,31 @@ public final class ThumbnailHelper {
         mPendingThumbnails = new LinkedList<Tab>();
         try {
             mPendingWidth = new AtomicInteger((int)GeckoAppShell.getContext().getResources().getDimension(R.dimen.tab_thumbnail_width));
         } catch (Resources.NotFoundException nfe) { mPendingWidth = new AtomicInteger(0); }
         mWidth = -1;
         mHeight = -1;
     }
 
-    public void getAndProcessThumbnailFor(Tab tab) {
+    public void getAndProcessThumbnailFor(Tab tab, final BrowserDB db) {
         if (AboutPages.isAboutHome(tab.getURL())) {
             tab.updateThumbnail(null, CachePolicy.NO_STORE);
             return;
         }
 
+        // If we don't have a database, there's nothing left to do.
+        if (db == null) {
+            return;
+        }
+
         if (tab.getState() == Tab.STATE_DELAYED) {
             String url = tab.getURL();
             if (url != null) {
-                byte[] thumbnail = BrowserDB.getThumbnailForUrl(GeckoAppShell.getContext().getContentResolver(), url);
+                byte[] thumbnail = db.getThumbnailForUrl(GeckoAppShell.getContext().getContentResolver(), url);
                 if (thumbnail != null) {
                     // Since this thumbnail is from the database, its ok to store it
                     setTabThumbnail(tab, null, thumbnail, CachePolicy.STORE);
                 }
             }
             return;
         }
 
--- a/mobile/android/base/db/BrowserDB.java
+++ b/mobile/android/base/db/BrowserDB.java
@@ -1,277 +1,186 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
+/* 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 java.util.ArrayList;
+import java.io.File;
+import java.util.Collection;
 import java.util.EnumSet;
 import java.util.List;
 
+import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.db.BrowserContract.ExpirePriority;
-import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
-import org.mozilla.gecko.mozglue.RobocopTarget;
-import org.mozilla.gecko.util.StringUtils;
 
+import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.ContentObserver;
 import android.database.Cursor;
-import android.graphics.Color;
 import android.graphics.drawable.BitmapDrawable;
 
 /**
- * A utility wrapper for accessing a static {@link LocalBrowserDB},
- * manually initialized with a particular profile, and similarly
- * managing a static instance of {@link SuggestedSites}.
+ * Interface for interactions with all databases. If you want an instance
+ * that implements this, you should go through GeckoProfile. E.g.,
+ * <code>GeckoProfile.get(context).getDB()</code>.
  *
- * Be careful using this class: if you're not BrowserApp, you probably
- * want to manually instantiate and use LocalBrowserDB itself.
- *
- * Also manages some flags.
+ * GeckoProfile itself will construct an appropriate subclass using
+ * a factory that the containing application can set with
+ * {@link GeckoProfile#setBrowserDBFactory(BrowserDB.Factory)}.
  */
-public class BrowserDB {
+public interface BrowserDB {
+    public interface Factory {
+        public BrowserDB get(String profileName, File profileDir);
+    }
+
     public static enum FilterFlags {
         EXCLUDE_PINNED_SITES
     }
 
-    private static volatile LocalBrowserDB sDb;
-    private static volatile SuggestedSites sSuggestedSites;
-    private static volatile boolean sAreContentProvidersEnabled = true;
-
-    public static void initialize(String profile) {
-        sDb = new LocalBrowserDB(profile);
-    }
+    public abstract Searches getSearches();
+    public abstract TabsAccessor getTabsAccessor();
+    public abstract URLMetadata getURLMetadata();
 
-    public static int addDefaultBookmarks(Context context, ContentResolver cr, final int offset) {
-        return sDb.addDefaultBookmarks(context, cr, offset);
-    }
-
-    public static int addDistributionBookmarks(ContentResolver cr, Distribution distribution, int offset) {
-        return sDb.addDistributionBookmarks(cr, distribution, offset);
-    }
+    /**
+     * Add default bookmarks to the database.
+     * Takes an offset; returns a new offset.
+     */
+    public abstract int addDefaultBookmarks(Context context, ContentResolver cr, int offset);
 
-    public static void setSuggestedSites(SuggestedSites suggestedSites) {
-        sSuggestedSites = suggestedSites;
-    }
-
-    public static boolean hideSuggestedSite(String url) {
-        return sSuggestedSites.hideSite(url);
-    }
+    /**
+     * Add bookmarks from the provided distribution.
+     * Takes an offset; returns a new offset.
+     */
+    public abstract int addDistributionBookmarks(ContentResolver cr, Distribution distribution, int offset);
 
-    public static void invalidateCachedState() {
-        sDb.invalidateCachedState();
-    }
+    /**
+     * Invalidate cached data.
+     */
+    public abstract void invalidate();
 
-    @RobocopTarget
-    public static Cursor filter(ContentResolver cr, CharSequence constraint, int limit,
-                                EnumSet<FilterFlags> flags) {
-        return sDb.filter(cr, constraint, limit, flags);
-    }
-
-    private static void appendUrlsFromCursor(List<String> urls, Cursor c) {
-        c.moveToPosition(-1);
-        while (c.moveToNext()) {
-            String url = c.getString(c.getColumnIndex(History.URL));
+    public abstract int getCount(ContentResolver cr, String database);
 
-            // Do a simpler check before decoding to avoid parsing
-            // all URLs unnecessarily.
-            if (StringUtils.isUserEnteredUrl(url)) {
-                url = StringUtils.decodeUserEnteredUrl(url);
-            }
-
-            urls.add(url);
-        };
-    }
-
-    public static Cursor getTopSites(ContentResolver cr, int minLimit, int maxLimit) {
-        // Note this is not a single query anymore, but actually returns a mixture
-        // of two queries, one for topSites and one for pinned sites.
-        Cursor pinnedSites = sDb.getPinnedSites(cr, minLimit);
-
-        int pinnedCount = pinnedSites.getCount();
-        Cursor topSites = sDb.getTopSites(cr, maxLimit - pinnedCount);
-        int topCount = topSites.getCount();
+    /**
+     * @return a cursor representing the contents of the DB filtered according to the arguments.
+     * Can return <code>null</code>. <code>CursorLoader</code> will handle this correctly.
+     */
+    public abstract Cursor filter(ContentResolver cr, CharSequence constraint,
+                                  int limit, EnumSet<BrowserDB.FilterFlags> flags);
 
-        Cursor suggestedSites = null;
-        if (sSuggestedSites != null) {
-            final int count = minLimit - pinnedCount - topCount;
-            if (count > 0) {
-                final List<String> excludeUrls = new ArrayList<String>(pinnedCount + topCount);
-                appendUrlsFromCursor(excludeUrls, pinnedSites);
-                appendUrlsFromCursor(excludeUrls, topSites);
+    /**
+     * @return a cursor over top sites (high-ranking bookmarks and history).
+     * Can return <code>null</code>.
+     */
+    public abstract Cursor getTopSites(ContentResolver cr, int limit);
 
-                suggestedSites = sSuggestedSites.get(count, excludeUrls);
-            }
-        }
+    /**
+     * @return a cursor over top sites (high-ranking bookmarks and history).
+     * Can return <code>null</code>.
+     * Returns no more than <code>maxLimit</code> results.
+     */
+    public abstract Cursor getTopSites(ContentResolver cr, int minLimit, int maxLimit);
 
-        return new TopSitesCursorWrapper(pinnedSites, topSites, suggestedSites, minLimit);
-    }
+    public abstract void updateVisitedHistory(ContentResolver cr, String uri);
 
-    public static void updateVisitedHistory(ContentResolver cr, String uri) {
-        if (sAreContentProvidersEnabled) {
-            sDb.updateVisitedHistory(cr, uri);
-        }
-    }
+    public abstract void updateHistoryTitle(ContentResolver cr, String uri, String title);
 
-    public static void updateHistoryTitle(ContentResolver cr, String uri, String title) {
-        if (sAreContentProvidersEnabled) {
-            sDb.updateHistoryTitle(cr, uri, title);
-        }
-    }
-
-    @RobocopTarget
-    public static Cursor getAllVisitedHistory(ContentResolver cr) {
-        return (sAreContentProvidersEnabled ? sDb.getAllVisitedHistory(cr) : null);
-    }
-
-    public static Cursor getRecentHistory(ContentResolver cr, int limit) {
-        return sDb.getRecentHistory(cr, limit);
-    }
+    /**
+     * Can return <code>null</code>.
+     */
+    public abstract Cursor getAllVisitedHistory(ContentResolver cr);
 
-    public static void expireHistory(ContentResolver cr, ExpirePriority priority) {
-        if (sDb == null) {
-            return;
-        }
+    /**
+     * Can return <code>null</code>.
+     */
+    public abstract Cursor getRecentHistory(ContentResolver cr, int limit);
 
-        sDb.expireHistory(cr, priority == null ? ExpirePriority.NORMAL : priority);
-    }
+    public abstract void expireHistory(ContentResolver cr, ExpirePriority priority);
 
-    @RobocopTarget
-    public static void removeHistoryEntry(ContentResolver cr, String url) {
-        sDb.removeHistoryEntry(cr, url);
-    }
+    public abstract void removeHistoryEntry(ContentResolver cr, String url);
 
-    @RobocopTarget
-    public static void clearHistory(ContentResolver cr) {
-        sDb.clearHistory(cr);
-    }
+    public abstract void clearHistory(ContentResolver cr);
 
-    @RobocopTarget
-    public static Cursor getBookmarksInFolder(ContentResolver cr, long folderId) {
-        return sDb.getBookmarksInFolder(cr, folderId);
-    }
+
+    public abstract String getUrlForKeyword(ContentResolver cr, String keyword);
 
-    @RobocopTarget
-    public static Cursor getReadingList(ContentResolver cr) {
-        return sDb.getReadingList(cr);
-    }
-
-    public static String getUrlForKeyword(ContentResolver cr, String keyword) {
-        return sDb.getUrlForKeyword(cr, keyword);
-    }
+    public abstract boolean isBookmark(ContentResolver cr, String uri);
+    public abstract void addBookmark(ContentResolver cr, String title, String uri);
+    public abstract Cursor getBookmarkForUrl(ContentResolver cr, String url);
+    public abstract void removeBookmarksWithURL(ContentResolver cr, String uri);
+    public abstract void registerBookmarkObserver(ContentResolver cr, ContentObserver observer);
+    public abstract void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword);
 
-    @RobocopTarget
-    public static boolean isBookmark(ContentResolver cr, String uri) {
-        return (sAreContentProvidersEnabled && sDb.isBookmark(cr, uri));
-    }
-
-    public static boolean isReadingListItem(ContentResolver cr, String uri) {
-        return (sAreContentProvidersEnabled && sDb.isReadingListItem(cr, uri));
-    }
-
-    public static void addBookmark(ContentResolver cr, String title, String uri) {
-        sDb.addBookmark(cr, title, uri);
-    }
+    /**
+     * Can return <code>null</code>.
+     */
+    public abstract Cursor getBookmarksInFolder(ContentResolver cr, long folderId);
 
-    @RobocopTarget
-    public static void removeBookmarksWithURL(ContentResolver cr, String uri) {
-        sDb.removeBookmarksWithURL(cr, uri);
-    }
+    /**
+     * Can return <code>null</code>.
+     */
+    public abstract Cursor getReadingList(ContentResolver cr);
+    public abstract boolean isReadingListItem(ContentResolver cr, String uri);
+    public abstract void addReadingListItem(ContentResolver cr, ContentValues values);
+    public abstract void removeReadingListItemWithURL(ContentResolver cr, String uri);
 
-    @RobocopTarget
-    public static void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword) {
-        sDb.updateBookmark(cr, id, uri, title, keyword);
-    }
 
-    public static void addReadingListItem(ContentResolver cr, ContentValues values) {
-        sDb.addReadingListItem(cr, values);
-    }
-
-    public static void removeReadingListItemWithURL(ContentResolver cr, String uri) {
-        sDb.removeReadingListItemWithURL(cr, uri);
-    }
-
-    public static LoadFaviconResult getFaviconForFaviconUrl(ContentResolver cr, String faviconURL) {
-        return sDb.getFaviconForUrl(cr, faviconURL);
-    }
+    /**
+     * Get the favicon from the database, if any, associated with the given favicon URL. (That is,
+     * the URL of the actual favicon image, not the URL of the page with which the favicon is associated.)
+     * @param cr The ContentResolver to use.
+     * @param faviconURL The URL of the favicon to fetch from the database.
+     * @return The decoded Bitmap from the database, if any. null if none is stored.
+     */
+    public abstract LoadFaviconResult getFaviconForUrl(ContentResolver cr, String faviconURL);
 
     /**
      * Try to find a usable favicon URL in the history or bookmarks table.
      */
-    public static String getFaviconURLFromPageURL(ContentResolver cr, String url) {
-        return sDb.getFaviconURLFromPageURL(cr, url);
-    }
-
-    public static void updateFaviconForUrl(ContentResolver cr, String pageUri, byte[] encodedFavicon, String faviconUri) {
-        sDb.updateFaviconForUrl(cr, pageUri, encodedFavicon, faviconUri);
-    }
+    public abstract String getFaviconURLFromPageURL(ContentResolver cr, String uri);
 
-    public static void updateThumbnailForUrl(ContentResolver cr, String uri, BitmapDrawable thumbnail) {
-        sDb.updateThumbnailForUrl(cr, uri, thumbnail);
-    }
+    public abstract void updateFaviconForUrl(ContentResolver cr, String pageUri, byte[] encodedFavicon, String faviconUri);
 
-    @RobocopTarget
-    public static byte[] getThumbnailForUrl(ContentResolver cr, String uri) {
-        return sDb.getThumbnailForUrl(cr, uri);
-    }
+    public abstract byte[] getThumbnailForUrl(ContentResolver cr, String uri);
+    public abstract void updateThumbnailForUrl(ContentResolver cr, String uri, BitmapDrawable thumbnail);
 
-    public static Cursor getThumbnailsForUrls(ContentResolver cr, List<String> urls) {
-        return sDb.getThumbnailsForUrls(cr, urls);
-    }
+    /**
+     * Query for non-null thumbnails matching the provided <code>urls</code>.
+     * The returned cursor will have no more than, but possibly fewer than,
+     * the requested number of thumbnails.
+     *
+     * Returns null if the provided list of URLs is empty or null.
+     */
+    public abstract Cursor getThumbnailsForUrls(ContentResolver cr,
+            List<String> urls);
 
-    @RobocopTarget
-    public static void removeThumbnails(ContentResolver cr) {
-        sDb.removeThumbnails(cr);
-    }
-
-    public static void registerBookmarkObserver(ContentResolver cr, ContentObserver observer) {
-        sDb.registerBookmarkObserver(cr, observer);
-    }
-
-    public static int getCount(ContentResolver cr, String database) {
-        return sDb.getCount(cr, database);
-    }
+    public abstract void removeThumbnails(ContentResolver cr);
 
-    public static void pinSite(ContentResolver cr, String url, String title, int position) {
-        sDb.pinSite(cr, url, title, position);
-    }
-
-    public static void unpinSite(ContentResolver cr, int position) {
-        sDb.unpinSite(cr, position);
-    }
+    // Utility function for updating existing history using batch operations
+    public abstract void updateHistoryInBatch(ContentResolver cr,
+            Collection<ContentProviderOperation> operations, String url,
+            String title, long date, int visits);
 
-    @RobocopTarget
-    public static Cursor getBookmarkForUrl(ContentResolver cr, String url) {
-        return sDb.getBookmarkForUrl(cr, url);
-    }
-
-    public static void setEnableContentProviders(boolean enableContentProviders) {
-        sAreContentProvidersEnabled = enableContentProviders;
-    }
+    public abstract void updateBookmarkInBatch(ContentResolver cr,
+            Collection<ContentProviderOperation> operations, String url,
+            String title, String guid, long parent, long added, long modified,
+            long position, String keyword, int type);
 
-    public static boolean hasSuggestedImageUrl(String url) {
-        return sSuggestedSites.contains(url);
-    }
+    public abstract void updateFaviconInBatch(ContentResolver cr,
+            Collection<ContentProviderOperation> operations, String url,
+            String faviconUrl, String faviconGuid, byte[] data);
 
-    public static String getSuggestedImageUrlForUrl(String url) {
-        return sSuggestedSites.getImageUrlForUrl(url);
-    }
 
-    public static int getSuggestedBackgroundColorForUrl(String url) {
-        final String bgColor = sSuggestedSites.getBackgroundColorForUrl(url);
-        if (bgColor != null) {
-            return Color.parseColor(bgColor);
-        }
+    public abstract Cursor getPinnedSites(ContentResolver cr, int limit);
+    public abstract void pinSite(ContentResolver cr, String url, String title, int position);
+    public abstract void unpinSite(ContentResolver cr, int position);
 
-        return 0;
-    }
-
-    public static int getTrackingIdForUrl(String url) {
-        return sSuggestedSites.getTrackingIdForUrl(url);
-    }
+    public abstract boolean hideSuggestedSite(String url);
+    public abstract void setSuggestedSites(SuggestedSites suggestedSites);
+    public abstract boolean hasSuggestedImageUrl(String url);
+    public abstract String getSuggestedImageUrlForUrl(String url);
+    public abstract int getSuggestedBackgroundColorForUrl(String url);
+    public abstract int getTrackingIdForUrl(String url);
 }
--- a/mobile/android/base/db/BrowserProvider.java
+++ b/mobile/android/base/db/BrowserProvider.java
@@ -110,16 +110,17 @@ public class BrowserProvider extends Sha
     static final Map<String, String> SCHEMA_PROJECTION_MAP;
     static final Map<String, String> SEARCH_SUGGEST_PROJECTION_MAP;
     static final Map<String, String> FAVICONS_PROJECTION_MAP;
     static final Map<String, String> THUMBNAILS_PROJECTION_MAP;
     static final Table[] sTables;
 
     static {
         sTables = new Table[] {
+            // See awful shortcut assumption hack in getURLMetadataTable.
             new URLMetadataTable()
         };
         // We will reuse this.
         HashMap<String, String> map;
 
         // Bookmarks
         URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks", BOOKMARKS);
         URI_MATCHER.addURI(BrowserContract.AUTHORITY, "bookmarks/#", BOOKMARKS_ID);
@@ -229,16 +230,22 @@ public class BrowserProvider extends Sha
 
         for (Table table : sTables) {
             for (Table.ContentProviderInfo type : table.getContentProviderInfo()) {
                 URI_MATCHER.addURI(BrowserContract.AUTHORITY, type.name, type.id);
             }
         }
     }
 
+    // Convenience accessor.
+    // Assumes structure of sTables!
+    private URLMetadataTable getURLMetadataTable() {
+        return (URLMetadataTable) sTables[0];
+    }
+
     private static boolean hasFaviconsInProjection(String[] projection) {
         if (projection == null) return true;
         for (int i = 0; i < projection.length; ++i) {
             if (projection[i].equals(FaviconColumns.FAVICON) ||
                 projection[i].equals(FaviconColumns.FAVICON_URL))
                 return true;
         }
 
@@ -1355,18 +1362,17 @@ public class BrowserProvider extends Sha
                 + " AND " + History.URL + " IS NOT NULL"
                 + " UNION ALL SELECT " + Bookmarks.URL
                 + " FROM " + TABLE_BOOKMARKS
                 + " WHERE " + Bookmarks.IS_DELETED + " = 0"
                 + " AND " + Bookmarks.URL + " IS NOT NULL)";
 
         return deleteFavicons(uri, faviconSelection, null) +
                deleteThumbnails(uri, thumbnailSelection, null) +
-               URLMetadata.deleteUnused(getContext().getContentResolver(),
-                                        uri.getQueryParameter(BrowserContract.PARAM_PROFILE));
+               getURLMetadataTable().deleteUnused(getWritableDatabase(uri));
     }
 
     @Override
     public ContentProviderResult[] applyBatch (ArrayList<ContentProviderOperation> operations)
         throws OperationApplicationException {
         final int numOperations = operations.size();
         final ContentProviderResult[] results = new ContentProviderResult[numOperations];
 
--- a/mobile/android/base/db/DBUtils.java
+++ b/mobile/android/base/db/DBUtils.java
@@ -1,20 +1,22 @@
 /* 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.database.sqlite.SQLiteDatabase;
 import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoProfile;
 
 import android.content.ContentValues;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
 import android.text.TextUtils;
 import android.util.Log;
 import org.mozilla.gecko.Telemetry;
 
 public class DBUtils {
     private static final String LOGTAG = "GeckoDBUtils";
 
     public static final String qualifyColumn(String table, String column) {
@@ -155,9 +157,20 @@ public class DBUtils {
             builder.append(cursor.getLong(0));
             if (i++ < commaLimit) {
                 builder.append(", ");
             }
         }
         builder.append(")");
         return builder.toString();
     }
+
+    public static Uri appendProfile(final String profile, final Uri uri) {
+        return uri.buildUpon().appendQueryParameter(BrowserContract.PARAM_PROFILE, profile).build();
+    }
+
+    public static Uri appendProfileWithDefault(final String profile, final Uri uri) {
+        if (TextUtils.isEmpty(profile)) {
+            return appendProfile(GeckoProfile.DEFAULT_PROFILE, uri);
+        }
+        return appendProfile(profile, uri);
+    }
 }
--- a/mobile/android/base/db/LocalBrowserDB.java
+++ b/mobile/android/base/db/LocalBrowserDB.java
@@ -29,44 +29,45 @@ import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserContract.ExpirePriority;
 import org.mozilla.gecko.db.BrowserContract.Favicons;
 import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
 import org.mozilla.gecko.db.BrowserContract.SyncColumns;
 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
-import org.mozilla.gecko.db.BrowserDB.FilterFlags;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
 import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.mozglue.RobocopTarget;
 import org.mozilla.gecko.sync.Utils;
 import org.mozilla.gecko.util.GeckoJarReader;
+import org.mozilla.gecko.util.StringUtils;
 
 import android.content.ContentProviderOperation;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.ContentObserver;
 import android.database.Cursor;
 import android.database.CursorWrapper;
 import android.graphics.Bitmap;
+import android.graphics.Color;
 import android.graphics.drawable.BitmapDrawable;
 import android.net.Uri;
 import android.provider.Browser;
 import android.text.TextUtils;
 import android.util.Log;
 import org.mozilla.gecko.util.IOUtils;
 
 import static org.mozilla.gecko.util.IOUtils.ConsumedInputStream;
 import static org.mozilla.gecko.favicons.LoadFaviconTask.DEFAULT_FAVICON_BUFFER_SIZE;
 
-public class LocalBrowserDB {
+public class LocalBrowserDB implements BrowserDB {
     // Calculate these once, at initialization. isLoggable is too expensive to
     // have in-line in each log call.
     private static final String LOGTAG = "GeckoLocalBrowserDB";
 
     // Sentinel value used to indicate a failure to locate an ID for a default favicon.
     private static final int FAVICON_ID_NOT_FOUND = Integer.MIN_VALUE;
 
     // Constant used to indicate that no folder was found for particular GUID.
@@ -82,50 +83,77 @@ public class LocalBrowserDB {
     private final String mProfile;
 
     // Map of folder GUIDs to IDs. Used for caching.
     private final HashMap<String, Long> mFolderIdMap;
 
     // Use wrapped Boolean so that we can have a null state
     private volatile Boolean mDesktopBookmarksExist;
 
+    private volatile SuggestedSites mSuggestedSites;
+
     private final Uri mBookmarksUriWithProfile;
     private final Uri mParentsUriWithProfile;
     private final Uri mHistoryUriWithProfile;
     private final Uri mHistoryExpireUriWithProfile;
     private final Uri mCombinedUriWithProfile;
     private final Uri mUpdateHistoryUriWithProfile;
     private final Uri mFaviconsUriWithProfile;
     private final Uri mThumbnailsUriWithProfile;
     private final Uri mReadingListUriWithProfile;
 
+    private LocalSearches searches;
+    private LocalTabsAccessor tabsAccessor;
+    private LocalURLMetadata urlMetadata;
+
     private static final String[] DEFAULT_BOOKMARK_COLUMNS =
             new String[] { Bookmarks._ID,
                            Bookmarks.GUID,
                            Bookmarks.URL,
                            Bookmarks.TITLE,
                            Bookmarks.TYPE,
                            Bookmarks.PARENT };
 
     public LocalBrowserDB(String profile) {
         mProfile = profile;
         mFolderIdMap = new HashMap<String, Long>();
 
-        mBookmarksUriWithProfile = appendProfile(Bookmarks.CONTENT_URI);
-        mParentsUriWithProfile = appendProfile(Bookmarks.PARENTS_CONTENT_URI);
-        mHistoryUriWithProfile = appendProfile(History.CONTENT_URI);
-        mHistoryExpireUriWithProfile = appendProfile(History.CONTENT_OLD_URI);
-        mCombinedUriWithProfile = appendProfile(Combined.CONTENT_URI);
-        mFaviconsUriWithProfile = appendProfile(Favicons.CONTENT_URI);
-        mThumbnailsUriWithProfile = appendProfile(Thumbnails.CONTENT_URI);
-        mReadingListUriWithProfile = appendProfile(ReadingListItems.CONTENT_URI);
+        mBookmarksUriWithProfile = DBUtils.appendProfile(profile, Bookmarks.CONTENT_URI);
+        mParentsUriWithProfile = DBUtils.appendProfile(profile, Bookmarks.PARENTS_CONTENT_URI);
+        mHistoryUriWithProfile = DBUtils.appendProfile(profile, History.CONTENT_URI);
+        mHistoryExpireUriWithProfile = DBUtils.appendProfile(profile, History.CONTENT_OLD_URI);
+        mCombinedUriWithProfile = DBUtils.appendProfile(profile, Combined.CONTENT_URI);
+        mFaviconsUriWithProfile = DBUtils.appendProfile(profile, Favicons.CONTENT_URI);
+        mThumbnailsUriWithProfile = DBUtils.appendProfile(profile, Thumbnails.CONTENT_URI);
+        mReadingListUriWithProfile = DBUtils.appendProfile(profile, ReadingListItems.CONTENT_URI);
+
+        mUpdateHistoryUriWithProfile =
+                mHistoryUriWithProfile.buildUpon()
+                                      .appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true")
+                                      .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
+                                      .build();
 
-        mUpdateHistoryUriWithProfile = mHistoryUriWithProfile.buildUpon().
-            appendQueryParameter(BrowserContract.PARAM_INCREMENT_VISITS, "true").
-            appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
+        searches = new LocalSearches(mProfile);
+        tabsAccessor = new LocalTabsAccessor(mProfile);
+        urlMetadata = new LocalURLMetadata(mProfile);
+    }
+
+    @Override
+    public Searches getSearches() {
+        return searches;
+    }
+
+    @Override
+    public TabsAccessor getTabsAccessor() {
+        return tabsAccessor;
+    }
+
+    @Override
+    public URLMetadata getURLMetadata() {
+        return urlMetadata;
     }
 
     /**
      * Not thread safe. A helper to allocate new IDs for arbitrary strings.
      */
     private static class NameCounter {
         private final HashMap<String, Integer> names = new HashMap<String, Integer>();
         private int counter;
@@ -152,16 +180,17 @@ public class LocalBrowserDB {
             return names.containsKey(name);
         }
     }
 
     /**
      * Add default bookmarks to the database.
      * Takes an offset; returns a new offset.
      */
+    @Override
     public int addDefaultBookmarks(Context context, ContentResolver cr, final int offset) {
         final long folderID = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID);
         if (folderID == FOLDER_NOT_FOUND) {
             Log.e(LOGTAG, "No mobile folder: cannot add default bookmarks.");
             return offset;
         }
 
         // Use reflection to walk the set of bookmark defaults.
@@ -249,16 +278,17 @@ public class LocalBrowserDB {
 
         return offset;
     }
 
     /**
      * Add bookmarks from the provided distribution.
      * Takes an offset; returns a new offset.
      */
+    @Override
     public int addDistributionBookmarks(ContentResolver cr, Distribution distribution, int offset) {
         if (!distribution.exists()) {
             Log.d(LOGTAG, "No distribution from which to add bookmarks.");
             return offset;
         }
 
         final JSONArray bookmarks = distribution.getBookmarks();
         if (bookmarks == null) {
@@ -460,67 +490,57 @@ public class LocalBrowserDB {
             return null;
         }
 
         InputStream iStream = context.getResources().openRawResource(faviconId);
         return IOUtils.readFully(iStream, DEFAULT_FAVICON_BUFFER_SIZE);
     }
 
     // Invalidate cached data
-    public void invalidateCachedState() {
+    @Override
+    public void invalidate() {
         mDesktopBookmarksExist = null;
     }
 
     private Uri bookmarksUriWithLimit(int limit) {
-        return mBookmarksUriWithProfile.buildUpon().appendQueryParameter(BrowserContract.PARAM_LIMIT,
-                                                                         String.valueOf(limit)).build();
+        return mBookmarksUriWithProfile.buildUpon()
+                                       .appendQueryParameter(BrowserContract.PARAM_LIMIT,
+                                                             String.valueOf(limit))
+                                       .build();
     }
 
     private Uri combinedUriWithLimit(int limit) {
-        return mCombinedUriWithProfile.buildUpon().appendQueryParameter(BrowserContract.PARAM_LIMIT,
-                String.valueOf(limit)).build();
-    }
-
-    private Uri appendProfile(Uri uri) {
-        return uri.buildUpon().appendQueryParameter(BrowserContract.PARAM_PROFILE, mProfile).build();
+        return mCombinedUriWithProfile.buildUpon()
+                                      .appendQueryParameter(BrowserContract.PARAM_LIMIT,
+                                                            String.valueOf(limit))
+                                      .build();
     }
 
-    private Uri getAllBookmarksUri() {
-        Uri.Builder uriBuilder = mBookmarksUriWithProfile.buildUpon()
-            .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1");
-        return uriBuilder.build();
-    }
-
-    private Uri getAllHistoryUri() {
-        Uri.Builder uriBuilder = mHistoryUriWithProfile.buildUpon()
-            .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1");
-        return uriBuilder.build();
-    }
-
-    private Uri getAllFaviconsUri() {
-        Uri.Builder uriBuilder = mFaviconsUriWithProfile.buildUpon()
-            .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1");
-        return uriBuilder.build();
+    private static Uri withDeleted(final Uri uri) {
+        return uri.buildUpon()
+                  .appendQueryParameter(BrowserContract.PARAM_SHOW_DELETED, "1")
+                  .build();
     }
 
     private Cursor filterAllSites(ContentResolver cr, String[] projection, CharSequence constraint,
-            int limit, CharSequence urlFilter, String selection, String[] selectionArgs) {
-        // The combined history/bookmarks selection queries for sites with a url or title containing
+                                  int limit, CharSequence urlFilter, String selection, String[] selectionArgs) {
+        // The combined history/bookmarks selection queries for sites with a URL or title containing
         // the constraint string(s), treating space-separated words as separate constraints
         if (!TextUtils.isEmpty(constraint)) {
-          String[] constraintWords = constraint.toString().split(" ");
-          // Only create a filter query with a maximum of 10 constraint words
-          int constraintCount = Math.min(constraintWords.length, 10);
-          for (int i = 0; i < constraintCount; i++) {
-              selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " LIKE ? OR " +
-                                                                    Combined.TITLE + " LIKE ?)");
-              String constraintWord =  "%" + constraintWords[i] + "%";
-              selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
-                  new String[] { constraintWord, constraintWord });
-          }
+            final String[] constraintWords = constraint.toString().split(" ");
+
+            // Only create a filter query with a maximum of 10 constraint words.
+            final int constraintCount = Math.min(constraintWords.length, 10);
+            for (int i = 0; i < constraintCount; i++) {
+                selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " LIKE ? OR " +
+                                                                      Combined.TITLE + " LIKE ?)");
+                String constraintWord =  "%" + constraintWords[i] + "%";
+                selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
+                                                            new String[] { constraintWord, constraintWord });
+            }
         }
 
         if (urlFilter != null) {
             selection = DBUtils.concatenateWhere(selection, "(" + Combined.URL + " NOT LIKE ?)");
             selectionArgs = DBUtils.appendSelectionArgs(selectionArgs, new String[] { urlFilter.toString() });
         }
 
         // Our version of frecency is computed by scaling the number of visits by a multiplier
@@ -534,21 +554,23 @@ public class LocalBrowserDB {
 
         return cr.query(combinedUriWithLimit(limit),
                         projection,
                         selection,
                         selectionArgs,
                         sortOrder);
     }
 
+    @Override
     public int getCount(ContentResolver cr, String database) {
         int count = 0;
         String[] columns = null;
         String constraint = null;
         Uri uri = null;
+
         if ("history".equals(database)) {
             uri = mHistoryUriWithProfile;
             columns = new String[] { History._ID };
             constraint = Combined.VISITS + " > 0";
         } else if ("bookmarks".equals(database)) {
             uri = mBookmarksUriWithProfile;
             columns = new String[] { Bookmarks._ID };
             // ignore folders, tags, keywords, separators, etc.
@@ -558,29 +580,32 @@ public class LocalBrowserDB {
             columns = new String[] { Thumbnails._ID };
         } else if ("favicons".equals(database)) {
             uri = mFaviconsUriWithProfile;
             columns = new String[] { Favicons._ID };
         } else if ("readinglist".equals(database)) {
             uri = mReadingListUriWithProfile;
             columns = new String[] { ReadingListItems._ID };
         }
+
         if (uri != null) {
             final Cursor cursor = cr.query(uri, columns, constraint, null, null);
 
             try {
                 count = cursor.getCount();
             } finally {
                 cursor.close();
             }
         }
+
         debug("Got count " + count + " for " + database);
         return count;
     }
 
+    @Override
     @RobocopTarget
     public Cursor filter(ContentResolver cr, CharSequence constraint, int limit,
                          EnumSet<FilterFlags> flags) {
         String selection = "";
         String[] selectionArgs = null;
 
         if (flags.contains(FilterFlags.EXCLUDE_PINNED_SITES)) {
             selection = Combined.URL + " NOT IN (SELECT " +
@@ -597,16 +622,17 @@ public class LocalBrowserDB {
                                              Combined.BOOKMARK_ID,
                                              Combined.HISTORY_ID },
                               constraint,
                               limit,
                               null,
                               selection, selectionArgs);
     }
 
+    @Override
     public Cursor getTopSites(ContentResolver cr, int limit) {
         // Filter out unvisited bookmarks and the ones that don't have real
         // parents (e.g. pinned sites or reading list items).
         String selection = DBUtils.concatenateWhere(Combined.HISTORY_ID + " <> -1",
                                              Combined.URL + " NOT IN (SELECT " +
                                              Bookmarks.URL + " FROM bookmarks WHERE " +
                                              DBUtils.qualifyColumn("bookmarks", Bookmarks.PARENT) + " < ? AND " +
                                              DBUtils.qualifyColumn("bookmarks", Bookmarks.IS_DELETED) + " == 0)");
@@ -620,81 +646,89 @@ public class LocalBrowserDB {
                                              Combined.HISTORY_ID },
                               "",
                               limit,
                               AboutPages.URL_FILTER,
                               selection,
                               selectionArgs);
     }
 
+    @Override
     public void updateVisitedHistory(ContentResolver cr, String uri) {
         ContentValues values = new ContentValues();
 
         values.put(History.URL, uri);
         values.put(History.DATE_LAST_VISITED, System.currentTimeMillis());
         values.put(History.IS_DELETED, 0);
 
         // This will insert a new history entry if one for this URL
         // doesn't already exist
         cr.update(mUpdateHistoryUriWithProfile,
                   values,
                   History.URL + " = ?",
                   new String[] { uri });
     }
 
+    @Override
     public void updateHistoryTitle(ContentResolver cr, String uri, String title) {
         ContentValues values = new ContentValues();
         values.put(History.TITLE, title);
 
         cr.update(mHistoryUriWithProfile,
                   values,
                   History.URL + " = ?",
                   new String[] { uri });
     }
 
+    @Override
     @RobocopTarget
     public Cursor getAllVisitedHistory(ContentResolver cr) {
         return cr.query(mHistoryUriWithProfile,
                         new String[] { History.URL },
                         History.VISITS + " > 0",
                         null,
                         null);
     }
 
+    @Override
     public Cursor getRecentHistory(ContentResolver cr, int limit) {
         return cr.query(combinedUriWithLimit(limit),
                         new String[] { Combined._ID,
                                        Combined.BOOKMARK_ID,
                                        Combined.HISTORY_ID,
                                        Combined.URL,
                                        Combined.TITLE,
                                        Combined.DATE_LAST_VISITED,
                                        Combined.VISITS },
                         History.DATE_LAST_VISITED + " > 0",
                         null,
                         History.DATE_LAST_VISITED + " DESC");
     }
 
+    @Override
     public void expireHistory(ContentResolver cr, ExpirePriority priority) {
         Uri url = mHistoryExpireUriWithProfile;
         url = url.buildUpon().appendQueryParameter(BrowserContract.PARAM_EXPIRE_PRIORITY, priority.toString()).build();
         cr.delete(url, null, null);
     }
 
+    @Override
     @RobocopTarget
     public void removeHistoryEntry(ContentResolver cr, String url) {
         cr.delete(mHistoryUriWithProfile,
                   History.URL + " = ?",
                   new String[] { url });
     }
 
+    @Override
     public void clearHistory(ContentResolver cr) {
         cr.delete(mHistoryUriWithProfile, null, null);
     }
 
+    @Override
     @RobocopTarget
     public Cursor getBookmarksInFolder(ContentResolver cr, long folderId) {
         final boolean addDesktopFolder;
 
         // We always want to show mobile bookmarks in the root view.
         if (folderId == Bookmarks.FIXED_ROOT_ID) {
             folderId = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID);
 
@@ -735,16 +769,17 @@ public class LocalBrowserDB {
         if (addDesktopFolder) {
             // Wrap cursor to add fake desktop bookmarks and reading list folders
             return new SpecialFoldersCursorWrapper(c, addDesktopFolder);
         }
 
         return c;
     }
 
+    @Override
     public Cursor getReadingList(ContentResolver cr) {
         return cr.query(mReadingListUriWithProfile,
                         ReadingListItems.DEFAULT_PROJECTION,
                         null,
                         null,
                         null);
     }
 
@@ -773,16 +808,17 @@ public class LocalBrowserDB {
             final boolean e = c.getCount() > 0;
             mDesktopBookmarksExist = e;
             return e;
         } finally {
             c.close();
         }
     }
 
+    @Override
     @RobocopTarget
     public boolean isBookmark(ContentResolver cr, String uri) {
         final Cursor c = cr.query(bookmarksUriWithLimit(1),
                                   new String[] { Bookmarks._ID },
                                   Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " != ?",
                                   new String[] { uri, String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) },
                                   Bookmarks.URL);
 
@@ -793,16 +829,17 @@ public class LocalBrowserDB {
 
         try {
             return c.getCount() > 0;
         } finally {
             c.close();
         }
     }
 
+    @Override
     public boolean isReadingListItem(ContentResolver cr, String uri) {
         final Cursor c = cr.query(mReadingListUriWithProfile,
                                   new String[] { ReadingListItems._ID },
                                   ReadingListItems.URL + " = ? ",
                                   new String[] { uri },
                                   null);
 
         if (c == null) {
@@ -812,16 +849,17 @@ public class LocalBrowserDB {
 
         try {
             return c.getCount() > 0;
         } finally {
             c.close();
         }
     }
 
+    @Override
     public String getUrlForKeyword(ContentResolver cr, String keyword) {
         final Cursor c = cr.query(mBookmarksUriWithProfile,
                                   new String[] { Bookmarks.URL },
                                   Bookmarks.KEYWORD + " = ?",
                                   new String[] { keyword },
                                   null);
         try {
             if (!c.moveToFirst()) {
@@ -919,35 +957,38 @@ public class LocalBrowserDB {
 
         ContentValues bumped = new ContentValues();
         bumped.put(Bookmarks.DATE_MODIFIED, now);
 
         final int updated = cr.update(mBookmarksUriWithProfile, bumped, where, args);
         debug("Updated " + updated + " rows to new modified time.");
     }
 
+    @Override
     @RobocopTarget
     public void addBookmark(ContentResolver cr, String title, String uri) {
         long folderId = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID);
         addBookmarkItem(cr, title, uri, folderId);
     }
 
+    @Override
     @RobocopTarget
     public void removeBookmarksWithURL(ContentResolver cr, String uri) {
         Uri contentUri = mBookmarksUriWithProfile;
 
         // Do this now so that the items still exist!
         bumpParents(cr, Bookmarks.URL, uri);
 
         final String[] urlArgs = new String[] { uri, String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) };
         final String urlEquals = Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " != ? ";
 
         cr.delete(contentUri, urlEquals, urlArgs);
     }
 
+    @Override
     public void addReadingListItem(ContentResolver cr, ContentValues values) {
         // Check that required fields are present.
         for (String field: ReadingListItems.REQUIRED_FIELDS) {
             if (!values.containsKey(field)) {
                 throw new IllegalArgumentException("Missing required field for reading list item: " + field);
             }
         }
 
@@ -963,24 +1004,27 @@ public class LocalBrowserDB {
         final int updated = cr.update(insertUri,
                                       values,
                                       ReadingListItems.URL + " = ? ",
                                       new String[] { values.getAsString(ReadingListItems.URL) });
 
         debug("Updated " + updated + " rows to new modified time.");
     }
 
+    @Override
     public void removeReadingListItemWithURL(ContentResolver cr, String uri) {
         cr.delete(mReadingListUriWithProfile, ReadingListItems.URL + " = ? ", new String[] { uri });
     }
 
+    @Override
     public void registerBookmarkObserver(ContentResolver cr, ContentObserver observer) {
         cr.registerContentObserver(mBookmarksUriWithProfile, false, observer);
     }
 
+    @Override
     @RobocopTarget
     public void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword) {
         ContentValues values = new ContentValues();
         values.put(Browser.BookmarkColumns.TITLE, title);
         values.put(Bookmarks.URL, uri);
         values.put(Bookmarks.KEYWORD, keyword);
         values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
 
@@ -992,16 +1036,17 @@ public class LocalBrowserDB {
 
     /**
      * Get the favicon from the database, if any, associated with the given favicon URL. (That is,
      * the URL of the actual favicon image, not the URL of the page with which the favicon is associated.)
      * @param cr The ContentResolver to use.
      * @param faviconURL The URL of the favicon to fetch from the database.
      * @return The decoded Bitmap from the database, if any. null if none is stored.
      */
+    @Override
     public LoadFaviconResult getFaviconForUrl(ContentResolver cr, String faviconURL) {
         final Cursor c = cr.query(mFaviconsUriWithProfile,
                                   new String[] { Favicons.DATA },
                                   Favicons.URL + " = ? AND " + Favicons.DATA + " IS NOT NULL",
                                   new String[] { faviconURL },
                                   null);
 
         boolean shouldDelete = false;
@@ -1039,16 +1084,17 @@ public class LocalBrowserDB {
         }
 
         return FaviconDecoder.decodeFavicon(b);
     }
 
     /**
      * Try to find a usable favicon URL in the history or bookmarks table.
      */
+    @Override
     public String getFaviconURLFromPageURL(ContentResolver cr, String uri) {
         // Check first in the history table.
         Cursor c = cr.query(mHistoryUriWithProfile,
                             new String[] { History.FAVICON_URL },
                             Combined.URL + " = ?",
                             new String[] { uri },
                             null);
 
@@ -1080,25 +1126,35 @@ public class LocalBrowserDB {
             }
 
             return null;
         } finally {
             c.close();
         }
     }
 
+    @Override
+    public boolean hideSuggestedSite(String url) {
+        if (mSuggestedSites == null) {
+            return false;
+        }
+
+        return mSuggestedSites.hideSite(url);
+    }
+
+    @Override
     public void updateFaviconForUrl(ContentResolver cr, String pageUri,
             byte[] encodedFavicon, String faviconUri) {
         ContentValues values = new ContentValues();
         values.put(Favicons.URL, faviconUri);
         values.put(Favicons.PAGE_URL, pageUri);
         values.put(Favicons.DATA, encodedFavicon);
 
         // Update or insert
-        Uri faviconsUri = getAllFaviconsUri().buildUpon().
+        Uri faviconsUri = withDeleted(mFaviconsUriWithProfile).buildUpon().
                 appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
 
         final int updated = cr.update(faviconsUri,
                                       values,
                                       Favicons.URL + " = ?",
                                       new String[] { faviconUri });
 
         if (updated == 0) {
@@ -1153,19 +1209,19 @@ public class LocalBrowserDB {
         final ContentValues historyValues = new ContentValues();
         historyValues.put(History.FAVICON_ID, id);
         cr.update(mHistoryUriWithProfile,
                   historyValues,
                   History.URL + " = ?",
                   new String[] { pageURL });
     }
 
+    @Override
     public void updateThumbnailForUrl(ContentResolver cr, String uri,
             BitmapDrawable thumbnail) {
-
         // If a null thumbnail was passed in, delete the stored thumbnail for this url.
         if (thumbnail == null) {
             cr.delete(mThumbnailsUriWithProfile, Thumbnails.URL + " == ?", new String[] { uri });
             return;
         }
 
         Bitmap bitmap = thumbnail.getBitmap();
 
@@ -1184,16 +1240,17 @@ public class LocalBrowserDB {
         Uri thumbnailsUri = mThumbnailsUriWithProfile.buildUpon().
                 appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
         cr.update(thumbnailsUri,
                   values,
                   Thumbnails.URL + " = ?",
                   new String[] { uri });
     }
 
+    @Override
     @RobocopTarget
     public byte[] getThumbnailForUrl(ContentResolver cr, String uri) {
         final Cursor c = cr.query(mThumbnailsUriWithProfile,
                                   new String[]{ Thumbnails.DATA },
                                   Thumbnails.URL + " = ? AND " + Thumbnails.DATA + " IS NOT NULL",
                                   new String[]{ uri },
                                   null);
         try {
@@ -1212,21 +1269,18 @@ public class LocalBrowserDB {
 
     /**
      * Query for non-null thumbnails matching the provided <code>urls</code>.
      * The returned cursor will have no more than, but possibly fewer than,
      * the requested number of thumbnails.
      *
      * Returns null if the provided list of URLs is empty or null.
      */
+    @Override
     public Cursor getThumbnailsForUrls(ContentResolver cr, List<String> urls) {
-        if (urls == null) {
-            return null;
-        }
-
         final int urlCount = urls.size();
         if (urlCount == 0) {
             return null;
         }
 
         // Don't match against null thumbnails.
         final String selection = Thumbnails.DATA + " IS NOT NULL AND " +
                            DBUtils.computeSQLInClause(urlCount, Thumbnails.URL);
@@ -1234,36 +1288,37 @@ public class LocalBrowserDB {
 
         return cr.query(mThumbnailsUriWithProfile,
                         new String[] { Thumbnails.URL, Thumbnails.DATA },
                         selection,
                         selectionArgs,
                         null);
     }
 
+    @Override
     @RobocopTarget
     public void removeThumbnails(ContentResolver cr) {
         cr.delete(mThumbnailsUriWithProfile, null, null);
     }
 
     // Utility function for updating existing history using batch operations
+    @Override
     public void updateHistoryInBatch(ContentResolver cr,
                                      Collection<ContentProviderOperation> operations,
                                      String url, String title,
                                      long date, int visits) {
-
         final String[] projection = {
             History._ID,
             History.VISITS,
             History.DATE_LAST_VISITED
         };
 
 
         // We need to get the old visit count.
-        final Cursor cursor = cr.query(getAllHistoryUri(),
+        final Cursor cursor = cr.query(withDeleted(mHistoryUriWithProfile),
                                        projection,
                                        History.URL + " = ?",
                                        new String[] { url },
                                        null);
         try {
             ContentValues values = new ContentValues();
 
             // Restore deleted record if possible
@@ -1283,32 +1338,33 @@ public class LocalBrowserDB {
                 values.put(History.VISITS, visits);
                 values.put(History.DATE_LAST_VISITED, date);
             }
             if (title != null) {
                 values.put(History.TITLE, title);
             }
             values.put(History.URL, url);
 
-            Uri historyUri = getAllHistoryUri().buildUpon().
+            Uri historyUri = withDeleted(mHistoryUriWithProfile).buildUpon().
                 appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
 
             // Update or insert
             ContentProviderOperation.Builder builder =
                 ContentProviderOperation.newUpdate(historyUri);
             builder.withSelection(History.URL + " = ?", new String[] { url });
             builder.withValues(values);
 
             // Queue the operation
             operations.add(builder.build());
         } finally {
             cursor.close();
         }
     }
 
+    @Override
     public void updateBookmarkInBatch(ContentResolver cr,
                                       Collection<ContentProviderOperation> operations,
                                       String url, String title, String guid,
                                       long parent, long added,
                                       long modified, long position,
                                       String keyword, int type) {
         ContentValues values = new ContentValues();
         if (title == null && url != null) {
@@ -1339,34 +1395,34 @@ public class LocalBrowserDB {
         // This assumes no "real" folder has a negative ID. Only
         // things like the reading list folder do.
         if (parent < 0) {
             parent = getFolderIdFromGuid(cr, Bookmarks.MOBILE_FOLDER_GUID);
         }
         values.put(Bookmarks.PARENT, parent);
         values.put(Bookmarks.TYPE, type);
 
-        Uri bookmarkUri = getAllBookmarksUri().buildUpon().
+        Uri bookmarkUri = withDeleted(mBookmarksUriWithProfile).buildUpon().
             appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
         // Update or insert
         ContentProviderOperation.Builder builder =
             ContentProviderOperation.newUpdate(bookmarkUri);
         if (url != null) {
             // Bookmarks are defined by their URL and Folder.
             builder.withSelection(Bookmarks.URL + " = ? AND "
                                   + Bookmarks.PARENT + " = ?",
                                   new String[] { url,
                                                  Long.toString(parent)
                                   });
         } else if (title != null) {
             // Or their title and parent folder. (Folders!)
             builder.withSelection(Bookmarks.TITLE + " = ? AND "
                                   + Bookmarks.PARENT + " = ?",
-                                  new String[] { title,
-                                                 Long.toString(parent)
+                                  new String[]{ title,
+                                                Long.toString(parent)
                                   });
         } else if (type == Bookmarks.TYPE_SEPARATOR) {
             // Or their their position (separators)
             builder.withSelection(Bookmarks.POSITION + " = ? AND "
                                   + Bookmarks.PARENT + " = ?",
                                   new String[] { Long.toString(position),
                                                  Long.toString(parent)
                                   });
@@ -1374,29 +1430,30 @@ public class LocalBrowserDB {
             Log.e(LOGTAG, "Bookmark entry without url or title and not a separator, not added.");
         }
         builder.withValues(values);
 
         // Queue the operation
         operations.add(builder.build());
     }
 
+    @Override
     public void updateFaviconInBatch(ContentResolver cr,
                                      Collection<ContentProviderOperation> operations,
                                      String url, String faviconUrl,
                                      String faviconGuid, byte[] data) {
         ContentValues values = new ContentValues();
         values.put(Favicons.DATA, data);
         values.put(Favicons.PAGE_URL, url);
         if (faviconUrl != null) {
             values.put(Favicons.URL, faviconUrl);
         }
 
         // Update or insert
-        Uri faviconsUri = getAllFaviconsUri().buildUpon().
+        Uri faviconsUri = withDeleted(mFaviconsUriWithProfile).buildUpon().
             appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true").build();
         // Update or insert
         ContentProviderOperation.Builder builder =
             ContentProviderOperation.newUpdate(faviconsUri);
         builder.withValues(values);
         builder.withSelection(Favicons.PAGE_URL + " = ?", new String[] { url });
         // Queue the operation
         operations.add(builder.build());
@@ -1475,16 +1532,17 @@ public class LocalBrowserDB {
             if (columnIndex == getColumnIndex(Bookmarks.GUID) && mAtDesktopBookmarksPosition) {
                 return Bookmarks.FAKE_DESKTOP_FOLDER_GUID;
             }
 
             return "";
         }
     }
 
+    @Override
     public void pinSite(ContentResolver cr, String url, String title, int position) {
         ContentValues values = new ContentValues();
         final long now = System.currentTimeMillis();
         values.put(Bookmarks.TITLE, title);
         values.put(Bookmarks.URL, url);
         values.put(Bookmarks.PARENT, Bookmarks.FIXED_PINNED_LIST_ID);
         values.put(Bookmarks.DATE_MODIFIED, now);
         values.put(Bookmarks.POSITION, position);
@@ -1499,36 +1557,39 @@ public class LocalBrowserDB {
         cr.update(uri,
                   values,
                   Bookmarks.POSITION + " = ? AND " +
                   Bookmarks.PARENT + " = ?",
                   new String[] { Integer.toString(position),
                                  String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) });
     }
 
+    @Override
     public Cursor getPinnedSites(ContentResolver cr, int limit) {
         return cr.query(bookmarksUriWithLimit(limit),
                         new String[] { Bookmarks._ID,
                                        Bookmarks.URL,
                                        Bookmarks.TITLE,
                                        Bookmarks.POSITION },
                         Bookmarks.PARENT + " == ?",
                         new String[] { String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID) },
                         Bookmarks.POSITION + " ASC");
     }
 
+    @Override
     public void unpinSite(ContentResolver cr, int position) {
         cr.delete(mBookmarksUriWithProfile,
                   Bookmarks.PARENT + " == ? AND " + Bookmarks.POSITION + " = ?",
                   new String[] {
                       String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID),
                       Integer.toString(position)
                   });
     }
 
+    @Override
     @RobocopTarget
     public Cursor getBookmarkForUrl(ContentResolver cr, String url) {
         Cursor c = cr.query(bookmarksUriWithLimit(1),
                             new String[] { Bookmarks._ID,
                                            Bookmarks.URL,
                                            Bookmarks.TITLE,
                                            Bookmarks.KEYWORD },
                             Bookmarks.URL + " = ?",
@@ -1537,9 +1598,91 @@ public class LocalBrowserDB {
 
         if (c != null && c.getCount() == 0) {
             c.close();
             c = null;
         }
 
         return c;
     }
+
+    @Override
+    public void setSuggestedSites(SuggestedSites suggestedSites) {
+        mSuggestedSites = suggestedSites;
+    }
+
+    @Override
+    public boolean hasSuggestedImageUrl(String url) {
+        if (mSuggestedSites == null) {
+            return false;
+        }
+        return mSuggestedSites.contains(url);
+    }
+
+    @Override
+    public String getSuggestedImageUrlForUrl(String url) {
+        if (mSuggestedSites == null) {
+            return null;
+        }
+        return mSuggestedSites.getImageUrlForUrl(url);
+    }
+
+    @Override
+    public int getSuggestedBackgroundColorForUrl(String url) {
+        if (mSuggestedSites == null) {
+            return 0;
+        }
+        final String bgColor = mSuggestedSites.getBackgroundColorForUrl(url);
+        if (bgColor != null) {
+            return Color.parseColor(bgColor);
+        }
+
+        return 0;
+    }
+
+    @Override
+    public int getTrackingIdForUrl(String url) {
+        return mSuggestedSites.getTrackingIdForUrl(url);
+    }
+
+    private static void appendUrlsFromCursor(List<String> urls, Cursor c) {
+        if (!c.moveToFirst()) {
+            return;
+        }
+
+        do {
+            String url = c.getString(c.getColumnIndex(History.URL));
+
+            // Do a simpler check before decoding to avoid parsing
+            // all URLs unnecessarily.
+            if (StringUtils.isUserEnteredUrl(url)) {
+                url = StringUtils.decodeUserEnteredUrl(url);
+            }
+
+            urls.add(url);
+        } while (c.moveToNext());
+    }
+
+    @Override
+    public Cursor getTopSites(ContentResolver cr, int minLimit, int maxLimit) {
+        // Note this is not a single query anymore, but actually returns a mixture
+        // of two queries, one for topSites and one for pinned sites.
+        Cursor pinnedSites = getPinnedSites(cr, minLimit);
+
+        int pinnedCount = pinnedSites.getCount();
+        Cursor topSites = getTopSites(cr, maxLimit - pinnedCount);
+        int topCount = topSites.getCount();
+
+        Cursor suggestedSites = null;
+        if (mSuggestedSites != null) {
+            final int count = minLimit - pinnedCount - topCount;
+            if (count > 0) {
+                final List<String> excludeUrls = new ArrayList<String>(pinnedCount + topCount);
+                appendUrlsFromCursor(excludeUrls, pinnedSites);
+                appendUrlsFromCursor(excludeUrls, topSites);
+
+                suggestedSites = mSuggestedSites.get(count, excludeUrls);
+            }
+        }
+
+        return new TopSitesCursorWrapper(pinnedSites, topSites, suggestedSites, minLimit);
+    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/LocalSearches.java
@@ -0,0 +1,28 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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.ContentResolver;
+import android.content.ContentValues;
+import android.net.Uri;
+
+/**
+ * Helper class for dealing with the search provider inside Fennec.
+ */
+public class LocalSearches implements Searches {
+    private final Uri uriWithProfile;
+
+    public LocalSearches(String mProfile) {
+        uriWithProfile = DBUtils.appendProfileWithDefault(mProfile, BrowserContract.SearchHistory.CONTENT_URI);
+    }
+
+    @Override
+    public void insert(ContentResolver cr, String query) {
+        final ContentValues values = new ContentValues();
+        values.put(BrowserContract.SearchHistory.QUERY, query);
+        cr.insert(uriWithProfile, values);
+    }
+}
rename from mobile/android/base/TabsAccessor.java
rename to mobile/android/base/db/LocalTabsAccessor.java
--- a/mobile/android/base/TabsAccessor.java
+++ b/mobile/android/base/db/LocalTabsAccessor.java
@@ -1,37 +1,36 @@
 /* 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;
+package org.mozilla.gecko.db;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.regex.Pattern;
 
 import org.json.JSONArray;
 import org.json.JSONException;
-import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UIAsyncTask;
 
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
-import android.os.Parcel;
-import android.os.Parcelable;
 import android.text.TextUtils;
 import android.text.format.DateUtils;
 import android.util.Log;
 
-public final class TabsAccessor {
+public class LocalTabsAccessor implements TabsAccessor {
     private static final String LOGTAG = "GeckoTabsAccessor";
 
     public static final String[] TABS_PROJECTION_COLUMNS = new String[] {
                                                                 BrowserContract.Tabs.TITLE,
                                                                 BrowserContract.Tabs.URL,
                                                                 BrowserContract.Clients.GUID,
                                                                 BrowserContract.Clients.NAME,
                                                                 BrowserContract.Clients.LAST_MODIFIED,
@@ -49,167 +48,39 @@ public final class TabsAccessor {
             BrowserContract.Clients.GUID + " DESC, " +
             // Within a single client, most recently used tabs first.
             BrowserContract.Tabs.LAST_USED + " DESC";
 
     private static final String LOCAL_CLIENT_SELECTION = BrowserContract.Clients.GUID + " IS NULL";
 
     private static final Pattern FILTERED_URL_PATTERN = Pattern.compile("^(about|chrome|wyciwyg|file):");
 
-    /**
-     * A thin representation of a remote client.
-     * <p>
-     * We use the hash of the client's GUID as the ID in
-     * {@link RemoteTabsExpandableListAdapter#getGroupId(int)}.
-     */
-    public static class RemoteClient implements Parcelable {
-        public final String guid;
-        public final String name;
-        public final long lastModified;
-        public final String deviceType;
-        public final ArrayList<RemoteTab> tabs;
-
-        public RemoteClient(String guid, String name, long lastModified, String deviceType) {
-            this.guid = guid;
-            this.name = name;
-            this.lastModified = lastModified;
-            this.deviceType = deviceType;
-            this.tabs = new ArrayList<RemoteTab>();
-        }
-
-        @Override
-        public int describeContents() {
-            return 0;
-        }
-
-        @Override
-        public void writeToParcel(Parcel parcel, int flags) {
-            parcel.writeString(guid);
-            parcel.writeString(name);
-            parcel.writeLong(lastModified);
-            parcel.writeString(deviceType);
-            parcel.writeTypedList(tabs);
-        }
-
-        public static final Creator<RemoteClient> CREATOR = new Creator<RemoteClient>() {
-            @Override
-            public RemoteClient createFromParcel(final Parcel source) {
-                final String guid = source.readString();
-                final String name = source.readString();
-                final long lastModified = source.readLong();
-                final String deviceType = source.readString();
-
-                final RemoteClient client = new RemoteClient(guid, name, lastModified, deviceType);
-                source.readTypedList(client.tabs, RemoteTab.CREATOR);
-
-                return client;
-            }
-
-            @Override
-            public RemoteClient[] newArray(final int size) {
-                return new RemoteClient[size];
-            }
-        };
-    }
-
-    /**
-     * A thin representation of a remote tab.
-     * <p>
-     * We use the hash of the tab as the ID in
-     * {@link RemoteTabsExpandableListAdapter#getClientId(int)}, and therefore we
-     * must implement equality as well. These are generated functions.
-     */
-    public static class RemoteTab implements Parcelable {
-        public final String title;
-        public final String url;
+    private final Uri tabsUriWithProfile;
+    private final Uri clientsUriWithProfile;
 
-        public RemoteTab(String title, String url) {
-            this.title = title;
-            this.url = url;
-        }
-
-        @Override
-        public int describeContents() {
-            return 0;
-        }
-
-        @Override
-        public void writeToParcel(Parcel parcel, int flags) {
-            parcel.writeString(title);
-            parcel.writeString(url);
-        }
-
-        public static final Creator<RemoteTab> CREATOR = new Creator<RemoteTab>() {
-            @Override
-            public RemoteTab createFromParcel(final Parcel source) {
-                final String title = source.readString();
-                final String url = source.readString();
-
-                return new RemoteTab(title, url);
-            }
-
-            @Override
-            public RemoteTab[] newArray(final int size) {
-                return new RemoteTab[size];
-            }
-        };
-
-        @Override
-        public int hashCode() {
-            final int prime = 31;
-            int result = 1;
-            result = prime * result + ((title == null) ? 0 : title.hashCode());
-            result = prime * result + ((url == null) ? 0 : url.hashCode());
-            return result;
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (this == obj) {
-                return true;
-            }
-            if (obj == null) {
-                return false;
-            }
-            if (getClass() != obj.getClass()) {
-                return false;
-            }
-            RemoteTab other = (RemoteTab) obj;
-            if (title == null) {
-                if (other.title != null) {
-                    return false;
-                }
-            } else if (!title.equals(other.title)) {
-                return false;
-            }
-            if (url == null) {
-                if (other.url != null) {
-                    return false;
-                }
-            } else if (!url.equals(other.url)) {
-                return false;
-            }
-            return true;
-        }
+    public LocalTabsAccessor(String mProfile) {
+        tabsUriWithProfile = DBUtils.appendProfileWithDefault(mProfile, BrowserContract.Tabs.CONTENT_URI);
+        clientsUriWithProfile = DBUtils.appendProfileWithDefault(mProfile, BrowserContract.Clients.CONTENT_URI);
     }
 
     /**
      * Extract client and tab records from a cursor.
      * <p>
      * The position of the cursor is moved to before the first record before
      * reading. The cursor is advanced until there are no more records to be
      * read. The position of the cursor is restored before returning.
      *
      * @param cursor
      *            to extract records from. The records should already be grouped
      *            by client GUID.
      * @return list of clients, each containing list of tabs.
      */
-    public static List<RemoteClient> getClientsFromCursor(final Cursor cursor) {
-        final ArrayList<RemoteClient> clients = new ArrayList<TabsAccessor.RemoteClient>();
+    @Override
+    public List<RemoteClient> getClientsFromCursor(final Cursor cursor) {
+        final ArrayList<RemoteClient> clients = new ArrayList<RemoteClient>();
 
         final int originalPosition = cursor.getPosition();
         try {
             if (!cursor.moveToFirst()) {
                 return clients;
             }
 
             final int tabTitleIndex = cursor.getColumnIndex(BrowserContract.Tabs.TITLE);
@@ -241,50 +112,50 @@ public final class TabsAccessor {
             }
         } finally {
             cursor.moveToPosition(originalPosition);
         }
 
         return clients;
     }
 
-    public static Cursor getRemoteTabsCursor(Context context) {
+    @Override
+    public Cursor getRemoteTabsCursor(Context context) {
         return getRemoteTabsCursor(context, -1);
     }
 
-    public static Cursor getRemoteTabsCursor(Context context, int limit) {
-        Uri uri = BrowserContract.Tabs.CONTENT_URI;
+    @Override
+    public Cursor getRemoteTabsCursor(Context context, int limit) {
+        Uri uri = tabsUriWithProfile;
 
         if (limit > 0) {
             uri = uri.buildUpon()
                      .appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit))
                      .build();
         }
 
         final Cursor cursor =  context.getContentResolver().query(uri,
                                                             TABS_PROJECTION_COLUMNS,
                                                             REMOTE_TABS_SELECTION,
                                                             null,
                                                             REMOTE_TABS_SORT_ORDER);
         return cursor;
     }
 
-    public interface OnQueryTabsCompleteListener {
-        public void onQueryTabsComplete(List<RemoteClient> clients);
-    }
-
     // This method returns all tabs from all remote clients,
     // ordered by most recent client first, most recent tab first
-    public static void getTabs(final Context context, final OnQueryTabsCompleteListener listener) {
+    @Override
+    public void getTabs(final Context context, final OnQueryTabsCompleteListener listener) {
         getTabs(context, 0, listener);
     }
 
     // This method returns limited number of tabs from all remote clients,
     // ordered by most recent client first, most recent tab first
-    public static void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener) {
+    @Override
+    public void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener) {
         // If there is no listener, no point in doing work.
         if (listener == null)
             return;
 
         (new UIAsyncTask.WithoutParams<List<RemoteClient>>(ThreadUtils.getBackgroundHandler()) {
             @Override
             protected List<RemoteClient> doInBackground() {
                 final Cursor cursor = getRemoteTabsCursor(context, limit);
@@ -301,38 +172,39 @@ public final class TabsAccessor {
             @Override
             protected void onPostExecute(List<RemoteClient> clients) {
                 listener.onQueryTabsComplete(clients);
             }
         }).execute();
     }
 
     // Updates the modified time of the local client with the current time.
-    private static void updateLocalClient(final ContentResolver cr) {
+    private void updateLocalClient(final ContentResolver cr) {
         ContentValues values = new ContentValues();
         values.put(BrowserContract.Clients.LAST_MODIFIED, System.currentTimeMillis());
-        cr.update(BrowserContract.Clients.CONTENT_URI, values, LOCAL_CLIENT_SELECTION, null);
+
+        cr.update(clientsUriWithProfile, values, LOCAL_CLIENT_SELECTION, null);
     }
 
     // Deletes all local tabs.
-    private static void deleteLocalTabs(final ContentResolver cr) {
-        cr.delete(BrowserContract.Tabs.CONTENT_URI, LOCAL_TABS_SELECTION, null);
+    private void deleteLocalTabs(final ContentResolver cr) {
+        cr.delete(tabsUriWithProfile, LOCAL_TABS_SELECTION, null);
     }
 
     /**
      * Tabs are positioned in the DB in the same order that they appear in the tabs param.
      *   - URL should never empty or null. Skip this tab if there's no URL.
      *   - TITLE should always a string, either a page title or empty.
      *   - LAST_USED should always be numeric.
      *   - FAVICON should be a URL or null.
      *   - HISTORY should be serialized JSON array of URLs.
      *   - POSITION should always be numeric.
      *   - CLIENT_GUID should always be null to represent the local client.
      */
-    private static void insertLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs) {
+    private void insertLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs) {
         // Reuse this for serializing individual history URLs as JSON.
         JSONArray history = new JSONArray();
         ArrayList<ContentValues> valuesToInsert = new ArrayList<ContentValues>();
 
         int position = 0;
         for (Tab tab : tabs) {
             // Skip this tab if it has a null URL or is in private browsing mode, or is a filtered URL.
             String url = tab.getURL();
@@ -363,39 +235,41 @@ public final class TabsAccessor {
 
             // A null client guid corresponds to the local client.
             values.putNull(BrowserContract.Tabs.CLIENT_GUID);
 
             valuesToInsert.add(values);
         }
 
         ContentValues[] valuesToInsertArray = valuesToInsert.toArray(new ContentValues[valuesToInsert.size()]);
-        cr.bulkInsert(BrowserContract.Tabs.CONTENT_URI, valuesToInsertArray);
+        cr.bulkInsert(tabsUriWithProfile, valuesToInsertArray);
     }
 
     // Deletes all local tabs and replaces them with a new list of tabs.
-    public static synchronized void persistLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs) {
+    @Override
+    public synchronized void persistLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs) {
         deleteLocalTabs(cr);
         insertLocalTabs(cr, tabs);
         updateLocalClient(cr);
     }
 
     /**
      * Matches the supplied URL string against the set of URLs to filter.
      *
      * @return true if the supplied URL should be skipped; false otherwise.
      */
-    private static boolean isFilteredURL(String url) {
+    private boolean isFilteredURL(String url) {
         return FILTERED_URL_PATTERN.matcher(url).lookingAt();
     }
 
     /**
      * Return a relative "Last synced" time span for the given tab record.
      *
      * @param now local time.
      * @param time to format string for.
      * @return string describing time span
      */
-    public static String getLastSyncedString(Context context, long now, long time) {
+    @Override
+    public String getLastSyncedString(Context context, long now, long time) {
         final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
         return context.getResources().getString(R.string.remote_tabs_last_synced, relativeTimeSpanString);
     }
 }
copy from mobile/android/base/db/URLMetadata.java
copy to mobile/android/base/db/LocalURLMetadata.java
--- a/mobile/android/base/db/URLMetadata.java
+++ b/mobile/android/base/db/LocalURLMetadata.java
@@ -1,62 +1,64 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */
 /* 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.Bookmarks;
-import org.mozilla.gecko.db.BrowserContract.History;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.Telemetry;
-
-import org.json.JSONObject;
-
-import android.content.ContentValues;
-import android.content.ContentResolver;
-import android.database.Cursor;
-import android.net.Uri;
-import android.support.v4.util.LruCache;
-import android.text.TextUtils;
-import android.util.Log;
-
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-// Holds metadata info about urls. Supports some helper functions for getting back a HashMap of key value data.
-public class URLMetadata {
+import org.json.JSONObject;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.net.Uri;
+import android.support.v4.util.LruCache;
+import android.util.Log;
+
+// Holds metadata info about URLs. Supports some helper functions for getting back a HashMap of key value data.
+public class LocalURLMetadata implements URLMetadata {
     private static final String LOGTAG = "GeckoURLMetadata";
+    private final Uri uriWithProfile;
+
+    public LocalURLMetadata(String mProfile) {
+        uriWithProfile = DBUtils.appendProfileWithDefault(mProfile, URLMetadataTable.CONTENT_URI);
+    }
 
     // This returns a list of columns in the table. It's used to simplify some loops for reading/writing data.
     @SuppressWarnings("serial")
-    private static final Set<String> getModel() {
+    private final Set<String> getModel() {
         return new HashSet<String>() {{
             add(URLMetadataTable.URL_COLUMN);
             add(URLMetadataTable.TILE_IMAGE_URL_COLUMN);
             add(URLMetadataTable.TILE_COLOR_COLUMN);
         }};
     }
 
     // Store a cache of recent results. This number is chosen to match the max number of tiles on about:home
     private static final int CACHE_SIZE = 9;
     // Note: Members of this cache are unmodifiable.
-    private static final LruCache<String, Map<String, Object>> cache = new LruCache<String, Map<String, Object>>(CACHE_SIZE);
+    private final LruCache<String, Map<String, Object>> cache = new LruCache<String, Map<String, Object>>(CACHE_SIZE);
 
     /**
      * Converts a JSON object into a unmodifiable Map of known metadata properties.
      * Will throw away any properties that aren't stored in the database.
      */
-    public static Map<String, Object> fromJSON(JSONObject obj) {
+    @Override
+    public Map<String, Object> fromJSON(JSONObject obj) {
         Map<String, Object> data = new HashMap<String, Object>();
 
         Set<String> model = getModel();
         for (String key : model) {
             if (obj.has(key)) {
                 data.put(key, obj.optString(key));
             }
         }
@@ -64,17 +66,17 @@ public class URLMetadata {
         return Collections.unmodifiableMap(data);
     }
 
     /**
      * Converts a Cursor into a unmodifiable Map of known metadata properties.
      * Will throw away any properties that aren't stored in the database.
      * Will also not iterate through multiple rows in the cursor.
      */
-    private static Map<String, Object> fromCursor(Cursor c) {
+    private Map<String, Object> fromCursor(Cursor c) {
         Map<String, Object> data = new HashMap<String, Object>();
 
         Set<String> model = getModel();
         String[] columns = c.getColumnNames();
         for (String column : columns) {
             if (model.contains(column)) {
                 try {
                     data.put(column, c.getString(c.getColumnIndexOrThrow(column)));
@@ -86,19 +88,20 @@ public class URLMetadata {
 
         return Collections.unmodifiableMap(data);
     }
 
     /**
      * Returns an unmodifiable Map of url->Metadata (i.e. A second HashMap) for a list of urls.
      * Must not be called from UI or Gecko threads.
      */
-    public static Map<String, Map<String, Object>> getForUrls(final ContentResolver cr,
-                                                              final List<String> urls,
-                                                              final List<String> columns) {
+    @Override
+    public Map<String, Map<String, Object>> getForURLs(final ContentResolver cr,
+                                                       final List<String> urls,
+                                                       final List<String> columns) {
         ThreadUtils.assertNotOnUiThread();
         ThreadUtils.assertNotOnGeckoThread();
 
         final Map<String, Map<String, Object>> data = new HashMap<String, Map<String, Object>>();
 
         // Nothing to query for
         if (urls.isEmpty() || columns.isEmpty()) {
             Log.e(LOGTAG, "Queried metadata for nothing");
@@ -125,17 +128,17 @@ public class URLMetadata {
         }
 
         final String selection = DBUtils.computeSQLInClause(urlsToQuery.size(), URLMetadataTable.URL_COLUMN);
         // We need the url to build our final HashMap, so we force it to be included in the query.
         if (!columns.contains(URLMetadataTable.URL_COLUMN)) {
             columns.add(URLMetadataTable.URL_COLUMN);
         }
 
-        final Cursor cursor = cr.query(URLMetadataTable.CONTENT_URI,
+        final Cursor cursor = cr.query(uriWithProfile,
                                        columns.toArray(new String[columns.size()]), // columns,
                                        selection, // selection
                                        urlsToQuery.toArray(new String[urlsToQuery.size()]), // selectionargs
                                        null);
         try {
             if (!cursor.moveToFirst()) {
                 return Collections.unmodifiableMap(data);
             }
@@ -155,17 +158,18 @@ public class URLMetadata {
         return Collections.unmodifiableMap(data);
     }
 
     /**
      * Saves a HashMap of metadata into the database. Will iterate through columns
      * in the Database and only save rows with matching keys in the HashMap.
      * Must not be called from UI or Gecko threads.
      */
-    public static void save(final ContentResolver cr, final String url, final Map<String, Object> data) {
+    @Override
+    public void save(final ContentResolver cr, final String url, final Map<String, Object> data) {
         ThreadUtils.assertNotOnUiThread();
         ThreadUtils.assertNotOnGeckoThread();
 
         try {
             ContentValues values = new ContentValues();
 
             Set<String> model = getModel();
             for (String key : model) {
@@ -173,40 +177,19 @@ public class URLMetadata {
                     values.put(key, (String) data.get(key));
                 }
             }
 
             if (values.size() == 0) {
                 return;
             }
 
-            Uri uri = URLMetadataTable.CONTENT_URI.buildUpon()
+            Uri uri = uriWithProfile.buildUpon()
                                  .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
                                  .build();
             cr.update(uri, values, URLMetadataTable.URL_COLUMN + "=?", new String[] {
                 (String) data.get(URLMetadataTable.URL_COLUMN)
             });
         } catch (Exception ex) {
             Log.e(LOGTAG, "error saving", ex);
         }
     }
-
-    public static int deleteUnused(final ContentResolver cr, final String profile) {
-        final String selection = URLMetadataTable.URL_COLUMN + " NOT IN "
-                + "(SELECT " + History.URL
-                + " FROM " + History.TABLE_NAME
-                + " WHERE " + History.IS_DELETED + " = 0"
-                + " UNION "
-                + " SELECT " + Bookmarks.URL
-                + " FROM " + Bookmarks.TABLE_NAME
-                + " WHERE " + Bookmarks.IS_DELETED + " = 0 "
-                + " AND " + Bookmarks.URL + " IS NOT NULL)";
-
-        Uri uri = URLMetadataTable.CONTENT_URI;
-        if (!TextUtils.isEmpty(profile)) {
-            uri = uri.buildUpon()
-                     .appendQueryParameter(BrowserContract.PARAM_PROFILE, profile)
-                     .build();
-        }
-
-        return cr.delete(uri, selection, null);
-    }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/RemoteClient.java
@@ -0,0 +1,65 @@
+/* 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 java.util.ArrayList;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A thin representation of a remote client.
+ * <p>
+ * We use the hash of the client's GUID as the ID elsewhere.
+ */
+public class RemoteClient implements Parcelable {
+    public final String guid;
+    public final String name;
+    public final long lastModified;
+    public final String deviceType;
+    public final ArrayList<RemoteTab> tabs;
+
+    public RemoteClient(String guid, String name, long lastModified, String deviceType) {
+        this.guid = guid;
+        this.name = name;
+        this.lastModified = lastModified;
+        this.deviceType = deviceType;
+        this.tabs = new ArrayList<RemoteTab>();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel parcel, int flags) {
+        parcel.writeString(guid);
+        parcel.writeString(name);
+        parcel.writeLong(lastModified);
+        parcel.writeString(deviceType);
+        parcel.writeTypedList(tabs);
+    }
+
+    public static final Creator<RemoteClient> CREATOR = new Creator<RemoteClient>() {
+        @Override
+        public RemoteClient createFromParcel(final Parcel source) {
+            final String guid = source.readString();
+            final String name = source.readString();
+            final long lastModified = source.readLong();
+            final String deviceType = source.readString();
+
+            final RemoteClient client = new RemoteClient(guid, name, lastModified, deviceType);
+            source.readTypedList(client.tabs, RemoteTab.CREATOR);
+
+            return client;
+        }
+
+        @Override
+        public RemoteClient[] newArray(final int size) {
+            return new RemoteClient[size];
+        }
+    };
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/RemoteTab.java
@@ -0,0 +1,91 @@
+/* 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.RemoteTabsExpandableListAdapter;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A thin representation of a remote tab.
+ * <p>
+ * We use the hash of the tab as the ID in
+ * {@link RemoteTabsExpandableListAdapter#getClientId(int)}, and therefore we
+ * must implement equality as well. These are generated functions.
+ */
+public class RemoteTab implements Parcelable {
+    public final String title;
+    public final String url;
+
+    public RemoteTab(String title, String url) {
+        this.title = title;
+        this.url = url;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel parcel, int flags) {
+        parcel.writeString(title);
+        parcel.writeString(url);
+    }
+
+    public static final Creator<RemoteTab> CREATOR = new Creator<RemoteTab>() {
+        @Override
+        public RemoteTab createFromParcel(final Parcel source) {
+            final String title = source.readString();
+            final String url = source.readString();
+
+            return new RemoteTab(title, url);
+        }
+
+        @Override
+        public RemoteTab[] newArray(final int size) {
+            return new RemoteTab[size];
+        }
+    };
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((title == null) ? 0 : title.hashCode());
+        result = prime * result + ((url == null) ? 0 : url.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        RemoteTab other = (RemoteTab) obj;
+        if (title == null) {
+            if (other.title != null) {
+                return false;
+            }
+        } else if (!title.equals(other.title)) {
+            return false;
+        }
+        if (url == null) {
+            if (other.url != null) {
+                return false;
+            }
+        } else if (!url.equals(other.url)) {
+            return false;
+        }
+        return true;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/Searches.java
@@ -0,0 +1,15 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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.ContentResolver;
+import android.content.ContentValues;
+
+import org.mozilla.gecko.GeckoProfile;
+
+public interface Searches {
+    public void insert(ContentResolver cr, String query);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/StubBrowserDB.java
@@ -0,0 +1,309 @@
+/* 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 java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.json.JSONObject;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.distribution.Distribution;
+import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
+import org.mozilla.gecko.mozglue.RobocopTarget;
+
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.graphics.drawable.BitmapDrawable;
+
+class StubSearches implements Searches {
+    public StubSearches() {
+    }
+
+    public void insert(ContentResolver cr, String query) {
+    }
+}
+
+class StubURLMetadata implements URLMetadata {
+    public StubURLMetadata() {
+    }
+
+    public Map<String, Object> fromJSON(JSONObject obj) {
+        return new HashMap<String, Object>();
+    }
+
+    public Map<String, Map<String, Object>> getForURLs(final ContentResolver cr,
+                                                       final List<String> urls,
+                                                       final List<String> columns) {
+        return new HashMap<String, Map<String, Object>>();
+    }
+
+    public void save(final ContentResolver cr, final String url, final Map<String, Object> data) {
+    }
+}
+
+class StubTabsAccessor implements TabsAccessor {
+    public StubTabsAccessor() {
+    }
+
+    @Override
+    public List<RemoteClient> getClientsFromCursor(final Cursor cursor) {
+        return new ArrayList<RemoteClient>();
+    }
+
+    public Cursor getRemoteTabsCursor(Context context) {
+        return null;
+    }
+
+    public Cursor getRemoteTabsCursor(Context context, int limit) {
+        return null;
+    }
+
+    public void getTabs(final Context context, final OnQueryTabsCompleteListener listener) {
+        listener.onQueryTabsComplete(new ArrayList<RemoteClient>());
+    }
+    public void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener) {
+        listener.onQueryTabsComplete(new ArrayList<RemoteClient>());
+    }
+
+    public synchronized void persistLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs) { }
+
+    public String getLastSyncedString(Context context, long now, long time) {
+        return "";
+    }
+}
+
+/*
+ * This base implementation just stubs all methods. For the
+ * real implementations, see LocalBrowserDB.java.
+ */
+public class StubBrowserDB implements BrowserDB {
+    private final StubSearches searches = new StubSearches();
+    private final StubTabsAccessor tabsAccessor = new StubTabsAccessor();
+    private final StubURLMetadata urlMetadata = new StubURLMetadata();
+
+    @Override
+    public Searches getSearches() {
+        return searches;
+    }
+
+    @Override
+    public TabsAccessor getTabsAccessor() {
+        return tabsAccessor;
+    }
+
+    @Override
+    public URLMetadata getURLMetadata() {
+        return urlMetadata;
+    }
+
+    protected static final Integer FAVICON_ID_NOT_FOUND = Integer.MIN_VALUE;
+
+    public StubBrowserDB(String profile) {
+    }
+
+    public void invalidate() { }
+
+    public int addDefaultBookmarks(Context context, ContentResolver cr, final int offset) {
+        return 0;
+    }
+
+    public int addDistributionBookmarks(ContentResolver cr, Distribution distribution, int offset) {
+        return 0;
+    }
+
+    public int getCount(ContentResolver cr, String database) {
+        return 0;
+    }
+
+    @RobocopTarget
+    public Cursor filter(ContentResolver cr, CharSequence constraint, int limit,
+                         EnumSet<BrowserDB.FilterFlags> flags) {
+        return null;
+    }
+
+    public Cursor getTopSites(ContentResolver cr, int limit) {
+        return null;
+    }
+
+    public void updateVisitedHistory(ContentResolver cr, String uri) {
+    }
+
+    public void updateHistoryTitle(ContentResolver cr, String uri, String title) {
+    }
+
+    @RobocopTarget
+    public Cursor getAllVisitedHistory(ContentResolver cr) {
+        return null;
+    }
+
+    public Cursor getRecentHistory(ContentResolver cr, int limit) {
+        return null;
+    }
+
+    public void expireHistory(ContentResolver cr, BrowserContract.ExpirePriority priority) {
+    }
+
+    @RobocopTarget
+    public void removeHistoryEntry(ContentResolver cr, String url) {
+    }
+
+    public void clearHistory(ContentResolver cr) {
+    }
+
+    @RobocopTarget
+    public Cursor getBookmarksInFolder(ContentResolver cr, long folderId) {
+        return null;
+    }
+
+    public Cursor getReadingList(ContentResolver cr) {
+        return null;
+    }
+
+    @RobocopTarget
+    public boolean isBookmark(ContentResolver cr, String uri) {
+        return false;
+    }
+
+    public boolean isReadingListItem(ContentResolver cr, String uri) {
+        return false;
+    }
+
+    public String getUrlForKeyword(ContentResolver cr, String keyword) {
+        return null;
+    }
+
+    protected void bumpParents(ContentResolver cr, String param, String value) {
+    }
+
+    @RobocopTarget
+    public void addBookmark(ContentResolver cr, String title, String uri) {
+    }
+
+    @RobocopTarget
+    public void removeBookmarksWithURL(ContentResolver cr, String uri) {
+    }
+
+    public void addReadingListItem(ContentResolver cr, ContentValues values) {
+    }
+
+    public void removeReadingListItemWithURL(ContentResolver cr, String uri) {
+    }
+
+    public void registerBookmarkObserver(ContentResolver cr, ContentObserver observer) {
+    }
+
+    @RobocopTarget
+    public void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword) {
+    }
+
+    public LoadFaviconResult getFaviconForUrl(ContentResolver cr, String faviconURL) {
+        return null;
+    }
+
+    public String getFaviconURLFromPageURL(ContentResolver cr, String uri) {
+        return null;
+    }
+
+    public void updateFaviconForUrl(ContentResolver cr, String pageUri,
+                                    byte[] encodedFavicon, String faviconUri) {
+    }
+
+    public boolean hideSuggestedSite(String url) {
+        return false;
+    }
+
+    public void updateThumbnailForUrl(ContentResolver cr, String uri,
+                                      BitmapDrawable thumbnail) {
+    }
+
+    @RobocopTarget
+    public byte[] getThumbnailForUrl(ContentResolver cr, String uri) {
+        return null;
+    }
+
+    public Cursor getThumbnailsForUrls(ContentResolver cr, List<String> urls) {
+        return null;
+    }
+
+    @RobocopTarget
+    public void removeThumbnails(ContentResolver cr) {
+    }
+
+    public void updateHistoryInBatch(ContentResolver cr,
+                                     Collection<ContentProviderOperation> operations,
+                                     String url, String title,
+                                     long date, int visits) {
+    }
+
+    public void updateBookmarkInBatch(ContentResolver cr,
+                                      Collection<ContentProviderOperation> operations,
+                                      String url, String title, String guid,
+                                      long parent, long added,
+                                      long modified, long position,
+                                      String keyword, int type) {
+    }
+
+    public void updateFaviconInBatch(ContentResolver cr,
+                                     Collection<ContentProviderOperation> operations,
+                                     String url, String faviconUrl,
+                                     String faviconGuid, byte[] data) {
+    }
+
+    public void pinSite(ContentResolver cr, String url, String title, int position) {
+    }
+
+    public Cursor getPinnedSites(ContentResolver cr, int limit) {
+        return null;
+    }
+
+    public void unpinSite(ContentResolver cr, int position) {
+    }
+
+    @RobocopTarget
+    public Cursor getBookmarkForUrl(ContentResolver cr, String url) {
+        return null;
+    }
+
+    public void setSuggestedSites(SuggestedSites suggestedSites) {
+    }
+
+    public boolean hasSuggestedImageUrl(String url) {
+        return false;
+    }
+
+    public String getSuggestedImageUrlForUrl(String url) {
+        return null;
+    }
+
+    public int getSuggestedBackgroundColorForUrl(String url) {
+        return 0;
+    }
+
+    public int getTrackingIdForUrl(String url) {
+        return 0;
+    }
+
+    public Cursor getTopSites(ContentResolver cr, int minLimit, int maxLimit) {
+        return null;
+    }
+
+    public static Factory getFactory() {
+        return new Factory() {
+            @Override
+            public BrowserDB get(String profileName, File profileDir) {
+                return new StubBrowserDB(profileName);
+            }
+        };
+    }
+}
copy from mobile/android/base/TabsAccessor.java
copy to mobile/android/base/db/TabsAccessor.java
--- a/mobile/android/base/TabsAccessor.java
+++ b/mobile/android/base/db/TabsAccessor.java
@@ -1,401 +1,27 @@
 /* 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;
+ * 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/. */
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.regex.Pattern;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.util.UIAsyncTask;
+package org.mozilla.gecko.db;
 
 import android.content.ContentResolver;
-import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
-import android.net.Uri;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.text.TextUtils;
-import android.text.format.DateUtils;
-import android.util.Log;
 
-public final class TabsAccessor {
-    private static final String LOGTAG = "GeckoTabsAccessor";
-
-    public static final String[] TABS_PROJECTION_COLUMNS = new String[] {
-                                                                BrowserContract.Tabs.TITLE,
-                                                                BrowserContract.Tabs.URL,
-                                                                BrowserContract.Clients.GUID,
-                                                                BrowserContract.Clients.NAME,
-                                                                BrowserContract.Clients.LAST_MODIFIED,
-                                                                BrowserContract.Clients.DEVICE_TYPE,
-                                                            };
-
-    private static final String LOCAL_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NULL";
-    private static final String REMOTE_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NOT NULL";
-
-    private static final String REMOTE_TABS_SORT_ORDER =
-            // Most recently synced clients first.
-            BrowserContract.Clients.LAST_MODIFIED + " DESC, " +
-            // If two clients somehow had the same last modified time, this will
-            // group them (arbitrarily).
-            BrowserContract.Clients.GUID + " DESC, " +
-            // Within a single client, most recently used tabs first.
-            BrowserContract.Tabs.LAST_USED + " DESC";
-
-    private static final String LOCAL_CLIENT_SELECTION = BrowserContract.Clients.GUID + " IS NULL";
-
-    private static final Pattern FILTERED_URL_PATTERN = Pattern.compile("^(about|chrome|wyciwyg|file):");
-
-    /**
-     * A thin representation of a remote client.
-     * <p>
-     * We use the hash of the client's GUID as the ID in
-     * {@link RemoteTabsExpandableListAdapter#getGroupId(int)}.
-     */
-    public static class RemoteClient implements Parcelable {
-        public final String guid;
-        public final String name;
-        public final long lastModified;
-        public final String deviceType;
-        public final ArrayList<RemoteTab> tabs;
-
-        public RemoteClient(String guid, String name, long lastModified, String deviceType) {
-            this.guid = guid;
-            this.name = name;
-            this.lastModified = lastModified;
-            this.deviceType = deviceType;
-            this.tabs = new ArrayList<RemoteTab>();
-        }
-
-        @Override
-        public int describeContents() {
-            return 0;
-        }
-
-        @Override
-        public void writeToParcel(Parcel parcel, int flags) {
-            parcel.writeString(guid);
-            parcel.writeString(name);
-            parcel.writeLong(lastModified);
-            parcel.writeString(deviceType);
-            parcel.writeTypedList(tabs);
-        }
-
-        public static final Creator<RemoteClient> CREATOR = new Creator<RemoteClient>() {
-            @Override
-            public RemoteClient createFromParcel(final Parcel source) {
-                final String guid = source.readString();
-                final String name = source.readString();
-                final long lastModified = source.readLong();
-                final String deviceType = source.readString();
-
-                final RemoteClient client = new RemoteClient(guid, name, lastModified, deviceType);
-                source.readTypedList(client.tabs, RemoteTab.CREATOR);
-
-                return client;
-            }
-
-            @Override
-            public RemoteClient[] newArray(final int size) {
-                return new RemoteClient[size];
-            }
-        };
-    }
-
-    /**
-     * A thin representation of a remote tab.
-     * <p>
-     * We use the hash of the tab as the ID in
-     * {@link RemoteTabsExpandableListAdapter#getClientId(int)}, and therefore we
-     * must implement equality as well. These are generated functions.
-     */
-    public static class RemoteTab implements Parcelable {
-        public final String title;
-        public final String url;
-
-        public RemoteTab(String title, String url) {
-            this.title = title;
-            this.url = url;
-        }
-
-        @Override
-        public int describeContents() {
-            return 0;
-        }
-
-        @Override
-        public void writeToParcel(Parcel parcel, int flags) {
-            parcel.writeString(title);
-            parcel.writeString(url);
-        }
-
-        public static final Creator<RemoteTab> CREATOR = new Creator<RemoteTab>() {
-            @Override
-            public RemoteTab createFromParcel(final Parcel source) {
-                final String title = source.readString();
-                final String url = source.readString();
+import org.mozilla.gecko.Tab;
 
-                return new RemoteTab(title, url);
-            }
-
-            @Override
-            public RemoteTab[] newArray(final int size) {
-                return new RemoteTab[size];
-            }
-        };
-
-        @Override
-        public int hashCode() {
-            final int prime = 31;
-            int result = 1;
-            result = prime * result + ((title == null) ? 0 : title.hashCode());
-            result = prime * result + ((url == null) ? 0 : url.hashCode());
-            return result;
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (this == obj) {
-                return true;
-            }
-            if (obj == null) {
-                return false;
-            }
-            if (getClass() != obj.getClass()) {
-                return false;
-            }
-            RemoteTab other = (RemoteTab) obj;
-            if (title == null) {
-                if (other.title != null) {
-                    return false;
-                }
-            } else if (!title.equals(other.title)) {
-                return false;
-            }
-            if (url == null) {
-                if (other.url != null) {
-                    return false;
-                }
-            } else if (!url.equals(other.url)) {
-                return false;
-            }
-            return true;
-        }
-    }
+import java.util.List;
 
-    /**
-     * Extract client and tab records from a cursor.
-     * <p>
-     * The position of the cursor is moved to before the first record before
-     * reading. The cursor is advanced until there are no more records to be
-     * read. The position of the cursor is restored before returning.
-     *
-     * @param cursor
-     *            to extract records from. The records should already be grouped
-     *            by client GUID.
-     * @return list of clients, each containing list of tabs.
-     */
-    public static List<RemoteClient> getClientsFromCursor(final Cursor cursor) {
-        final ArrayList<RemoteClient> clients = new ArrayList<TabsAccessor.RemoteClient>();
-
-        final int originalPosition = cursor.getPosition();
-        try {
-            if (!cursor.moveToFirst()) {
-                return clients;
-            }
-
-            final int tabTitleIndex = cursor.getColumnIndex(BrowserContract.Tabs.TITLE);
-            final int tabUrlIndex = cursor.getColumnIndex(BrowserContract.Tabs.URL);
-            final int clientGuidIndex = cursor.getColumnIndex(BrowserContract.Clients.GUID);
-            final int clientNameIndex = cursor.getColumnIndex(BrowserContract.Clients.NAME);
-            final int clientLastModifiedIndex = cursor.getColumnIndex(BrowserContract.Clients.LAST_MODIFIED);
-            final int clientDeviceTypeIndex = cursor.getColumnIndex(BrowserContract.Clients.DEVICE_TYPE);
-
-            // A walking partition, chunking by client GUID. We assume the
-            // cursor records are already grouped by client GUID; see the query
-            // sort order.
-            RemoteClient lastClient = null;
-            while (!cursor.isAfterLast()) {
-                final String clientGuid = cursor.getString(clientGuidIndex);
-                if (lastClient == null || !TextUtils.equals(lastClient.guid, clientGuid)) {
-                    final String clientName = cursor.getString(clientNameIndex);
-                    final long lastModified = cursor.getLong(clientLastModifiedIndex);
-                    final String deviceType = cursor.getString(clientDeviceTypeIndex);
-                    lastClient = new RemoteClient(clientGuid, clientName, lastModified, deviceType);
-                    clients.add(lastClient);
-                }
-
-                final String tabTitle = cursor.getString(tabTitleIndex);
-                final String tabUrl = cursor.getString(tabUrlIndex);
-                lastClient.tabs.add(new RemoteTab(tabTitle, tabUrl));
-
-                cursor.moveToNext();
-            }
-        } finally {
-            cursor.moveToPosition(originalPosition);
-        }
-
-        return clients;
-    }
-
-    public static Cursor getRemoteTabsCursor(Context context) {
-        return getRemoteTabsCursor(context, -1);
-    }
-
-    public static Cursor getRemoteTabsCursor(Context context, int limit) {
-        Uri uri = BrowserContract.Tabs.CONTENT_URI;
-
-        if (limit > 0) {
-            uri = uri.buildUpon()
-                     .appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit))
-                     .build();
-        }
-
-        final Cursor cursor =  context.getContentResolver().query(uri,
-                                                            TABS_PROJECTION_COLUMNS,
-                                                            REMOTE_TABS_SELECTION,
-                                                            null,
-                                                            REMOTE_TABS_SORT_ORDER);
-        return cursor;
-    }
-
+public interface TabsAccessor {
     public interface OnQueryTabsCompleteListener {
         public void onQueryTabsComplete(List<RemoteClient> clients);
     }
 
-    // This method returns all tabs from all remote clients,
-    // ordered by most recent client first, most recent tab first
-    public static void getTabs(final Context context, final OnQueryTabsCompleteListener listener) {
-        getTabs(context, 0, listener);
-    }
-
-    // This method returns limited number of tabs from all remote clients,
-    // ordered by most recent client first, most recent tab first
-    public static void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener) {
-        // If there is no listener, no point in doing work.
-        if (listener == null)
-            return;
-
-        (new UIAsyncTask.WithoutParams<List<RemoteClient>>(ThreadUtils.getBackgroundHandler()) {
-            @Override
-            protected List<RemoteClient> doInBackground() {
-                final Cursor cursor = getRemoteTabsCursor(context, limit);
-                if (cursor == null)
-                    return null;
-
-                try {
-                    return Collections.unmodifiableList(getClientsFromCursor(cursor));
-                } finally {
-                    cursor.close();
-                }
-            }
-
-            @Override
-            protected void onPostExecute(List<RemoteClient> clients) {
-                listener.onQueryTabsComplete(clients);
-            }
-        }).execute();
-    }
-
-    // Updates the modified time of the local client with the current time.
-    private static void updateLocalClient(final ContentResolver cr) {
-        ContentValues values = new ContentValues();
-        values.put(BrowserContract.Clients.LAST_MODIFIED, System.currentTimeMillis());
-        cr.update(BrowserContract.Clients.CONTENT_URI, values, LOCAL_CLIENT_SELECTION, null);
-    }
-
-    // Deletes all local tabs.
-    private static void deleteLocalTabs(final ContentResolver cr) {
-        cr.delete(BrowserContract.Tabs.CONTENT_URI, LOCAL_TABS_SELECTION, null);
-    }
-
-    /**
-     * Tabs are positioned in the DB in the same order that they appear in the tabs param.
-     *   - URL should never empty or null. Skip this tab if there's no URL.
-     *   - TITLE should always a string, either a page title or empty.
-     *   - LAST_USED should always be numeric.
-     *   - FAVICON should be a URL or null.
-     *   - HISTORY should be serialized JSON array of URLs.
-     *   - POSITION should always be numeric.
-     *   - CLIENT_GUID should always be null to represent the local client.
-     */
-    private static void insertLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs) {
-        // Reuse this for serializing individual history URLs as JSON.
-        JSONArray history = new JSONArray();
-        ArrayList<ContentValues> valuesToInsert = new ArrayList<ContentValues>();
-
-        int position = 0;
-        for (Tab tab : tabs) {
-            // Skip this tab if it has a null URL or is in private browsing mode, or is a filtered URL.
-            String url = tab.getURL();
-            if (url == null || tab.isPrivate() || isFilteredURL(url))
-                continue;
-
-            ContentValues values = new ContentValues();
-            values.put(BrowserContract.Tabs.URL, url);
-            values.put(BrowserContract.Tabs.TITLE, tab.getTitle());
-            values.put(BrowserContract.Tabs.LAST_USED, tab.getLastUsed());
-
-            String favicon = tab.getFaviconURL();
-            if (favicon != null)
-                values.put(BrowserContract.Tabs.FAVICON, favicon);
-            else
-                values.putNull(BrowserContract.Tabs.FAVICON);
-
-            // We don't have access to session history in Java, so for now, we'll
-            // just use a JSONArray that holds most recent history item.
-            try {
-                history.put(0, tab.getURL());
-                values.put(BrowserContract.Tabs.HISTORY, history.toString());
-            } catch (JSONException e) {
-                Log.w(LOGTAG, "JSONException adding URL to tab history array.", e);
-            }
-
-            values.put(BrowserContract.Tabs.POSITION, position++);
-
-            // A null client guid corresponds to the local client.
-            values.putNull(BrowserContract.Tabs.CLIENT_GUID);
-
-            valuesToInsert.add(values);
-        }
-
-        ContentValues[] valuesToInsertArray = valuesToInsert.toArray(new ContentValues[valuesToInsert.size()]);
-        cr.bulkInsert(BrowserContract.Tabs.CONTENT_URI, valuesToInsertArray);
-    }
-
-    // Deletes all local tabs and replaces them with a new list of tabs.
-    public static synchronized void persistLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs) {
-        deleteLocalTabs(cr);
-        insertLocalTabs(cr, tabs);
-        updateLocalClient(cr);
-    }
-
-    /**
-     * Matches the supplied URL string against the set of URLs to filter.
-     *
-     * @return true if the supplied URL should be skipped; false otherwise.
-     */
-    private static boolean isFilteredURL(String url) {
-        return FILTERED_URL_PATTERN.matcher(url).lookingAt();
-    }
-
-    /**
-     * Return a relative "Last synced" time span for the given tab record.
-     *
-     * @param now local time.
-     * @param time to format string for.
-     * @return string describing time span
-     */
-    public static String getLastSyncedString(Context context, long now, long time) {
-        final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
-        return context.getResources().getString(R.string.remote_tabs_last_synced, relativeTimeSpanString);
-    }
+    public Cursor getRemoteTabsCursor(Context context);
+    public Cursor getRemoteTabsCursor(Context context, int limit);
+    public List<RemoteClient> getClientsFromCursor(final Cursor cursor);
+    public void getTabs(final Context context, final OnQueryTabsCompleteListener listener);
+    public void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener);
+    public void persistLocalTabs(final ContentResolver cr, final Iterable<Tab> tabs);
+    public String getLastSyncedString(Context context, long now, long time);
 }
--- a/mobile/android/base/db/URLMetadata.java
+++ b/mobile/android/base/db/URLMetadata.java
@@ -1,212 +1,23 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */
 /* 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.Bookmarks;
-import org.mozilla.gecko.db.BrowserContract.History;
-import org.mozilla.gecko.util.ThreadUtils;
-import org.mozilla.gecko.Telemetry;
+import android.content.ContentResolver;
+import android.database.Cursor;
 
 import org.json.JSONObject;
 
-import android.content.ContentValues;
-import android.content.ContentResolver;
-import android.database.Cursor;
-import android.net.Uri;
-import android.support.v4.util.LruCache;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
-// Holds metadata info about urls. Supports some helper functions for getting back a HashMap of key value data.
-public class URLMetadata {
-    private static final String LOGTAG = "GeckoURLMetadata";
-
-    // This returns a list of columns in the table. It's used to simplify some loops for reading/writing data.
-    @SuppressWarnings("serial")
-    private static final Set<String> getModel() {
-        return new HashSet<String>() {{
-            add(URLMetadataTable.URL_COLUMN);
-            add(URLMetadataTable.TILE_IMAGE_URL_COLUMN);
-            add(URLMetadataTable.TILE_COLOR_COLUMN);
-        }};
-    }
-
-    // Store a cache of recent results. This number is chosen to match the max number of tiles on about:home
-    private static final int CACHE_SIZE = 9;
-    // Note: Members of this cache are unmodifiable.
-    private static final LruCache<String, Map<String, Object>> cache = new LruCache<String, Map<String, Object>>(CACHE_SIZE);
-
-    /**
-     * Converts a JSON object into a unmodifiable Map of known metadata properties.
-     * Will throw away any properties that aren't stored in the database.
-     */
-    public static Map<String, Object> fromJSON(JSONObject obj) {
-        Map<String, Object> data = new HashMap<String, Object>();
-
-        Set<String> model = getModel();
-        for (String key : model) {
-            if (obj.has(key)) {
-                data.put(key, obj.optString(key));
-            }
-        }
-
-        return Collections.unmodifiableMap(data);
-    }
-
-    /**
-     * Converts a Cursor into a unmodifiable Map of known metadata properties.
-     * Will throw away any properties that aren't stored in the database.
-     * Will also not iterate through multiple rows in the cursor.
-     */
-    private static Map<String, Object> fromCursor(Cursor c) {
-        Map<String, Object> data = new HashMap<String, Object>();
-
-        Set<String> model = getModel();
-        String[] columns = c.getColumnNames();
-        for (String column : columns) {
-            if (model.contains(column)) {
-                try {
-                    data.put(column, c.getString(c.getColumnIndexOrThrow(column)));
-                } catch (Exception ex) {
-                    Log.i(LOGTAG, "Error getting data for " + column, ex);
-                }
-            }
-        }
-
-        return Collections.unmodifiableMap(data);
-    }
-
-    /**
-     * Returns an unmodifiable Map of url->Metadata (i.e. A second HashMap) for a list of urls.
-     * Must not be called from UI or Gecko threads.
-     */
-    public static Map<String, Map<String, Object>> getForUrls(final ContentResolver cr,
-                                                              final List<String> urls,
-                                                              final List<String> columns) {
-        ThreadUtils.assertNotOnUiThread();
-        ThreadUtils.assertNotOnGeckoThread();
-
-        final Map<String, Map<String, Object>> data = new HashMap<String, Map<String, Object>>();
-
-        // Nothing to query for
-        if (urls.isEmpty() || columns.isEmpty()) {
-            Log.e(LOGTAG, "Queried metadata for nothing");
-            return data;
-        }
-
-        // Search the cache for any of these urls
-        List<String> urlsToQuery = new ArrayList<String>();
-        for (String url : urls) {
-            final Map<String, Object> hit = cache.get(url);
-            if (hit != null) {
-                // Cache hit!
-                data.put(url, hit);
-            } else {
-                urlsToQuery.add(url);
-            }
-        }
-
-        Telemetry.addToHistogram("FENNEC_TILES_CACHE_HIT", data.size());
-
-        // If everything was in the cache, we're done!
-        if (urlsToQuery.size() == 0) {
-            return Collections.unmodifiableMap(data);
-        }
-
-        final String selection = DBUtils.computeSQLInClause(urlsToQuery.size(), URLMetadataTable.URL_COLUMN);
-        // We need the url to build our final HashMap, so we force it to be included in the query.
-        if (!columns.contains(URLMetadataTable.URL_COLUMN)) {
-            columns.add(URLMetadataTable.URL_COLUMN);
-        }
-
-        final Cursor cursor = cr.query(URLMetadataTable.CONTENT_URI,
-                                       columns.toArray(new String[columns.size()]), // columns,
-                                       selection, // selection
-                                       urlsToQuery.toArray(new String[urlsToQuery.size()]), // selectionargs
-                                       null);
-        try {
-            if (!cursor.moveToFirst()) {
-                return Collections.unmodifiableMap(data);
-            }
-
-            do {
-                final Map<String, Object> metadata = fromCursor(cursor);
-                final String url = cursor.getString(cursor.getColumnIndexOrThrow(URLMetadataTable.URL_COLUMN));
-
-                data.put(url, metadata);
-                cache.put(url, metadata);
-            } while(cursor.moveToNext());
-
-        } finally {
-            cursor.close();
-        }
-
-        return Collections.unmodifiableMap(data);
-    }
-
-    /**
-     * Saves a HashMap of metadata into the database. Will iterate through columns
-     * in the Database and only save rows with matching keys in the HashMap.
-     * Must not be called from UI or Gecko threads.
-     */
-    public static void save(final ContentResolver cr, final String url, final Map<String, Object> data) {
-        ThreadUtils.assertNotOnUiThread();
-        ThreadUtils.assertNotOnGeckoThread();
-
-        try {
-            ContentValues values = new ContentValues();
-
-            Set<String> model = getModel();
-            for (String key : model) {
-                if (data.containsKey(key)) {
-                    values.put(key, (String) data.get(key));
-                }
-            }
-
-            if (values.size() == 0) {
-                return;
-            }
-
-            Uri uri = URLMetadataTable.CONTENT_URI.buildUpon()
-                                 .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
-                                 .build();
-            cr.update(uri, values, URLMetadataTable.URL_COLUMN + "=?", new String[] {
-                (String) data.get(URLMetadataTable.URL_COLUMN)
-            });
-        } catch (Exception ex) {
-            Log.e(LOGTAG, "error saving", ex);
-        }
-    }
-
-    public static int deleteUnused(final ContentResolver cr, final String profile) {
-        final String selection = URLMetadataTable.URL_COLUMN + " NOT IN "
-                + "(SELECT " + History.URL
-                + " FROM " + History.TABLE_NAME
-                + " WHERE " + History.IS_DELETED + " = 0"
-                + " UNION "
-                + " SELECT " + Bookmarks.URL
-                + " FROM " + Bookmarks.TABLE_NAME
-                + " WHERE " + Bookmarks.IS_DELETED + " = 0 "
-                + " AND " + Bookmarks.URL + " IS NOT NULL)";
-
-        Uri uri = URLMetadataTable.CONTENT_URI;
-        if (!TextUtils.isEmpty(profile)) {
-            uri = uri.buildUpon()
-                     .appendQueryParameter(BrowserContract.PARAM_PROFILE, profile)
-                     .build();
-        }
-
-        return cr.delete(uri, selection, null);
-    }
+public interface URLMetadata {
+    public Map<String, Object> fromJSON(JSONObject obj);
+    public Map<String, Map<String, Object>> getForURLs(final ContentResolver cr,
+                                                       final List<String> urls,
+                                                       final List<String> columns);
+    public void save(final ContentResolver cr, final String url, final Map<String, Object> data);
 }
--- a/mobile/android/base/db/URLMetadataTable.java
+++ b/mobile/android/base/db/URLMetadataTable.java
@@ -1,19 +1,20 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- */
 /* 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.Bookmarks;
+import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.Telemetry;
-
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import android.database.sqlite.SQLiteDatabase;
 import android.net.Uri;
 import android.util.Log;
 
 import java.util.List;
@@ -23,17 +24,17 @@ import java.util.HashSet;
 // Holds metadata info about urls. Supports some helper functions for getting back a HashMap of key value data.
 public class URLMetadataTable extends BaseTable {
     private static final String LOGTAG = "GeckoURLMetadataTable";
 
     private static final String TABLE = "metadata"; // Name of the table in the db
     private static final int TABLE_ID_NUMBER = 1200;
 
     // Uri for querying this table
-    public static final Uri CONTENT_URI = Uri.withAppendedPath(BrowserContract.AUTHORITY_URI, "metadata");
+    static final Uri CONTENT_URI = Uri.withAppendedPath(BrowserContract.AUTHORITY_URI, "metadata");
 
     // Columns in the table
     public static final String ID_COLUMN = "id";
     public static final String URL_COLUMN = "url";
     public static final String TILE_IMAGE_URL_COLUMN = "tileImage";
     public static final String TILE_COLOR_COLUMN = "tileColor";
 
     URLMetadataTable() { }
@@ -64,9 +65,23 @@ public class URLMetadataTable extends Ba
     }
 
     @Override
     public Table.ContentProviderInfo[] getContentProviderInfo() {
         return new Table.ContentProviderInfo[] {
             new Table.ContentProviderInfo(TABLE_ID_NUMBER, TABLE)
         };
     }
+
+    public int deleteUnused(final SQLiteDatabase db) {
+        final String selection = URL_COLUMN + " NOT IN " +
+                                 "(SELECT " + History.URL +
+                                 " FROM " + History.TABLE_NAME +
+                                 " WHERE " + History.IS_DELETED + " = 0" +
+                                 " UNION " +
+                                 " SELECT " + Bookmarks.URL +
+                                 " FROM " + Bookmarks.TABLE_NAME +
+                                 " WHERE " + Bookmarks.IS_DELETED + " = 0 " +
+                                 " AND " + Bookmarks.URL + " IS NOT NULL)";
+
+        return db.delete(getTable(), selection, null);
+    }
 }
--- a/mobile/android/base/favicons/Favicons.java
+++ b/mobile/android/base/favicons/Favicons.java
@@ -294,35 +294,36 @@ public class Favicons {
     public static int getSizedFaviconForPageFromLocal(Context context, final String pageURL, final OnFaviconLoadedListener callback) {
         return getSizedFaviconForPageFromLocal(context, pageURL, defaultFaviconSize, callback);
     }
 
     /**
      * Helper method to determine the URL of the Favicon image for a given page URL by querying the
      * history database. Should only be called from the background thread - does database access.
      *
+     * @param db The LocalBrowserDB to use when accessing favicons.
+     * @param cr A ContentResolver to run queries through.
      * @param pageURL The URL of a webpage with a Favicon.
      * @return The URL of the Favicon used by that webpage, according to either the History database
      *         or a somewhat educated guess.
      */
-    public static String getFaviconURLForPageURL(Context context, String pageURL) {
+    public static String getFaviconURLForPageURL(final BrowserDB db, final ContentResolver cr, final String pageURL) {
         // Attempt to determine the Favicon URL from the Tabs datastructure. Can dodge having to use
         // the database sometimes by doing this.
         String targetURL;
         Tab theTab = Tabs.getInstance().getFirstTabForUrl(pageURL);
         if (theTab != null) {
             targetURL = theTab.getFaviconURL();
             if (targetURL != null) {
                 return targetURL;
             }
         }
 
         // Try to find the faviconURL in the history and/or bookmarks table.
-        final ContentResolver resolver = context.getContentResolver();
-        targetURL = BrowserDB.getFaviconURLFromPageURL(resolver, pageURL);
+        targetURL = db.getFaviconURLFromPageURL(cr, pageURL);
         if (targetURL != null) {
             return targetURL;
         }
 
         // If we still can't find it, fall back to the default URL and hope for the best.
         return guessDefaultFaviconURL(pageURL);
     }
 
--- a/mobile/android/base/favicons/LoadFaviconTask.java
+++ b/mobile/android/base/favicons/LoadFaviconTask.java
@@ -11,16 +11,17 @@ import android.graphics.Bitmap;
 import android.net.http.AndroidHttpClient;
 import android.text.TextUtils;
 import android.util.Log;
 import org.apache.http.Header;
 import org.apache.http.HttpEntity;
 import org.apache.http.HttpResponse;
 import org.apache.http.client.methods.HttpGet;
 import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
 import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
 import org.mozilla.gecko.util.GeckoJarReader;
 import org.mozilla.gecko.util.IOUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import java.io.IOException;
@@ -56,16 +57,17 @@ public class LoadFaviconTask {
 
     private static final AtomicInteger nextFaviconLoadId = new AtomicInteger(0);
     private final Context context;
     private final int id;
     private final String pageUrl;
     private String faviconURL;
     private final OnFaviconLoadedListener listener;
     private final int flags;
+    private final BrowserDB db;
 
     private final boolean onlyFromLocal;
     volatile boolean mCancelled;
 
     // Assuming square favicons, judging by width only is acceptable.
     protected int targetWidth;
     private LinkedList<LoadFaviconTask> chainees;
     private boolean isChaining;
@@ -76,42 +78,43 @@ public class LoadFaviconTask {
         this(context, pageURL, faviconURL, flags, listener, -1, false);
     }
 
     public LoadFaviconTask(Context context, String pageURL, String faviconURL, int flags, OnFaviconLoadedListener listener,
                            int targetWidth, boolean onlyFromLocal) {
         id = nextFaviconLoadId.incrementAndGet();
 
         this.context = context;
+        db = GeckoProfile.get(context).getDB();
         this.pageUrl = pageURL;
         this.faviconURL = faviconURL;
         this.listener = listener;
         this.flags = flags;
         this.targetWidth = targetWidth;
         this.onlyFromLocal = onlyFromLocal;
     }
 
     // Runs in background thread
-    private LoadFaviconResult loadFaviconFromDb() {
+    private LoadFaviconResult loadFaviconFromDb(final BrowserDB db) {
         ContentResolver resolver = context.getContentResolver();
-        return BrowserDB.getFaviconForFaviconUrl(resolver, faviconURL);
+        return db.getFaviconForUrl(resolver, faviconURL);
     }
 
     // Runs in background thread
-    private void saveFaviconToDb(final byte[] encodedFavicon) {
+    private void saveFaviconToDb(final BrowserDB db, final byte[] encodedFavicon) {
         if (encodedFavicon == null) {
             return;
         }
 
         if ((flags & FLAG_PERSIST) == 0) {
             return;
         }
 
         ContentResolver resolver = context.getContentResolver();
-        BrowserDB.updateFaviconForUrl(resolver, pageUrl, encodedFavicon, faviconURL);
+        db.updateFaviconForUrl(resolver, pageUrl, encodedFavicon, faviconURL);
     }
 
     /**
      * Helper method for trying the download request to grab a Favicon.
      * @param faviconURI URL of Favicon to try and download
      * @return The HttpResponse containing the downloaded Favicon if successful, null otherwise.
      */
     private HttpResponse tryDownload(URI faviconURI) throws URISyntaxException, IOException {
@@ -296,17 +299,17 @@ public class LoadFaviconTask {
 
     // LoadFaviconTasks are performed on a unique background executor thread
     // to avoid network blocking.
     public final void execute() {
         try {
             Favicons.longRunningExecutor.execute(new Runnable() {
                 @Override
                 public void run() {
-                    final Bitmap result = doInBackground();
+                    final Bitmap result = doInBackground(db);
 
                     ThreadUtils.getUiHandler().post(new Runnable() {
                        @Override
                         public void run() {
                             if (mCancelled) {
                                 onCancelled();
                             } else {
                                 onPostExecute(result);
@@ -325,17 +328,17 @@ public class LoadFaviconTask {
         mCancelled = true;
         return true;
     }
 
     public final boolean isCancelled() {
         return mCancelled;
     }
 
-    Bitmap doInBackground() {
+    Bitmap doInBackground(final BrowserDB db) {
         if (isCancelled()) {
             return null;
         }
 
         // Attempt to decode the favicon URL as a data URL. We don't bother storing such URIs in
         // the database: the cost of decoding them here probably doesn't exceed the cost of mucking
         // about with the DB.
         final boolean isEmpty = TextUtils.isEmpty(faviconURL);
@@ -348,21 +351,22 @@ public class LoadFaviconTask {
 
         String storedFaviconUrl;
         boolean isUsingDefaultURL = false;
 
         // Handle the case of malformed favicon URL.
         // If favicon is empty, fall back to the stored one.
         if (isEmpty) {
             // Try to get the favicon URL from the memory cache.
+            final ContentResolver cr = context.getContentResolver();
             storedFaviconUrl = Favicons.getFaviconURLForPageURLFromCache(pageUrl);
 
             // If that failed, try to get the URL from the database.
             if (storedFaviconUrl == null) {
-                storedFaviconUrl = Favicons.getFaviconURLForPageURL(context, pageUrl);
+                storedFaviconUrl = Favicons.getFaviconURLForPageURL(db, cr, pageUrl);
                 if (storedFaviconUrl != null) {
                     // If that succeeded, cache the URL loaded from the database in memory.
                     Favicons.putFaviconURLForPageURLInCache(pageUrl, storedFaviconUrl);
                 }
             }
 
             // If we found a faviconURL - use it.
             if (storedFaviconUrl != null) {
@@ -408,17 +412,17 @@ public class LoadFaviconTask {
             loadsInFlight.put(faviconURL, this);
         }
 
         if (isCancelled()) {
             return null;
         }
 
         // If there are no valid bitmaps decoded, the returned LoadFaviconResult is null.
-        LoadFaviconResult loadedBitmaps = loadFaviconFromDb();
+        LoadFaviconResult loadedBitmaps = loadFaviconFromDb(db);
         if (loadedBitmaps != null) {
             return pushToCacheAndGetResult(loadedBitmaps);
         }
 
         if (onlyFromLocal || isCancelled()) {
             return null;
         }
 
@@ -438,17 +442,17 @@ public class LoadFaviconTask {
         } catch (Exception e) {
             Log.e(LOGTAG, "Couldn't download favicon.", e);
         }
 
         if (loadedBitmaps != null) {
             // Fetching bytes to store can fail. saveFaviconToDb will
             // do the right thing, but we still choose to cache the
             // downloaded icon in memory.
-            saveFaviconToDb(loadedBitmaps.getBytesForDatabaseStorage());
+            saveFaviconToDb(db, loadedBitmaps.getBytesForDatabaseStorage());
             return pushToCacheAndGetResult(loadedBitmaps);
         }
 
         if (isUsingDefaultURL) {
             Favicons.putFaviconInFailedCache(faviconURL);
             return null;
         }
 
@@ -473,17 +477,17 @@ public class LoadFaviconTask {
         try {
             loadedBitmaps = downloadFavicon(new URI(guessed));
         } catch (Exception e) {
             // Not interesting. It was an educated guess, anyway.
             return null;
         }
 
         if (loadedBitmaps != null) {
-            saveFaviconToDb(loadedBitmaps.getBytesForDatabaseStorage());
+            saveFaviconToDb(db, loadedBitmaps.getBytesForDatabaseStorage());
             return pushToCacheAndGetResult(loadedBitmaps);
         }
 
         return null;
     }
 
     /**
      * Helper method to put the result of a favicon load into the memory cache and then query the
--- a/mobile/android/base/gfx/BitmapUtils.java
+++ b/mobile/android/base/gfx/BitmapUtils.java
@@ -6,16 +6,17 @@
 package org.mozilla.gecko.gfx;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.reflect.Field;
 import java.net.MalformedURLException;
 import java.net.URL;
 
+import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.util.GeckoJarReader;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UIAsyncTask;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.ThumbnailHelper;
 
@@ -147,17 +148,18 @@ public final class BitmapUtils {
                  @Override
                  public void onTabChanged(Tab t, Tabs.TabEvents msg, Object data) {
                      if (tab == t && msg == Tabs.TabEvents.THUMBNAIL) {
                          Tabs.unregisterOnTabsChangedListener(this);
                          runOnBitmapFoundOnUiThread(loader, t.getThumbnail());
                      }
                  }
              });
-         ThumbnailHelper.getInstance().getAndProcessThumbnailFor(tab);
+         final GeckoProfile profile = GeckoProfile.get(context);
+         ThumbnailHelper.getInstance().getAndProcessThumbnailFor(tab, profile.getDB());
     }
 
     public static Bitmap decodeByteArray(byte[] bytes) {
         return decodeByteArray(bytes, null);
     }
 
     public static Bitmap decodeByteArray(byte[] bytes, BitmapFactory.Options options) {
         return decodeByteArray(bytes, 0, bytes.length, options);
--- a/mobile/android/base/home/BookmarksPanel.java
+++ b/mobile/android/base/home/BookmarksPanel.java
@@ -2,29 +2,29 @@
  * 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.home;
 
 import java.util.List;
 
+import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
-import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.home.BookmarksListAdapter.FolderInfo;
 import org.mozilla.gecko.home.BookmarksListAdapter.OnRefreshFolderListener;
 import org.mozilla.gecko.home.BookmarksListAdapter.RefreshType;
 import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 
 import android.app.Activity;
 import android.content.Context;
 import android.content.res.Configuration;
-import android.content.res.Resources;
 import android.database.Cursor;
 import android.os.Bundle;
 import android.support.v4.app.LoaderManager.LoaderCallbacks;
 import android.support.v4.content.Loader;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
@@ -171,41 +171,41 @@ public class BookmarksPanel extends Home
     }
 
     /**
      * Loader for the list for bookmarks.
      */
     private static class BookmarksLoader extends SimpleCursorLoader {
         private final FolderInfo mFolderInfo;
         private final RefreshType mRefreshType;
+        private final BrowserDB mDB;
 
         public BookmarksLoader(Context context) {
-            super(context);
-            final Resources res = context.getResources();
-            final String title = res.getString(R.string.bookmarks_title);
-            mFolderInfo = new FolderInfo(Bookmarks.FIXED_ROOT_ID, title);
-            mRefreshType = RefreshType.CHILD;
+            this(context,
+                 new FolderInfo(Bookmarks.FIXED_ROOT_ID, context.getResources().getString(R.string.bookmarks_title)),
+                 RefreshType.CHILD);
         }
 
         public BookmarksLoader(Context context, FolderInfo folderInfo, RefreshType refreshType) {
             super(context);
             mFolderInfo = folderInfo;
             mRefreshType = refreshType;
+            mDB = GeckoProfile.get(context).getDB();
         }
 
         @Override
         public Cursor loadCursor() {
-            return BrowserDB.getBookmarksInFolder(getContext().getContentResolver(), mFolderInfo.id);
+            return mDB.getBookmarksInFolder(getContext().getContentResolver(), mFolderInfo.id);
         }
 
         @Override
         public void onContentChanged() {
             // Invalidate the cached value that keeps track of whether or
             // not desktop bookmarks exist.
-            BrowserDB.invalidateCachedState();
+            mDB.invalidate();
             super.onContentChanged();
         }
 
         public FolderInfo getFolderInfo() {
             return mFolderInfo;
         }
 
         public RefreshType getRefreshType() {
--- a/mobile/android/base/home/HistoryPanel.java
+++ b/mobile/android/base/home/HistoryPanel.java
@@ -8,16 +8,17 @@ package org.mozilla.gecko.home;
 import java.util.Date;
 import java.util.EnumSet;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
+import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
@@ -186,25 +187,27 @@ public class HistoryPanel extends HomeFr
     @Override
     protected void load() {
         getLoaderManager().initLoader(LOADER_ID_HISTORY, null, mCursorLoaderCallbacks);
     }
 
     private static class HistoryCursorLoader extends SimpleCursorLoader {
         // Max number of history results
         private static final int HISTORY_LIMIT = 100;
+        private final BrowserDB mDB;
 
         public HistoryCursorLoader(Context context) {
             super(context);
+            mDB = GeckoProfile.get(context).getDB();
         }
 
         @Override
         public Cursor loadCursor() {
             final ContentResolver cr = getContext().getContentResolver();
-            return BrowserDB.getRecentHistory(cr, HISTORY_LIMIT);
+            return mDB.getRecentHistory(cr, HISTORY_LIMIT);
         }
     }
 
     private void updateUiFromCursor(Cursor c) {
         if (c != null && c.getCount() > 0) {
             mClearHistoryButton.setVisibility(View.VISIBLE);
             return;
         }
--- a/mobile/android/base/home/HomeFragment.java
+++ b/mobile/android/base/home/HomeFragment.java
@@ -12,18 +12,18 @@ import org.json.JSONObject;
 import org.mozilla.gecko.EditBookmarkDialog;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.ReaderModeUtils;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.BrowserContract.SuggestedSites;
-import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.ThreadUtils;
@@ -321,52 +321,54 @@ public abstract class HomeFragment exten
         mIsLoaded = true;
     }
 
     protected static class RemoveItemByUrlTask extends UIAsyncTask.WithoutParams<Void> {
         private final Context mContext;
         private final String mUrl;
         private final RemoveItemType mType;
         private final int mPosition;
+        private final BrowserDB mDB;
 
         /**
          * Remove bookmark/history/reading list type item by url, and also unpin the
          * Top Sites grid item at index <code>position</code>.
          */
         public RemoveItemByUrlTask(Context context, String url, RemoveItemType type, int position) {
             super(ThreadUtils.getBackgroundHandler());
 
             mContext = context;
             mUrl = url;
             mType = type;
             mPosition = position;
+            mDB = GeckoProfile.get(context).getDB();
         }
 
         @Override
         public Void doInBackground() {
             ContentResolver cr = mContext.getContentResolver();
 
             if (mPosition > -1) {
-                BrowserDB.unpinSite(cr, mPosition);
-                if (BrowserDB.hideSuggestedSite(mUrl)) {
+                mDB.unpinSite(cr, mPosition);
+                if (mDB.hideSuggestedSite(mUrl)) {
                     cr.notifyChange(SuggestedSites.CONTENT_URI, null);
                 }
             }
 
             switch(mType) {
                 case BOOKMARKS:
-                    BrowserDB.removeBookmarksWithURL(cr, mUrl);
+                    mDB.removeBookmarksWithURL(cr, mUrl);
                     break;
 
                 case HISTORY:
-                    BrowserDB.removeHistoryEntry(cr, mUrl);
+                    mDB.removeHistoryEntry(cr, mUrl);
                     break;
 
                 case READING_LIST:
-                    BrowserDB.removeReadingListItemWithURL(cr, mUrl);
+                    mDB.removeReadingListItemWithURL(cr, mUrl);
                     GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:Removed", mUrl));
                     break;
 
                 default:
                     Log.e(LOGTAG, "Can't remove item type " + mType.toString());
                     break;
             }
             return null;
--- a/mobile/android/base/home/PinSiteDialog.java
+++ b/mobile/android/base/home/PinSiteDialog.java
@@ -3,17 +3,16 @@
  * 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.home;
 
 import java.util.EnumSet;
 
 import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserContract.URLColumns;
 import org.mozilla.gecko.db.BrowserDB.FilterFlags;
 import org.mozilla.gecko.util.StringUtils;
 
 import android.content.Context;
 import android.database.Cursor;
 import android.os.Bundle;
 import android.support.v4.app.DialogFragment;
--- a/mobile/android/base/home/ReadingListPanel.java
+++ b/mobile/android/base/home/ReadingListPanel.java
@@ -2,23 +2,24 @@
  * 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.home;
 
 import java.util.EnumSet;
 
+import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.ReaderModeUtils;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
 import org.mozilla.gecko.db.BrowserContract.URLColumns;
-import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 
 import android.content.Context;
 import android.database.Cursor;
 import android.os.Bundle;
 import android.support.v4.app.LoaderManager.LoaderCallbacks;
 import android.support.v4.content.Loader;
@@ -159,23 +160,26 @@ public class ReadingListPanel extends Ho
             mList.setEmptyView(mEmptyView);
         }
     }
 
     /**
      * Cursor loader for the list of reading list items.
      */
     private static class ReadingListLoader extends SimpleCursorLoader {
+        private final BrowserDB mDB;
+
         public ReadingListLoader(Context context) {
             super(context);
+            mDB = GeckoProfile.get(context).getDB();
         }
 
         @Override
         public Cursor loadCursor() {
-            return BrowserDB.getReadingList(getContext().getContentResolver());
+            return mDB.getReadingList(getContext().getContentResolver());
         }
     }
 
     /**
      * Cursor adapter for the list of reading list items.
      */
     private class ReadingListAdapter extends CursorAdapter {
         public ReadingListAdapter(Context context, Cursor cursor) {
--- a/mobile/android/base/home/RemoteTabsExpandableListFragment.java
+++ b/mobile/android/base/home/RemoteTabsExpandableListFragment.java
@@ -5,25 +5,26 @@
 
 package org.mozilla.gecko.home;
 
 import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.Iterator;
 import java.util.List;
 
+import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.RemoteClientsDialogFragment;
 import org.mozilla.gecko.RemoteClientsDialogFragment.ChoiceMode;
 import org.mozilla.gecko.RemoteClientsDialogFragment.RemoteClientsListener;
 import org.mozilla.gecko.RemoteTabsExpandableListAdapter;
-import org.mozilla.gecko.TabsAccessor;
-import org.mozilla.gecko.TabsAccessor.RemoteClient;
-import org.mozilla.gecko.TabsAccessor.RemoteTab;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.RemoteClient;
+import org.mozilla.gecko.db.RemoteTab;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.widget.GeckoSwipeRefreshLayout;
 import org.mozilla.gecko.widget.GeckoSwipeRefreshLayout.OnRefreshListener;
 
 import android.accounts.Account;
@@ -377,35 +378,41 @@ public class RemoteTabsExpandableListFra
     }
 
     @Override
     protected void load() {
         getLoaderManager().initLoader(LOADER_ID_REMOTE_TABS, null, mCursorLoaderCallbacks);
     }
 
     private static class RemoteTabsCursorLoader extends SimpleCursorLoader {
+        private final GeckoProfile mProfile;
+
         public RemoteTabsCursorLoader(Context context) {
             super(context);
+            mProfile = GeckoProfile.get(context);
         }
 
         @Override
         public Cursor loadCursor() {
-            return TabsAccessor.getRemoteTabsCursor(getContext());
+            return mProfile.getDB().getTabsAccessor().getRemoteTabsCursor(getContext());
         }
     }
 
     private class CursorLoaderCallbacks extends TransitionAwareCursorLoaderCallbacks {
+        private BrowserDB mDB;    // Pseudo-final: set in onCreateLoader.
+
         @Override
         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+            mDB = GeckoProfile.get(getActivity()).getDB();
             return new RemoteTabsCursorLoader(getActivity());
         }
 
         @Override
         public void onLoadFinishedAfterTransitions(Loader<Cursor> loader, Cursor c) {
-            final List<RemoteClient> clients = TabsAccessor.getClientsFromCursor(c);
+            final List<RemoteClient> clients = mDB.getTabsAccessor().getClientsFromCursor(c);
 
             // Filter the hidden clients out of the clients list. The clients
             // list is updated in place; the hidden clients list is built
             // incrementally.
             mHiddenClients.clear();
             final Iterator<RemoteClient> it = clients.iterator();
             while (it.hasNext()) {
                 final RemoteClient client = it.next();
--- a/mobile/android/base/home/SearchLoader.java
+++ b/mobile/android/base/home/SearchLoader.java
@@ -2,18 +2,18 @@
  * 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.home;
 
 import java.util.EnumSet;
 
+import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.BrowserDB.FilterFlags;
 
 import android.content.Context;
 import android.database.Cursor;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.support.v4.app.LoaderManager;
 import android.support.v4.app.LoaderManager.LoaderCallbacks;
@@ -73,35 +73,37 @@ class SearchLoader {
                                EnumSet<FilterFlags> flags) {
         final Bundle args = createArgs(searchTerm, flags);
         manager.restartLoader(loaderId, args, callbacks);
     }
 
     public static class SearchCursorLoader extends SimpleCursorLoader {
         private static final String TELEMETRY_HISTOGRAM_LOAD_CURSOR = "FENNEC_SEARCH_LOADER_TIME_MS";
 
-        // Max number of search results
+        // Max number of search results.
         private static final int SEARCH_LIMIT = 100;
 
-        // The target search term associated with the loader
+        // The target search term associated with the loader.
         private final String mSearchTerm;
 
-        // The filter flags associated with the loader
+        // The filter flags associated with the loader.
         private final EnumSet<FilterFlags> mFlags;
+        private final GeckoProfile mProfile;
 
         public SearchCursorLoader(Context context, String searchTerm, EnumSet<FilterFlags> flags) {
             super(context);
             mSearchTerm = searchTerm;
             mFlags = flags;
+            mProfile = GeckoProfile.get(context);
         }
 
         @Override
         public Cursor loadCursor() {
             final long start = SystemClock.uptimeMillis();
-            final Cursor cursor = BrowserDB.filter(getContext().getContentResolver(), mSearchTerm, SEARCH_LIMIT, mFlags);
+            final Cursor cursor = mProfile.getDB().filter(getContext().getContentResolver(), mSearchTerm, SEARCH_LIMIT, mFlags);
             final long end = SystemClock.uptimeMillis();
             final long took = end - start;
             Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_LOAD_CURSOR, (int) Math.min(took, Integer.MAX_VALUE));
             return cursor;
         }
 
         public String getSearchTerm() {
             return mSearchTerm;
--- a/mobile/android/base/home/TopSitesPanel.java
+++ b/mobile/android/base/home/TopSitesPanel.java
@@ -20,17 +20,16 @@ import org.mozilla.gecko.Locales;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserContract.Thumbnails;
 import org.mozilla.gecko.db.BrowserContract.TopSites;
 import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.db.URLMetadata;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.home.HomeContextMenuInfo.RemoveItemType;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.PinSiteDialog.OnSiteSelectedListener;
 import org.mozilla.gecko.home.TopSitesGridView.OnEditPinnedSiteListener;
 import org.mozilla.gecko.home.TopSitesGridView.TopSitesGridContextMenuInfo;
@@ -287,27 +286,28 @@ public class TopSitesPanel extends HomeF
 
         registerForContextMenu(mList);
         registerForContextMenu(mGrid);
     }
 
     private List<Tile> getTilesSnapshot() {
         final int count = mGrid.getCount();
         final ArrayList<Tile> snapshot = new ArrayList<>();
+        final BrowserDB db = GeckoProfile.get(getActivity()).getDB();
         for (int i = 0; i < count; i++) {
             final Cursor cursor = (Cursor) mGrid.getItemAtPosition(i);
             final int type = cursor.getInt(cursor.getColumnIndexOrThrow(TopSites.TYPE));
 
             if (type == TopSites.TYPE_BLANK) {
                 snapshot.add(null);
                 continue;
             }
 
             final String url = cursor.getString(cursor.getColumnIndexOrThrow(TopSites.URL));
-            final int id = BrowserDB.getTrackingIdForUrl(url);
+            final int id = db.getTrackingIdForUrl(url);
             final boolean pinned = (type == TopSites.TYPE_PINNED);
             snapshot.add(new Tile(id, pinned));
         }
         return snapshot;
     }
 
     @Override
     public void onDestroyView() {
@@ -395,42 +395,43 @@ public class TopSitesPanel extends HomeF
 
         if (!(menuInfo instanceof TopSitesGridContextMenuInfo)) {
             return false;
         }
 
         TopSitesGridContextMenuInfo info = (TopSitesGridContextMenuInfo) menuInfo;
 
         final int itemId = item.getItemId();
+        final BrowserDB db = GeckoProfile.get(getActivity()).getDB();
 
         if (itemId == R.id.top_sites_pin) {
             final String url = info.url;
             final String title = info.title;
             final int position = info.position;
             final Context context = getActivity().getApplicationContext();
 
             ThreadUtils.postToBackgroundThread(new Runnable() {
                 @Override
                 public void run() {
-                    BrowserDB.pinSite(context.getContentResolver(), url, title, position);
+                    db.pinSite(context.getContentResolver(), url, title, position);
                 }
             });
 
             Telemetry.sendUIEvent(TelemetryContract.Event.PIN);
             return true;
         }
 
         if (itemId == R.id.top_sites_unpin) {
             final int position = info.position;
             final Context context = getActivity().getApplicationContext();
 
             ThreadUtils.postToBackgroundThread(new Runnable() {
                 @Override
                 public void run() {
-                    BrowserDB.unpinSite(context.getContentResolver(), position);
+                    db.unpinSite(context.getContentResolver(), position);
                 }
             });
 
             Telemetry.sendUIEvent(TelemetryContract.Event.UNPIN);
 
             return true;
         }
 
@@ -483,20 +484,21 @@ public class TopSitesPanel extends HomeF
                 dialog.show(manager, TAG_PIN_SITE);
             }
         }
 
         @Override
         public void onSiteSelected(final String url, final String title) {
             final int position = mPosition;
             final Context context = getActivity().getApplicationContext();
+            final BrowserDB db = GeckoProfile.get(getActivity()).getDB();
             ThreadUtils.postToBackgroundThread(new Runnable() {
                 @Override
                 public void run() {
-                    BrowserDB.pinSite(context.getContentResolver(), url, title, position);
+                    db.pinSite(context.getContentResolver(), url, title, position);
                 }
             });
         }
     }
 
     private void updateUiFromCursor(Cursor c) {
         mList.setHeaderDividersEnabled(c != null && c.getCount() > mMaxGridEntries);
     }
@@ -510,27 +512,29 @@ public class TopSitesPanel extends HomeF
         // Gecko to normal priority.
         ThreadUtils.resetGeckoPriority();
     }
 
     private static class TopSitesLoader extends SimpleCursorLoader {
         // Max number of search results.
         private static final int SEARCH_LIMIT = 30;
         private static final String TELEMETRY_HISTOGRAM_LOAD_CURSOR = "FENNEC_TOPSITES_LOADER_TIME_MS";
+        private final BrowserDB mDB;
         private final int mMaxGridEntries;
 
         public TopSitesLoader(Context context) {
             super(context);
             mMaxGridEntries = context.getResources().getInteger(R.integer.number_of_top_sites);
+            mDB = GeckoProfile.get(context).getDB();
         }
 
         @Override
         public Cursor loadCursor() {
             final long start = SystemClock.uptimeMillis();
-            final Cursor cursor = BrowserDB.getTopSites(getContext().getContentResolver(), mMaxGridEntries, SEARCH_LIMIT);
+            final Cursor cursor = mDB.getTopSites(getContext().getContentResolver(), mMaxGridEntries, SEARCH_LIMIT);
             final long end = SystemClock.uptimeMillis();
             final long took = end - start;
             Telemetry.addToHistogram(TELEMETRY_HISTOGRAM_LOAD_CURSOR, (int) Math.min(took, Integer.MAX_VALUE));
             return cursor;
         }
     }
 
     private class VisitedAdapter extends CursorAdapter {
@@ -559,22 +563,24 @@ public class TopSitesPanel extends HomeF
 
         @Override
         public View newView(Context context, Cursor cursor, ViewGroup parent) {
             return LayoutInflater.from(context).inflate(R.layout.bookmark_item_row, parent, false);
         }
     }
 
     public class TopSitesGridAdapter extends CursorAdapter {
+        private final BrowserDB mDB;
         // Cache to store the thumbnails.
         // Ensure that this is only accessed from the UI thread.
         private Map<String, ThumbnailInfo> mThumbnailInfos;
 
         public TopSitesGridAdapter(Context context, Cursor cursor) {
             super(context, cursor, 0);
+            mDB = GeckoProfile.get(context).getDB();
         }
 
         @Override
         public int getCount() {
             return Math.min(mMaxGridEntries, super.getCount());
         }
 
         @Override
@@ -634,19 +640,19 @@ public class TopSitesPanel extends HomeF
                 return;
             }
 
             // Make sure we query suggested images without the user-entered wrapper.
             final String decodedUrl = StringUtils.decodeUserEnteredUrl(url);
 
             // Suggested images have precedence over thumbnails, no need to wait
             // for them to be loaded. See: CursorLoaderCallbacks.onLoadFinished()
-            final String imageUrl = BrowserDB.getSuggestedImageUrlForUrl(decodedUrl);
+            final String imageUrl = mDB.getSuggestedImageUrlForUrl(decodedUrl);
             if (!TextUtils.isEmpty(imageUrl)) {
-                final int bgColor = BrowserDB.getSuggestedBackgroundColorForUrl(decodedUrl);
+                final int bgColor = mDB.getSuggestedBackgroundColorForUrl(decodedUrl);
                 view.displayThumbnail(imageUrl, bgColor);
                 return;
             }
 
             // If thumbnails are still being loaded, don't try to load favicons
             // just yet. If we sent in a thumbnail, we're done now.
             if (mThumbnailInfos == null || thumbnail != null) {
                 return;
@@ -731,17 +737,18 @@ public class TopSitesPanel extends HomeF
 
             final ArrayList<String> urls = new ArrayList<String>();
             int i = 1;
             do {
                 final String url = c.getString(col);
 
                 // Only try to fetch thumbnails for non-empty URLs that
                 // don't have an associated suggested image URL.
-                if (TextUtils.isEmpty(url) || BrowserDB.hasSuggestedImageUrl(url)) {
+                final GeckoProfile profile = GeckoProfile.get(getActivity());
+                if (TextUtils.isEmpty(url) || profile.getDB().hasSuggestedImageUrl(url)) {
                     continue;
                 }
 
                 urls.add(url);
             } while (i++ < mMaxGridEntries && c.moveToNext());
 
             if (urls.isEmpty()) {
                 // Short-circuit empty results to the UI.
@@ -806,39 +813,41 @@ public class TopSitesPanel extends HomeF
         }
     }
 
     /**
      * An AsyncTaskLoader to load the thumbnails from a cursor.
      */
     @SuppressWarnings("serial")
     static class ThumbnailsLoader extends AsyncTaskLoader<Map<String, ThumbnailInfo>> {
+        private final BrowserDB mDB;
         private Map<String, ThumbnailInfo> mThumbnailInfos;
         private final ArrayList<String> mUrls;
 
         private static final ArrayList<String> COLUMNS = new ArrayList<String>() {{
             add(TILE_IMAGE_URL_COLUMN);
             add(TILE_COLOR_COLUMN);
         }};
 
         public ThumbnailsLoader(Context context, ArrayList<String> urls) {
             super(context);
             mUrls = urls;
+            mDB = GeckoProfile.get(context).getDB();
         }
 
         @Override
         public Map<String, ThumbnailInfo> loadInBackground() {
             final Map<String, ThumbnailInfo> thumbnails = new HashMap<String, ThumbnailInfo>();
             if (mUrls == null || mUrls.size() == 0) {
                 return thumbnails;
             }
 
             // Query the DB for tile images.
             final ContentResolver cr = getContext().getContentResolver();
-            final Map<String, Map<String, Object>> metadata = URLMetadata.getForUrls(cr, mUrls, COLUMNS);
+            final Map<String, Map<String, Object>> metadata = mDB.getURLMetadata().getForURLs(cr, mUrls, COLUMNS);
 
             // Keep a list of urls that don't have tiles images. We'll use thumbnails for them instead.
             final List<String> thumbnailUrls = new ArrayList<String>();
             for (String url : mUrls) {
                 ThumbnailInfo info = ThumbnailInfo.fromMetadata(metadata.get(url));
                 if (info == null) {
                     // If we didn't find metadata, we'll look for a thumbnail for this url.
                     thumbnailUrls.add(url);
@@ -848,17 +857,17 @@ public class TopSitesPanel extends HomeF
                 thumbnails.put(url, info);
             }
 
             if (thumbnailUrls.size() == 0) {
                 return thumbnails;
             }
 
             // Query the DB for tile thumbnails.
-            final Cursor cursor = BrowserDB.getThumbnailsForUrls(cr, thumbnailUrls);
+            final Cursor cursor = mDB.getThumbnailsForUrls(cr, thumbnailUrls);
             if (cursor == null) {
                 return thumbnails;
             }
 
             try {
                 final int urlIndex = cursor.getColumnIndexOrThrow(Thumbnails.URL);
                 final int dataIndex = cursor.getColumnIndexOrThrow(Thumbnails.DATA);
 
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -153,25 +153,33 @@ gbjar.sources += [
     '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/LocalSearches.java',
+    'db/LocalTabsAccessor.java',
+    'db/LocalURLMetadata.java',
     'db/PasswordsProvider.java',
     'db/PerProfileDatabaseProvider.java',
     'db/PerProfileDatabases.java',
     'db/ReadingListProvider.java',
+    'db/RemoteClient.java',
+    'db/RemoteTab.java',
+    'db/Searches.java',
     'db/SearchHistoryProvider.java',
     'db/SharedBrowserDatabaseProvider.java',
     'db/SQLiteBridgeContentProvider.java',
+    'db/StubBrowserDB.java',
     'db/SuggestedSites.java',
     'db/Table.java',
+    'db/TabsAccessor.java',
     'db/TabsProvider.java',
     'db/TopSitesCursorWrapper.java',
     'db/URLMetadata.java',
     'db/URLMetadataTable.java',
     'distribution/Distribution.java',
     'distribution/ReferrerDescriptor.java',
     'distribution/ReferrerReceiver.java',
     'DoorHangerPopup.java',
@@ -410,17 +418,16 @@ gbjar.sources += [
     'tabs/TabHistoryFragment.java',
     'tabs/TabHistoryItemRow.java',
     'tabs/TabHistoryPage.java',
     'tabs/TabsGridLayout.java',
     'tabs/TabsLayoutAdapter.java',
     'tabs/TabsLayoutItemView.java',
     'tabs/TabsListLayout.java',
     'tabs/TabsPanel.java',
-    'TabsAccessor.java',
     'Telemetry.java',
     'TelemetryContract.java',
     'TextSelection.java',
     'TextSelectionHandle.java',
     'ThumbnailHelper.java',
     'tiles/Tile.java',
     'tiles/TilesRecorder.java',
     'toolbar/ActionBarViewFlipper.java',
--- a/mobile/android/base/tests/DatabaseHelper.java
+++ b/mobile/android/base/tests/DatabaseHelper.java
@@ -8,16 +8,17 @@ import java.util.ArrayList;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.Assert;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.db.BrowserDB;
 
 import android.app.Activity;
 import android.content.ContentResolver;
+import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
 
 class DatabaseHelper {
     protected enum BrowserDataType {BOOKMARKS, HISTORY};
     private final Activity mActivity;
     private final Assert mAsserter;
 
@@ -25,17 +26,17 @@ class DatabaseHelper {
         mActivity = activity;
         mAsserter = asserter;
     }
     /**
     * This method can be used to check if an URL is present in the bookmarks database
     */
     protected boolean isBookmark(String url) {
         final ContentResolver resolver = mActivity.getContentResolver();
-        return BrowserDB.isBookmark(resolver, url);
+        return getProfileDB().isBookmark(resolver, url);
     }
 
     protected Uri buildUri(BrowserDataType dataType) {
         Uri uri = null;
         if (dataType == BrowserDataType.BOOKMARKS || dataType == BrowserDataType.HISTORY) {
             uri = Uri.parse("content://" + AppConstants.ANDROID_PACKAGE_NAME + ".db.browser/" + dataType.toString().toLowerCase());
         } else {
            mAsserter.ok(false, "The wrong data type has been provided = " + dataType.toString(), "Please provide the correct data type");
@@ -47,109 +48,123 @@ class DatabaseHelper {
 
     /**
      * Adds a bookmark, or updates the bookmark title if the url already exists.
      *
      * The LocalBrowserDB.addBookmark implementation handles updating existing bookmarks.
      */
     protected void addOrUpdateMobileBookmark(String title, String url) {
         final ContentResolver resolver = mActivity.getContentResolver();
-        BrowserDB.addBookmark(resolver, title, url);
+        getProfileDB().addBookmark(resolver, title, url);
         mAsserter.ok(true, "Inserting/updating a new bookmark", "Inserting/updating the bookmark with the title = " + title + " and the url = " + url);
     }
 
     /**
      * Updates the title and keyword of a bookmark with the given URL.
      *
      * Warning: This method assumes that there's only one bookmark with the given URL.
      */
     protected void updateBookmark(String url, String title, String keyword) {
         final ContentResolver resolver = mActivity.getContentResolver();
         // Get the id for the bookmark with the given URL.
         Cursor c = null;
         try {
-            c = BrowserDB.getBookmarkForUrl(resolver, url);
+            c = getProfileDB().getBookmarkForUrl(resolver, url);
             if (!c.moveToFirst()) {
                 mAsserter.ok(false, "Getting bookmark with url", "Couldn't find bookmark with url = " + url);
                 return;
             }
 
             int id = c.getInt(c.getColumnIndexOrThrow("_id"));
-            BrowserDB.updateBookmark(resolver, id, url, title, keyword);
+            getProfileDB().updateBookmark(resolver, id, url, title, keyword);
 
             mAsserter.ok(true, "Updating bookmark", "Updating bookmark with url = " + url);
         } finally {
             if (c != null) {
                 c.close();
             }
         }
     }
 
     protected void deleteBookmark(String url) {
         final ContentResolver resolver = mActivity.getContentResolver();
-        BrowserDB.removeBookmarksWithURL(resolver, url);
+        getProfileDB().removeBookmarksWithURL(resolver, url);
     }
 
     protected void deleteHistoryItem(String url) {
         final ContentResolver resolver = mActivity.getContentResolver();
-        BrowserDB.removeHistoryEntry(resolver, url);
+        getProfileDB().removeHistoryEntry(resolver, url);
     }
 
     // About the same implementation as getFolderIdFromGuid from LocalBrowserDB because it is declared private and we can't use reflections to access it
     protected long getFolderIdFromGuid(String guid) {
-        ContentResolver resolver = mActivity.getContentResolver();
+        final ContentResolver resolver = mActivity.getContentResolver();
         long folderId = -1L;
-        Uri bookmarksUri = buildUri(BrowserDataType.BOOKMARKS);
+        final Uri bookmarksUri = buildUri(BrowserDataType.BOOKMARKS);
+
         Cursor c = null;
         try {
             c = resolver.query(bookmarksUri,
                                new String[] { "_id" },
                                "guid = ?",
                                new String[] { guid },
                                null);
             if (c.moveToFirst()) {
                 folderId = c.getLong(c.getColumnIndexOrThrow("_id"));
             }
+
             if (folderId == -1) {
                 mAsserter.ok(false, "Trying to get the folder id" ,"We did not get the correct folder id");
             }
         } finally {
             if (c != null) {
                 c.close();
             }
         }
         return folderId;
     }
 
-     /**
-     * @param  a BrowserDataType value - either HISTORY or BOOKMARKS
-     * @return an ArrayList of the urls in the Firefox for Android Bookmarks or History databases
+    /**
+     * Returns all of the bookmarks or history entries in a database.
+     *
+     * @return an ArrayList of the urls in the Firefox for Android Bookmarks or History databases.
      */
     protected ArrayList<String> getBrowserDBUrls(BrowserDataType dataType) {
-        ArrayList<String> browserData = new ArrayList<String>();
-        ContentResolver resolver = mActivity.getContentResolver();
+        final ArrayList<String> browserData = new ArrayList<String>();
+        final ContentResolver resolver = mActivity.getContentResolver();
+
         Cursor cursor = null;
-        Uri uri = buildUri(dataType);
-        if (dataType == BrowserDataType.HISTORY) {
-            cursor = BrowserDB.getAllVisitedHistory(resolver);
-        } else if (dataType == BrowserDataType.BOOKMARKS) {
-            cursor = BrowserDB.getBookmarksInFolder(resolver, getFolderIdFromGuid("mobile"));
-        }
-        if (cursor != null) {
-            cursor.moveToFirst();
-            for (int i = 0; i < cursor.getCount(); i++ ) {
-                 // The url field may be null for folders in the structure of the Bookmarks table for Firefox so we should eliminate those
+        try {
+            if (dataType == BrowserDataType.HISTORY) {
+                cursor = getProfileDB().getAllVisitedHistory(resolver);
+            } else if (dataType == BrowserDataType.BOOKMARKS) {
+                cursor = getProfileDB().getBookmarksInFolder(resolver, getFolderIdFromGuid("mobile"));
+            }
+
+            if (cursor == null) {
+                mAsserter.ok(false, "We could not retrieve any data from the database", "The cursor was null");
+                return browserData;
+            }
+
+            if (!cursor.moveToFirst()) {
+                mAsserter.ok(false, "We could not move to the first item in the database", "moveToFirst failed");
+                return browserData;
+            }
+
+            do {
+                // The URL field may be null for folders in the structure of the Bookmarks table for Firefox. Eliminate those.
                 if (cursor.getString(cursor.getColumnIndex("url")) != null) {
                     browserData.add(cursor.getString(cursor.getColumnIndex("url")));
                 }
-                if(!cursor.isLast()) {
-                    cursor.moveToNext();
-                }
+            } while (cursor.moveToNext());
+        } finally {
+            if (cursor != null) {
+                cursor.close();
             }
-        } else {
-             mAsserter.ok(false, "We could not retrieve any data from the database", "The cursor was null");
         }
-        if (cursor != null) {
-            cursor.close();
-        }
+
         return browserData;
     }
+
+    protected BrowserDB getProfileDB() {
+        return GeckoProfile.get(mActivity).getDB();
+    }
 }
--- a/mobile/android/base/tests/testClearPrivateData.java
+++ b/mobile/android/base/tests/testClearPrivateData.java
@@ -24,16 +24,17 @@ public class testClearPrivateData extend
     }
 
     private void clearHistory() {
 
         // Loading a page and adding a second one as bookmark to have user made bookmarks and history
         String blank1 = getAbsoluteUrl(StringHelper.ROBOCOP_BLANK_PAGE_01_URL);
         String blank2 = getAbsoluteUrl(StringHelper.ROBOCOP_BLANK_PAGE_02_URL);
         String title = StringHelper.ROBOCOP_BLANK_PAGE_01_TITLE;
+
         inputAndLoadUrl(blank1);
         verifyUrlBarTitle(blank1);
         mDatabaseHelper.addOrUpdateMobileBookmark(StringHelper.ROBOCOP_BLANK_PAGE_02_TITLE, blank2);
 
         // Checking that the history list is not empty
         verifyHistoryCount(1);
 
         //clear and check for device
@@ -55,28 +56,30 @@ public class testClearPrivateData extend
         }, TEST_WAIT_MS);
         mAsserter.ok(match, "Checking that the number of history items is correct", String.valueOf(expectedCount) + " history items present in the database");
     }
 
     public void clearSiteSettings() {
         String shareStrings[] = {"Share your location with", "Share", "Don't share", "There are no settings to clear"};
         String titleGeolocation = StringHelper.ROBOCOP_GEOLOCATION_TITLE;
         String url = getAbsoluteUrl(StringHelper.ROBOCOP_GEOLOCATION_URL);
+
         loadCheckDismiss(shareStrings[1], url, shareStrings[0]);
         checkOption(shareStrings[1], "Clear");
         checkOption(shareStrings[3], "Cancel");
         loadCheckDismiss(shareStrings[2], url, shareStrings[0]);
         checkOption(shareStrings[2], "Cancel");
         checkDevice(titleGeolocation, url);
     }
 
     public void clearPassword(){
         String passwordStrings[] = {"Save password", "Save", "Don't save"};
         String title = StringHelper.ROBOCOP_BLANK_PAGE_01_TITLE;
         String loginUrl = getAbsoluteUrl(StringHelper.ROBOCOP_LOGIN_URL);
+
         loadCheckDismiss(passwordStrings[1], loginUrl, passwordStrings[0]);
         checkOption(passwordStrings[1], "Clear");
         loadCheckDismiss(passwordStrings[2], loginUrl, passwordStrings[0]);
         checkDevice(title, getAbsoluteUrl(StringHelper.ROBOCOP_BLANK_PAGE_01_URL));
     }
 
     // clear private data and verify the device type because for phone there is an extra back action to exit the settings menu
     public void checkDevice(final String title, final String url) {
@@ -105,13 +108,14 @@ public class testClearPrivateData extend
             final View toolbarView = mSolo.getView(R.id.browser_toolbar);
             mSolo.clickLongOnView(toolbarView);
             mAsserter.ok(waitForText(StringHelper.CONTEXT_MENU_ITEMS_IN_URL_BAR[2]), "Waiting for the pop-up to open", "Pop up was opened");
         } else {
             // Use the Page menu in 11+
             selectMenuItem(StringHelper.PAGE_LABEL);
             mAsserter.ok(waitForText(StringHelper.CONTEXT_MENU_ITEMS_IN_URL_BAR[2]), "Waiting for the submenu to open", "Submenu was opened");
         }
+
         mSolo.clickOnText(StringHelper.CONTEXT_MENU_ITEMS_IN_URL_BAR[2]);
         mAsserter.ok(waitForText(option), "Verify that the option: " + option + " is in the list", "The option is in the list. There are settings to clear");
         mSolo.clickOnButton(button);
     }
 }
--- a/mobile/android/base/tests/testDistribution.java
+++ b/mobile/android/base/tests/testDistribution.java
@@ -16,19 +16,19 @@ import java.util.jar.JarInputStream;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import org.mozilla.gecko.Actions;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.BrowserLocaleManager;
+import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.db.BrowserContract;
-import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.SuggestedSites;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.distribution.ReferrerDescriptor;
 import org.mozilla.gecko.distribution.ReferrerReceiver;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.app.Activity;
 import android.content.BroadcastReceiver;
@@ -130,17 +130,17 @@ public class testDistribution extends Co
         // Wait for any startup-related background distribution shenanigans to
         // finish. This reduces the chance of us racing with startup pref writes.
         waitForBackgroundHappiness();
 
         // Pre-clear distribution pref, override suggested sites and run tiles tests.
         clearDistributionPref();
         Distribution dist = initDistribution(mockPackagePath);
         SuggestedSites suggestedSites = new SuggestedSites(mActivity, dist);
-        BrowserDB.setSuggestedSites(suggestedSites);
+        GeckoProfile.get(mActivity).getDB().setSuggestedSites(suggestedSites);
 
         // Test tiles uploading for an en-US OS locale with no app locale.
         setOSLocale(Locale.US);
         checkTilesReporting("en-US");
 
         // Test tiles uploading for an es-MX OS locale with no app locale.
         setOSLocale(new Locale("es", "MX"));
         checkTilesReporting("es-MX");
--- a/mobile/android/base/tests/testFilterOpenTab.java
+++ b/mobile/android/base/tests/testFilterOpenTab.java
@@ -3,19 +3,19 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tests;
 
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Callable;
 
+import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.PrivateTab;
 import org.mozilla.gecko.Tab;
-import org.mozilla.gecko.TabsAccessor;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.TabsProvider;
 
 import android.content.ContentProvider;
 import android.content.Context;
 import android.database.Cursor;
 
 /**
@@ -101,18 +101,20 @@ public class testFilterOpenTab extends C
             Tab tab6 = createPrivateTab(6, URL1, false, 0, TITLE1);
             tabs.add(tab1);
             tabs.add(tab2);
             tabs.add(tab3);
             tabs.add(tab4);
             tabs.add(tab5);
             tabs.add(tab6);
 
-            // Persist the created tabs.
-            TabsAccessor.persistLocalTabs(mResolver, tabs);
+            // Persist the created tabs. Normally, you should be careful that you get a profile on the
+            // original thread, and do the work in a background one, but for testing we don't.
+            final DatabaseHelper helper = new DatabaseHelper(getActivity(), mAsserter);
+            helper.getProfileDB().getTabsAccessor().persistLocalTabs(mResolver, tabs);
 
             // Get the persisted tab and check if urls are filtered.
             Cursor c = getTabsFromLocalClient();
             assertCountIsAndClose(c, 1, 1 + " tabs entries found");
         }
     }
 
     /**
--- a/mobile/android/base/tests/testPrivateBrowsing.java
+++ b/mobile/android/base/tests/testPrivateBrowsing.java
@@ -46,17 +46,18 @@ public class testPrivateBrowsing extends
         mAsserter.ok(isTabPrivate(eventData), "Checking if the new tab opened from the context menu was a private tab", "The tab was a private tab");
         verifyTabCount(2);
 
         // Open a normal tab to check later that it was registered in the Firefox Browser History
         addTab(blank2Url, StringHelper.ROBOCOP_BLANK_PAGE_02_TITLE, false);
         verifyTabCount(2);
 
         // Get the history list and check that the links open in private browsing are not saved
-        ArrayList<String> firefoxHistory = mDatabaseHelper.getBrowserDBUrls(DatabaseHelper.BrowserDataType.HISTORY);
+        final ArrayList<String> firefoxHistory = mDatabaseHelper.getBrowserDBUrls(DatabaseHelper.BrowserDataType.HISTORY);
+
         mAsserter.ok(!firefoxHistory.contains(bigLinkUrl), "Check that the link opened in the first private tab was not saved", bigLinkUrl + " was not added to history");
         mAsserter.ok(!firefoxHistory.contains(blank1Url), "Check that the link opened in the private tab from the context menu was not saved", blank1Url + " was not added to history");
         mAsserter.ok(firefoxHistory.contains(blank2Url), "Check that the link opened in the normal tab was saved", blank2Url + " was added to history");
     }
 
     private boolean isTabPrivate(String eventData) {
         try {
             JSONObject data = new JSONObject(eventData);
--- a/mobile/android/base/tests/testThumbnails.java
+++ b/mobile/android/base/tests/testThumbnails.java
@@ -46,23 +46,26 @@ public class testThumbnails extends Base
         inputAndLoadUrl(StringHelper.ABOUT_HOME_URL);
         waitForTest(new ThumbnailTest(site1Title, Color.RED), 5000);
         mAsserter.is(getTopSiteThumbnailColor(site1Title), Color.RED, "Top site thumbnail updated for HTTP 200");
         waitForTest(new ThumbnailTest(site2Title, Color.GREEN), 5000);
         mAsserter.is(getTopSiteThumbnailColor(site2Title), Color.GREEN, "Top site thumbnail not updated for HTTP 404");
 
         // test dropping thumbnails
         final ContentResolver resolver = getActivity().getContentResolver();
+        final DatabaseHelper helper = new DatabaseHelper(getActivity(), mAsserter);
+        final BrowserDB db = helper.getProfileDB();
+
         // check that the thumbnail is non-null
-        byte[] thumbnailData = BrowserDB.getThumbnailForUrl(resolver, site1Url);
+        byte[] thumbnailData = db.getThumbnailForUrl(resolver, site1Url);
         mAsserter.ok(thumbnailData != null && thumbnailData.length > 0, "Checking for thumbnail data", "No thumbnail data found");
         // drop thumbnails
-        BrowserDB.removeThumbnails(resolver);
+        db.removeThumbnails(resolver);
         // check that the thumbnail is now null
-        thumbnailData = BrowserDB.getThumbnailForUrl(resolver, site1Url);
+        thumbnailData = db.getThumbnailForUrl(resolver, site1Url);
         mAsserter.ok(thumbnailData == null || thumbnailData.length == 0, "Checking for thumbnail data", "Thumbnail data found");
     }
 
     private class ThumbnailTest implements BooleanTest {
         private final String mTitle;
         private final int mColor;
 
         public ThumbnailTest(String title, int color) {
--- a/mobile/android/base/toolbar/BrowserToolbar.java
+++ b/mobile/android/base/toolbar/BrowserToolbar.java
@@ -7,17 +7,16 @@ package org.mozilla.gecko.toolbar;
 
 import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.List;
 
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.BrowserApp;
 import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoApplication;
 import org.mozilla.gecko.NewTabletUI;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener;
--- a/mobile/android/base/webapp/WebappImpl.java
+++ b/mobile/android/base/webapp/WebappImpl.java
@@ -13,16 +13,18 @@ import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.GeckoThread;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.StubBrowserDB;
 import org.mozilla.gecko.mozglue.ContextUtils.SafeIntent;
 import org.mozilla.gecko.util.NativeJSObject;
 import org.mozilla.gecko.webapp.InstallHelper.InstallCallback;
 
 import android.content.Intent;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.graphics.Color;
 import android.graphics.drawable.Drawable;
@@ -56,20 +58,27 @@ public class WebappImpl extends GeckoApp
 
     @Override
     public int getLayout() { return R.layout.web_app; }
 
     @Override
     public boolean hasTabsSideBar() { return false; }
 
     @Override
-    public void onCreate(Bundle savedInstance)
-    {
+    public BrowserDB.Factory getBrowserDBFactory() {
+        return new BrowserDB.Factory() {
+            @Override
+            public BrowserDB get(String profileName, File profileDir) {
+                return new StubBrowserDB(profileName);
+            }
+        };
+    }
 
-        String action = getIntent().getAction();
+    @Override
+    public void onCreate(Bundle savedInstance) {
         Bundle extras = getIntent().getExtras();
         if (extras == null) {
             extras = savedInstance;
         }
 
         if (extras == null) {
             extras = new Bundle();
         }