Bug 1234315 - Introduce SavedReaderViewHelper to track cached reader view items r?nalexander draft
authorAndrzej Hunt <ahunt@mozilla.com>
Wed, 30 Mar 2016 10:26:43 -0700
changeset 345971 1775058f9170b88525f32362edcf8003b5b050bd
parent 345970 5e59fd8abc5bce8b99d3bd7fb1e3ebedfc79c578
child 345972 063d95b470c66b4d79a5b17a03fa8935482174ed
child 346025 3580529549db4350975aeab6b8866579d514bc15
push id14203
push userahunt@mozilla.com
push dateWed, 30 Mar 2016 17:53:06 +0000
reviewersnalexander
bugs1234315
milestone48.0a1
Bug 1234315 - Introduce SavedReaderViewHelper to track cached reader view items r?nalexander NOTE: there is a followup patch which changes how we initialise / load this list (direct descendant of this commit on reviewboard), which we will probably want to squash into this patch (assuming the new initialisation is satisfactory). MozReview-Commit-ID: CcylWpiAA3h
mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java
mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java
mobile/android/base/java/org/mozilla/gecko/reader/SavedReaderViewHelper.java
mobile/android/base/moz.build
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
@@ -468,17 +468,16 @@ public final class GeckoProfile {
         // 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;
         }
 
         boolean profileExists;
         synchronized (this) {
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java
@@ -201,9 +201,19 @@ public class LocalUrlAnnotations impleme
                         BrowserContract.UrlAnnotations.DATE_CREATED,
                 },
                 BrowserContract.UrlAnnotations.DATE_CREATED + " DESC");
     }
 
     public void insertScreenshot(final ContentResolver cr, final String pageUrl, final String screenshotPath) {
         insertAnnotation(cr, pageUrl, Key.SCREENSHOT.getDbValue(), screenshotPath);
     }
