Bug 1131257 - Part 1: split LocalReadingListDB out of LocalBrowserDB. r=margaret
authorRichard Newman <rnewman@mozilla.com>
Tue, 10 Feb 2015 16:42:13 -0800
changeset 242176 b323b586b5e62fe83e77d3dbefc86b0dd950e96c
parent 242175 ea1ed091f31eb3dc92bcbb5d70d14a710c782346
child 242177 bcc03debfb8f306b73e0ff2bd98a043294b2c08d
push id649
push userwcosta@mozilla.com
push dateWed, 11 Feb 2015 16:57:44 +0000
reviewersmargaret
bugs1131257, 1130461
milestone38.0a1
Bug 1131257 - Part 1: split LocalReadingListDB out of LocalBrowserDB. r=margaret Centralizing reading list access logic will make Bug 1130461 much easier. This bug is the first part of that. We follow the same pattern as for URLMetadata, TabsAccessor, and Searches; BrowserDB hands over a single class that's specialized to handle the Reading List.
mobile/android/base/BrowserApp.java
mobile/android/base/ReadingListHelper.java
mobile/android/base/db/BrowserDB.java
mobile/android/base/db/LocalBrowserDB.java
mobile/android/base/db/LocalReadingListAccessor.java
mobile/android/base/db/ReadingListAccessor.java
mobile/android/base/db/StubBrowserDB.java
mobile/android/base/home/HomeFragment.java
mobile/android/base/home/ReadingListPanel.java
mobile/android/base/moz.build
mobile/android/base/overlays/service/sharemethods/AddToReadingList.java
mobile/android/base/overlays/ui/ShareDialog.java
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -1693,17 +1693,17 @@ public class BrowserApp extends GeckoApp
 
         } else if ("Telemetry:Gather".equals(event)) {
             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("FENNEC_READING_LIST_COUNT", db.getReadingListAccessor().getCount(cr));
             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)) {
--- a/mobile/android/base/ReadingListHelper.java
+++ b/mobile/android/base/ReadingListHelper.java
@@ -4,16 +4,17 @@
 
 package org.mozilla.gecko;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.DBUtils;
+import org.mozilla.gecko.db.ReadingListAccessor;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.NativeEventListener;
 import org.mozilla.gecko.util.NativeJSObject;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UIAsyncTask;
 
 import android.content.ContentResolver;
@@ -25,37 +26,36 @@ import android.net.Uri;
 import android.util.Log;
 import android.widget.Toast;
 
 public final class ReadingListHelper implements NativeEventListener {
     private static final String LOGTAG = "GeckoReadingListHelper";
 
     protected final Context context;
     private final BrowserDB db;
-
-    private final Uri readingListUriWithProfile;
+    private final ReadingListAccessor readingListAccessor;
     private final ContentObserver contentObserver;
 
     public ReadingListHelper(Context context, GeckoProfile profile) {
         this.context = context;
         this.db = profile.getDB();
+        this.readingListAccessor = db.getReadingListAccessor();
 
         EventDispatcher.getInstance().registerGeckoThreadListener((NativeEventListener) this,
             "Reader:AddToList", "Reader:UpdateList", "Reader:FaviconRequest", "Reader:ListStatusRequest", "Reader:RemoveFromList");
 
-        readingListUriWithProfile = DBUtils.appendProfile(profile.getName(), ReadingListItems.CONTENT_URI);
 
         contentObserver = new ContentObserver(null) {
             @Override
             public void onChange(boolean selfChange) {
                 fetchContent();
             }
         };
 
-        context.getContentResolver().registerContentObserver(readingListUriWithProfile, false, contentObserver);
+        this.readingListAccessor.registerContentObserver(context, contentObserver);
     }
 
     public void uninit() {
         EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) this,
             "Reader:AddToList", "Reader:UpdateList", "Reader:FaviconRequest", "Reader:ListStatusRequest", "Reader:RemoveFromList");
 
         context.getContentResolver().unregisterContentObserver(contentObserver);
     }
