summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/reader/SavedReaderViewHelper.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/reader/SavedReaderViewHelper.java')
-rw-r--r--mobile/android/base/java/org/mozilla/gecko/reader/SavedReaderViewHelper.java247
1 files changed, 247 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/reader/SavedReaderViewHelper.java b/mobile/android/base/java/org/mozilla/gecko/reader/SavedReaderViewHelper.java
new file mode 100644
index 000000000..e60abac71
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/reader/SavedReaderViewHelper.java
@@ -0,0 +1,247 @@
+/* -*- 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.annotation.RobocopTarget;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.UrlAnnotations;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Iterator;
+
+/**
+ * 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;
+
+ // We use null to indicate that the cache hasn't yet been loaded. Loading has to be explicitly
+ // requested by client code, and must happen on the background thread. Attempting to access
+ // items (which happens mainly on the UI thread) before explicitly loading them is not permitted.
+ private JSONObject mItems = null;
+
+ private final Context mContext;
+
+ private static SavedReaderViewHelper instance = null;
+
+ private SavedReaderViewHelper(Context context) {
+ mContext = context;
+ }
+
+ public static synchronized SavedReaderViewHelper getSavedReaderViewHelper(final Context context) {
+ if (instance == null) {
+ instance = new SavedReaderViewHelper(context);
+ }
+
+ return instance;
+ }
+
+ /**
+ * Load the reader view cache list from our JSON file.
+ *
+ * Must not be run on the UI thread due to file access.
+ */
+ public synchronized void loadItems() {
+ // TODO bug 1264489
+ // This is a band aid fix for Bug 1264134. We need to figure out the root cause and reenable this
+ // assertion.
+ // ThreadUtils.assertNotOnUiThread();
+
+ if (mItems != null) {
+ return;
+ }
+
+ try {
+ mItems = GeckoProfile.get(mContext).readJSONObjectFromFile(FILE_PATH);
+ } catch (IOException e) {
+ mItems = new JSONObject();
+ }
+ }
+
+ private synchronized void assertItemsLoaded() {
+ if (mItems == null) {
+ throw new IllegalStateException("SavedReaderView items must be explicitly loaded using loadItems() before access.");
+ }
+ }
+
+ 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) {
+ assertItemsLoaded();
+ return mItems.has(URL);
+ }
+
+ /**
+ * Insert an item into the list of cached items.
+ *
+ * This may be called from any thread.
+ */
+ public synchronized void put(@NonNull final String pageURL, @NonNull final String path, final long size) {
+ assertItemsLoaded();
+
+ 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 = BrowserDB.from(mContext).getUrlAnnotations();
+ annotations.insertReaderViewUrl(mContext.getContentResolver(), pageURL);
+
+ commit();
+ }
+ });
+ }
+
+ protected synchronized void remove(@NonNull final String pageURL) {
+ assertItemsLoaded();
+
+ mItems.remove(pageURL);
+
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ UrlAnnotations annotations = BrowserDB.from(mContext).getUrlAnnotations();
+ annotations.deleteReaderViewUrl(mContext.getContentResolver(), pageURL);
+
+ commit();
+ }
+ });
+ }
+
+ @RobocopTarget
+ public synchronized int size() {
+ assertItemsLoaded();
+ 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");
+
+ boolean cacheDirCreated = cacheDir.mkdir();
+ if (!cacheDirCreated) {
+ throw new IllegalStateException("Couldn't create cache directory, unable to track reader view cache");
+ }
+ }
+
+ profile.writeFile(FILE_PATH, mItems.toString());
+ }
+
+ /**
+ * 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 rvh = getSavedReaderViewHelper(context);
+
+ if (rvh.isURLCached(pageURL)) {
+ return ReaderModeUtils.getAboutReaderForUrl(pageURL);
+ } else {
+ return pageURL;
+ }
+ }
+
+ /**
+ * Obtain the total disk space used for saved reader view items, in KB.
+ *
+ * @return Total disk space used (KB), or Integer.MAX_VALUE on overflow.
+ */
+ public synchronized int getDiskSpacedUsedKB() {
+ // JSONObject is not thread safe - we need to be synchronized to avoid issues (most likely to
+ // occur if items are removed during iteration).
+ final Iterator<String> keys = mItems.keys();
+ long bytes = 0;
+
+ while (keys.hasNext()) {
+ final String pageURL = keys.next();
+ try {
+ final JSONObject item = mItems.getJSONObject(pageURL);
+ bytes += item.getLong(SIZE);
+
+ // Overflow is highly unlikely (we will hit device storage limits before we hit integer limits),
+ // but we should still handle this for correctness.
+ // We definitely can't store our output in an int if we overflow the long here.
+ if (bytes < 0) {
+ return Integer.MAX_VALUE;
+ }
+ } catch (JSONException e) {
+ // This shouldn't ever happen:
+ throw new IllegalStateException("Must be able to access items in saved reader view list", e);
+ }
+ }
+
+ long kb = bytes / 1024;
+ if (kb > Integer.MAX_VALUE) {
+ return Integer.MAX_VALUE;
+ } else {
+ return (int) kb;
+ }
+ }
+}