+
+    @Override
+    public void insertReaderviewUrl(final ContentResolver cr, final String pageUrl) {
+        insertAnnotation(cr, pageUrl, Key.READER_VIEW.getDbValue(), Boolean.toString(true));
+    }
+
+    @Override
+    public void deleteReaderviewUrl(ContentResolver cr, String pageURL) {
+        deleteAnnotation(cr, pageURL, Key.READER_VIEW);
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
@@ -185,16 +185,22 @@ class StubUrlAnnotations implements UrlA
     @Override
     public void insertFeedSubscription(ContentResolver cr, FeedSubscription subscription) {}
 
     @Override
     public boolean hasFeedUrlForWebsite(ContentResolver cr, String websiteUrl) { return false; }
 
     @Override
     public void insertFeedUrl(ContentResolver cr, String originUrl, String feedUrl) {}
+
+    @Override
+    public void insertReaderviewUrl(ContentResolver cr, String pageURL) {}
+
+    @Override
+    public void deleteReaderviewUrl(ContentResolver cr, String pageURL) {}
 }
 
 /*
  * 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();
--- a/mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java
@@ -20,9 +20,12 @@ public interface UrlAnnotations {
     void deleteFeedUrl(ContentResolver cr, String websiteUrl);
     boolean hasWebsiteForFeedUrl(ContentResolver cr, String feedUrl);
     void deleteFeedSubscription(ContentResolver cr, FeedSubscription subscription);
     void updateFeedSubscription(ContentResolver cr, FeedSubscription subscription);
     boolean hasFeedSubscription(ContentResolver cr, String feedUrl);
     void insertFeedSubscription(ContentResolver cr, FeedSubscription subscription);
     boolean hasFeedUrlForWebsite(ContentResolver cr, String websiteUrl);
     void insertFeedUrl(ContentResolver cr, String originUrl, String feedUrl);
+
+    void insertReaderviewUrl(ContentResolver cr, String pageURL);
+    void deleteReaderviewUrl(ContentResolver cr, String pageURL);
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/reader/SavedReaderViewHelper.java
@@ -0,0 +1,170 @@
+/* -*- 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.reader;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.db.UrlAnnotations;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Helper to keep track of items that are stored in the reader view cache. This is an in-memory list
+ * of the reader view items that are cached on disk. It is intended to allow quickly determining whether
+ * a given URL is in the cache, and also how many cached items there are.
+ *
+ * Currently we have 1:1 correspondence of reader view items (in the URL-annotations table)
+ * to cached items. This is _not_ a true cache, we never purge/cleanup items here - we only remove
+ * items when we un-reader-view/bookmark them. This is an acceptable model while we can guarantee the
+ * 1:1 correspondence.
+ *
+ * It isn't strictly necessary to mirror cached items in SQL at this stage, however it seems sensible
+ * to maintain URL anotations to avoid additional DB migrations in future.
+ * It is also simpler to implement the reading list smart-folder using the annotations (even if we do
+ * all other decoration from our in-memory cache record), as that is what we will need when
+ * we move away from the 1:1 correspondence.
+ *
+ * Bookmarks can be in one of two states - plain bookmark, or reader view bookmark that is also saved
+ * offline. We're hoping to introduce real cache management / cleanup in future, in which case a
+ * third user-visible state (reader view bookmark without a cache entry) will be added. However that logic is
+ * much more complicated and requires substantial changes in how we decorate reader view bookmarks.
+ * With the current 1:1 correspondence we can use this in-memory helper  to quickly decorate
+ * bookmarks (in all the various lists and panels that are used), whereas supporting
+ * the third state requires significant changes in order to allow joining with the
+ * url-annotations table wherever bookmarks might be retrieved (i.e. multiple homepanels, each with
+ * their own loaders and adapter).
+ *
+ * If/when cache cleanup and sync are implemented, url annotations will be the canonical record of
+ * user intent, and the cache will no longer represent all reader view bookmarks. We will have (A)
+ * cached items that are not a bookmark, or bookmarks without the reader view annotation (both of
+ * these would need purging), and (B) bookmarks with a reader view annotation, but not stored in
+ * the cache (which we might want to download in the background). Supporting (B) is currently difficult,
+ * see previous paragraph.
+ */
+public class SavedReaderViewHelper {
+    private static final String LOG_TAG = "SavedReaderViewHelper";
+
+    private static final String PATH = "path";
+    private static final String SIZE = "size";
+
+    private static final String DIRECTORY = "readercache";
+    private static final String FILE_NAME = "items.json";
+    private static final String FILE_PATH = DIRECTORY + "/" + FILE_NAME;
+
+    private final JSONObject mItems;
+
+    private final Context mContext;
+
+    private ReaderCache(Context context) {
+        mContext = context;
+
+        JSONObject items;
+        try {
+            items = GeckoProfile.get(mContext).readJSONObjectFromFile(FILE_PATH);
+        } catch (IOException e) {
+            Log.i(LOG_TAG, "Couldn't read readercache file, initialising empty readercache instead", e);
+            items = new JSONObject();
+        }
+        mItems = items;
+    }
+
+    private JSONObject makeItem(@NonNull String path, long size) throws JSONException {
+        final JSONObject item = new JSONObject();
+
+        item.put(PATH, path);
+        item.put(SIZE, size);
+
+        return item;
+    }
+
+    public synchronized boolean isURLCached(@NonNull final String URL) {
+        return mItems.has(URL);
+    }
+
+    public synchronized void put(@NonNull final String pageURL, @NonNull final String path, final long size) {
+        try {
+            mItems.put(pageURL, makeItem(path, size));
+        } catch (JSONException e) {
+            Log.w(LOG_TAG, "Item insertion failed:", e);
+            // This should never happen, absent any errors in our own implementation
+            throw new IllegalStateException("Failure inserting into SavedReaderViewHelper json");
+        }
+
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                UrlAnnotations annotations = GeckoProfile.get(mContext).getDB().getUrlAnnotations();
+                annotations.insertReaderviewUrl(mContext.getContentResolver(), pageURL);
+
+                commit();
+            }
+        });
+    }
+
+    protected synchronized void remove(@NonNull final String pageURL) {
+        mItems.remove(pageURL);
+
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                UrlAnnotations annotations = GeckoProfile.get(mContext).getDB().getUrlAnnotations();
+                annotations.deleteReaderviewUrl(mContext.getContentResolver(), pageURL);
+
+                commit();
+            }
+        });
+    }
+
+    public synchronized int size() {
+        return mItems.length();
+    }
+
+    private synchronized void commit() {
+        ThreadUtils.assertOnBackgroundThread();
+
+        GeckoProfile profile = GeckoProfile.get(mContext);
+        File cacheDir = new File(profile.getDir(), DIRECTORY);
+
+        if (!cacheDir.exists()) {
+            Log.i(LOG_TAG, "No preexisting cache directory, creating now");
+            cacheDir.mkdir();
+        }
+
+        profile.writeFile(FILE_PATH, mItems.toString());
+    }
+
+    private static SavedReaderViewHelper ourSavedReaderViewHelper = null;
+
+    public static synchronized SavedReaderViewHelper getSavedReaderViewHelper(final Context context) {
+        if (ourSavedReaderViewHelper != null) {
+            return ourSavedReaderViewHelper;
+        }
+
+        ourSavedReaderViewHelper = new SavedReaderViewHelper(context);
+        return ourSavedReaderViewHelper;
+    }
+
+    /**
+     * Return the Reader View URL for a given URL if it is contained in the cache. Returns the
+     * plain URL if the page is not cached.
+     */
+    public static String getReaderURLIfCached(final Context context, @NonNull final String pageURL) {
+        SavedReaderViewHelper rch = getSavedReaderViewHelper(context);
+
+        if (rch.isURLCached(pageURL)) {
+            return ReaderModeUtils.getAboutReaderForUrl(pageURL);
+        } else {
+            return pageURL;
+        }
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -516,16 +516,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'prompts/Prompt.java',
     'prompts/PromptInput.java',
     'prompts/PromptListAdapter.java',
     'prompts/PromptListItem.java',
     'prompts/PromptService.java',
     'prompts/TabInput.java',
     'reader/ReaderModeUtils.java',
     'reader/ReadingListHelper.java',
+    'reader/SavedReaderViewHelper.java',
     'RemoteClientsDialogFragment.java',
     'RemoteTabsExpandableListAdapter.java',
     'Restarter.java',
     'restrictions/DefaultConfiguration.java',
     'restrictions/GuestProfileConfiguration.java',
     'restrictions/Restrictable.java',
     'restrictions/RestrictedProfileConfiguration.java',
     'restrictions/RestrictionCache.java',