@@ -99,21 +99,21 @@ public final class ReadingListHelper imp
 
         // We can't access a NativeJSObject from the background thread, so we need to get the
         // values here, even if we may not use them to insert an item into the DB.
         final ContentValues values = getContentValues(message);
 
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
-                if (db.isReadingListItem(cr, url)) {
+                if (readingListAccessor.isReadingListItem(cr, url)) {
                     showToast(R.string.reading_list_duplicate, Toast.LENGTH_SHORT);
                     callback.sendError("URL already in reading list: " + url);
                 } else {
-                    db.addReadingListItem(cr, values);
+                    readingListAccessor.addReadingListItem(cr, values);
                     showToast(R.string.reading_list_added, Toast.LENGTH_SHORT);
                     callback.sendSuccess(url);
                 }
             }
         });
     }
 
     /**
@@ -121,17 +121,17 @@ public final class ReadingListHelper imp
      */
     private void handleUpdateList(final NativeJSObject message) {
         final ContentResolver cr = context.getContentResolver();
         final ContentValues values = getContentValues(message);
 
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
-                db.updateReadingListItem(cr, values);
+                readingListAccessor.updateReadingListItem(cr, values);
             }
         });
     }
 
     /**
      * Creates reading list item content values from JS message.
      */
     private ContentValues getContentValues(NativeJSObject message) {
@@ -187,32 +187,32 @@ public final class ReadingListHelper imp
     /**
      * 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) {
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
-                db.removeReadingListItemWithURL(context.getContentResolver(), url);
+                readingListAccessor.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) {
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
-                final int inReadingList = db.isReadingListItem(context.getContentResolver(), url) ? 1 : 0;
+                final int inReadingList = readingListAccessor.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);
                 }
@@ -234,17 +234,17 @@ public final class ReadingListHelper imp
             }
         });
     }
 
     private void fetchContent() {
         ThreadUtils.postToBackgroundThread(new Runnable() {
             @Override
             public void run() {
-                final Cursor c = db.getReadingListUnfetched(context.getContentResolver());
+                final Cursor c = readingListAccessor.getReadingListUnfetched(context.getContentResolver());
                 try {
                     while (c.moveToNext()) {
                         JSONObject json = new JSONObject();
                         try {
                             json.put("id", c.getInt(c.getColumnIndexOrThrow(ReadingListItems._ID)));
                             json.put("url", c.getString(c.getColumnIndexOrThrow(ReadingListItems.URL)));
                             GeckoAppShell.sendEventToGecko(
                                 GeckoEvent.createBroadcastEvent("Reader:FetchContent", json.toString()));
--- a/mobile/android/base/db/BrowserDB.java
+++ b/mobile/android/base/db/BrowserDB.java
@@ -11,17 +11,16 @@ import java.util.List;
 
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.db.BrowserContract.ExpirePriority;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.favicons.decoders.LoadFaviconResult;
 
 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;
 
 /**
  * Interface for interactions with all databases. If you want an instance
  * that implements this, you should go through GeckoProfile. E.g.,
@@ -38,16 +37,17 @@ public interface BrowserDB {
 
     public static enum FilterFlags {
         EXCLUDE_PINNED_SITES
     }
 
     public abstract Searches getSearches();
     public abstract TabsAccessor getTabsAccessor();
     public abstract URLMetadata getURLMetadata();
+    public abstract ReadingListAccessor getReadingListAccessor();
 
     /**
      * Add default bookmarks to the database.
      * Takes an offset; returns a new offset.
      */
     public abstract int addDefaultBookmarks(Context context, ContentResolver cr, int offset);
 
     /**
@@ -114,27 +114,16 @@ public interface BrowserDB {
     public abstract void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword);
 
     /**
      * Can return <code>null</code>.
      */
     public abstract Cursor getBookmarksInFolder(ContentResolver cr, long folderId);
 
     /**
-     * Can return <code>null</code>.
-     */
-    public abstract Cursor getReadingList(ContentResolver cr);
-    public abstract Cursor getReadingListUnfetched(ContentResolver cr);
-    public abstract boolean isReadingListItem(ContentResolver cr, String uri);
-    public abstract void addReadingListItem(ContentResolver cr, ContentValues values);
-    public abstract void updateReadingListItem(ContentResolver cr, ContentValues values);
-    public abstract void removeReadingListItemWithURL(ContentResolver cr, String uri);
-
-
-    /**
      * 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);
 
--- a/mobile/android/base/db/LocalBrowserDB.java
+++ b/mobile/android/base/db/LocalBrowserDB.java
@@ -93,21 +93,21 @@ public class LocalBrowserDB implements B
     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 LocalReadingListAccessor readingListAccessor;
 
     private static final String[] DEFAULT_BOOKMARK_COLUMNS =
             new String[] { Bookmarks._ID,
                            Bookmarks.GUID,
                            Bookmarks.URL,
                            Bookmarks.TITLE,
                            Bookmarks.TYPE,
                            Bookmarks.PARENT };
@@ -118,27 +118,27 @@ public class LocalBrowserDB implements B
 
         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();
 
         searches = new LocalSearches(mProfile);
         tabsAccessor = new LocalTabsAccessor(mProfile);
         urlMetadata = new LocalURLMetadata(mProfile);
+        readingListAccessor = new LocalReadingListAccessor(mProfile);
     }
 
     @Override
     public Searches getSearches() {
         return searches;
     }
 
     @Override
@@ -146,16 +146,21 @@ public class LocalBrowserDB implements B
         return tabsAccessor;
     }
 
     @Override
     public URLMetadata getURLMetadata() {
         return urlMetadata;
     }
 
+    @Override
+    public ReadingListAccessor getReadingListAccessor() {
+        return readingListAccessor;
+    }
+
     /**
      * 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;
         private final int increment;
 
@@ -571,19 +576,16 @@ public class LocalBrowserDB implements B
             // ignore folders, tags, keywords, separators, etc.
             constraint = Bookmarks.TYPE + " = " + Bookmarks.TYPE_BOOKMARK;
         } else if ("thumbnails".equals(database)) {
             uri = mThumbnailsUriWithProfile;
             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 {
@@ -815,36 +817,16 @@ public class LocalBrowserDB implements B
         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) {
-            Log.e(LOGTAG, "Null cursor in isReadingListItem");
-            return false;
-        }
-
-        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()) {
@@ -964,80 +946,16 @@ public class LocalBrowserDB implements B
 
         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 Cursor getReadingList(ContentResolver cr) {
-        return cr.query(mReadingListUriWithProfile,
-                        ReadingListItems.DEFAULT_PROJECTION,
-                        null,
-                        null,
-                        null);
-    }
-
-    @Override
-    public Cursor getReadingListUnfetched(ContentResolver cr) {
-        return cr.query(mReadingListUriWithProfile,
-                        new String[] { ReadingListItems._ID, ReadingListItems.URL },
-                        ReadingListItems.CONTENT_STATUS + " = " + ReadingListItems.STATUS_UNFETCHED,
-                        null,
-                        null);
-
-    }
-
-    @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);
-            }
-        }
-
-        // Clear delete flag if necessary
-        values.put(ReadingListItems.IS_DELETED, 0);
-
-        // Restore deleted record if possible
-        final Uri insertUri = mReadingListUriWithProfile
-                              .buildUpon()
-                              .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
-                              .build();
-
-        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 updateReadingListItem(ContentResolver cr, ContentValues values) {
-        if (!values.containsKey(ReadingListItems._ID)) {
-            throw new IllegalArgumentException("Cannot update reading list item without an ID");
-        }
-
-        final int updated = cr.update(mReadingListUriWithProfile,
-                                      values,
-                                      ReadingListItems._ID + " = ? ",
-                                      new String[] { values.getAsString(ReadingListItems._ID) });
-
-        debug("Updated " + updated + " reading list rows.");
-    }
-
-    @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();
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/LocalReadingListAccessor.java
@@ -0,0 +1,128 @@
+/* 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.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.net.Uri;
+import android.util.Log;
+
+public class LocalReadingListAccessor implements ReadingListAccessor {
+    private static final String LOG_TAG = "GeckoReadingListAcc";
+
+    private final Uri mReadingListUriWithProfile;
+
+    public LocalReadingListAccessor(final String profile) {
+        mReadingListUriWithProfile = DBUtils.appendProfile(profile, BrowserContract.ReadingListItems.CONTENT_URI);
+    }
+
+    @Override
+    public int getCount(ContentResolver cr) {
+        final String[] columns = new String[]{BrowserContract.ReadingListItems._ID};
+        final Cursor cursor = cr.query(mReadingListUriWithProfile, columns, null, null, null);
+        int count = 0;
+
+        try {
+            count = cursor.getCount();
+        } finally {
+            cursor.close();
+        }
+
+        Log.d(LOG_TAG, "Got count " + count + " for reading list.");
+        return count;
+    }
+
+    @Override
+    public Cursor getReadingList(ContentResolver cr) {
+        return cr.query(mReadingListUriWithProfile,
+                        BrowserContract.ReadingListItems.DEFAULT_PROJECTION,
+                        null,
+                        null,
+                        null);
+    }
+
+    @Override
+    public Cursor getReadingListUnfetched(ContentResolver cr) {
+        return cr.query(mReadingListUriWithProfile,
+                        new String[] { BrowserContract.ReadingListItems._ID, BrowserContract.ReadingListItems.URL },
+                        BrowserContract.ReadingListItems.CONTENT_STATUS + " = " + BrowserContract.ReadingListItems.STATUS_UNFETCHED,
+                        null,
+                        null);
+    }
+
+    @Override
+    public boolean isReadingListItem(ContentResolver cr, String uri) {
+        final Cursor c = cr.query(mReadingListUriWithProfile,
+                                  new String[] { BrowserContract.ReadingListItems._ID },
+                                  BrowserContract.ReadingListItems.URL + " = ? ",
+                                  new String[] { uri },
+                                  null);
+
+        if (c == null) {
+            Log.e(LOG_TAG, "Null cursor in isReadingListItem");
+            return false;
+        }
+
+        try {
+            return c.getCount() > 0;
+        } finally {
+            c.close();
+        }
+    }
+
+
+    @Override
+    public void addReadingListItem(ContentResolver cr, ContentValues values) {
+        // Check that required fields are present.
+        for (String field: BrowserContract.ReadingListItems.REQUIRED_FIELDS) {
+            if (!values.containsKey(field)) {
+                throw new IllegalArgumentException("Missing required field for reading list item: " + field);
+            }
+        }
+
+        // Clear delete flag if necessary
+        values.put(BrowserContract.ReadingListItems.IS_DELETED, 0);
+
+        // Restore deleted record if possible
+        final Uri insertUri = mReadingListUriWithProfile
+                .buildUpon()
+                .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
+                .build();
+
+        final int updated = cr.update(insertUri,
+                values,
+                BrowserContract.ReadingListItems.URL + " = ? ",
+                new String[] { values.getAsString(BrowserContract.ReadingListItems.URL) });
+
+        Log.d(LOG_TAG, "Updated " + updated + " rows to new modified time.");
+    }
+
+    @Override
+    public void updateReadingListItem(ContentResolver cr, ContentValues values) {
+        if (!values.containsKey(BrowserContract.ReadingListItems._ID)) {
+            throw new IllegalArgumentException("Cannot update reading list item without an ID");
+        }
+
+        final int updated = cr.update(mReadingListUriWithProfile,
+                                      values,
+                                      BrowserContract.ReadingListItems._ID + " = ? ",
+                                      new String[] { values.getAsString(BrowserContract.ReadingListItems._ID) });
+
+        Log.d(LOG_TAG, "Updated " + updated + " reading list rows.");
+    }
+
+    @Override
+    public void removeReadingListItemWithURL(ContentResolver cr, String uri) {
+        cr.delete(mReadingListUriWithProfile, BrowserContract.ReadingListItems.URL + " = ? ", new String[]{uri});
+    }
+
+    @Override
+    public void registerContentObserver(Context context, ContentObserver observer) {
+        context.getContentResolver().registerContentObserver(mReadingListUriWithProfile, false, observer);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/db/ReadingListAccessor.java
@@ -0,0 +1,32 @@
+/* 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.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+
+public interface ReadingListAccessor {
+    /**
+     * Can return <code>null</code>.
+     */
+    Cursor getReadingList(ContentResolver cr);
+
+    int getCount(ContentResolver cr);
+
+    Cursor getReadingListUnfetched(ContentResolver cr);
+
+    boolean isReadingListItem(ContentResolver cr, String uri);
+
+    void addReadingListItem(ContentResolver cr, ContentValues values);
+
+    void updateReadingListItem(ContentResolver cr, ContentValues values);
+
+    void removeReadingListItemWithURL(ContentResolver cr, String uri);
+
+    void registerContentObserver(Context context, ContentObserver observer);
+}
--- a/mobile/android/base/db/StubBrowserDB.java
+++ b/mobile/android/base/db/StubBrowserDB.java
@@ -21,16 +21,58 @@ import org.mozilla.gecko.mozglue.Robocop
 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 StubReadingListAccessor implements ReadingListAccessor {
+    @Override
+    public Cursor getReadingList(ContentResolver cr) {
+        return null;
+    }
+
+    @Override
+    public int getCount(ContentResolver cr) {
+        return 0;
+    }
+
+    @Override
+    public Cursor getReadingListUnfetched(ContentResolver cr) {
+        return null;
+    }
+
+    @Override
+    public boolean isReadingListItem(ContentResolver cr, String uri) {
+        return false;
+    }
+
+    @Override
+    public void addReadingListItem(ContentResolver cr, ContentValues values) {
+
+    }
+
+    @Override
+    public void updateReadingListItem(ContentResolver cr, ContentValues values) {
+
+    }
+
+    @Override
+    public void removeReadingListItemWithURL(ContentResolver cr, String uri) {
+
+    }
+
+    @Override
+    public void registerContentObserver(Context context, ContentObserver observer) {
+
+    }
+}
+
 class StubSearches implements Searches {
     public StubSearches() {
     }
 
     public void insert(ContentResolver cr, String query) {
     }
 }
 
