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
--- 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',