@@ -86,32 +128,38 @@ class StubTabsAccessor implements TabsAc
 /*
  * 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();
+    private final StubReadingListAccessor readingListAccessor = new StubReadingListAccessor();
 
     @Override
     public Searches getSearches() {
         return searches;
     }
 
     @Override
     public TabsAccessor getTabsAccessor() {
         return tabsAccessor;
     }
 
     @Override
     public URLMetadata getURLMetadata() {
         return urlMetadata;
     }
 
+    @Override
+    public ReadingListAccessor getReadingListAccessor() {
+        return readingListAccessor;
+    }
+
     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) {
--- a/mobile/android/base/home/HomeFragment.java
+++ b/mobile/android/base/home/HomeFragment.java
@@ -358,17 +358,17 @@ public abstract class HomeFragment exten
                     mDB.removeBookmarksWithURL(cr, mUrl);
                     break;
 
                 case HISTORY:
                     mDB.removeHistoryEntry(cr, mUrl);
                     break;
 
                 case READING_LIST:
-                    mDB.removeReadingListItemWithURL(cr, mUrl);
+                    mDB.getReadingListAccessor().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/ReadingListPanel.java
+++ b/mobile/android/base/home/ReadingListPanel.java
@@ -10,16 +10,17 @@ 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.BrowserContract.ReadingListItems;
 import org.mozilla.gecko.db.BrowserContract.URLColumns;
 import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.ReadingListAccessor;
 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.content.Loader;
 import android.support.v4.widget.CursorAdapter;
@@ -159,26 +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;
+        private final ReadingListAccessor accessor;
 
         public ReadingListLoader(Context context) {
             super(context);
-            mDB = GeckoProfile.get(context).getDB();
+            accessor = GeckoProfile.get(context).getDB().getReadingListAccessor();
         }
 
         @Override
         public Cursor loadCursor() {
-            return mDB.getReadingList(getContext().getContentResolver());
+            return accessor.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/moz.build
+++ b/mobile/android/base/moz.build
@@ -154,22 +154,24 @@ 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/LocalReadingListAccessor.java',
     'db/LocalSearches.java',
     'db/LocalTabsAccessor.java',
     'db/LocalURLMetadata.java',
     'db/PasswordsProvider.java',
     'db/PerProfileDatabaseProvider.java',
     'db/PerProfileDatabases.java',
+    'db/ReadingListAccessor.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',
--- a/mobile/android/base/overlays/service/sharemethods/AddToReadingList.java
+++ b/mobile/android/base/overlays/service/sharemethods/AddToReadingList.java
@@ -29,17 +29,17 @@ public class AddToReadingList extends Sh
         ContentResolver resolver = context.getContentResolver();
 
         LocalBrowserDB browserDB = new LocalBrowserDB(GeckoProfile.DEFAULT_PROFILE);
 
         ContentValues values = new ContentValues();
         values.put(Bookmarks.TITLE, shareData.title);
         values.put(Bookmarks.URL, shareData.url);
 
-        browserDB.addReadingListItem(resolver, values);
+        browserDB.getReadingListAccessor().addReadingListItem(resolver, values);
 
         return Result.SUCCESS;
     }
 
     @Override
     public String getSuccessMessage() {
         return context.getResources().getString(R.string.reading_list_added);
     }
--- a/mobile/android/base/overlays/ui/ShareDialog.java
+++ b/mobile/android/base/overlays/ui/ShareDialog.java
@@ -256,17 +256,17 @@ public class ShareDialog extends Locales
             boolean isBookmark;
             boolean isReadingListItem;
 
             @Override
             protected Void doInBackground() {
                 final ContentResolver contentResolver = getApplicationContext().getContentResolver();
 
                 isBookmark = browserDB.isBookmark(contentResolver, pageURL);
-                isReadingListItem = browserDB.isReadingListItem(contentResolver, pageURL);
+                isReadingListItem = browserDB.getReadingListAccessor().isReadingListItem(contentResolver, pageURL);
 
                 return null;
             }
 
             @Override
             protected void onPostExecute(Void aVoid) {
                 findViewById(R.id.overlay_share_bookmark_btn).setEnabled(!isBookmark);
                 findViewById(R.id.overlay_share_reading_list_btn).setEnabled(!isReadingListItem);