mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-04 23:08:39 +09:00
pt 1 in reviving the android build (copied from pm 28a1, wish me luck)
This commit is contained in:
parent
efa9662725
commit
d7788a6d6d
4249 changed files with 468189 additions and 0 deletions
|
|
@ -0,0 +1,72 @@
|
|||
/* 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 org.mozilla.gecko.AboutPages;
|
||||
import org.mozilla.gecko.util.StringUtils;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
public class ReaderModeUtils {
|
||||
private static final String LOGTAG = "ReaderModeUtils";
|
||||
|
||||
/**
|
||||
* Extract the URL from a valid about:reader URL. You may want to use stripAboutReaderUrl
|
||||
* instead to always obtain a valid String.
|
||||
*
|
||||
* @see #stripAboutReaderUrl(String) for a safer version that returns the original URL for malformed/invalid
|
||||
* URLs.
|
||||
* @return <code>null</code> if the URL is malformed or doesn't contain a URL parameter.
|
||||
*/
|
||||
private static String getUrlFromAboutReader(String aboutReaderUrl) {
|
||||
return StringUtils.getQueryParameter(aboutReaderUrl, "url");
|
||||
}
|
||||
|
||||
public static boolean isEnteringReaderMode(String oldURL, String newURL) {
|
||||
if (oldURL == null || newURL == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!AboutPages.isAboutReader(newURL)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String urlFromAboutReader = getUrlFromAboutReader(newURL);
|
||||
if (urlFromAboutReader == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return urlFromAboutReader.equals(oldURL);
|
||||
}
|
||||
|
||||
public static String getAboutReaderForUrl(String url) {
|
||||
return getAboutReaderForUrl(url, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the underlying URL from an about:reader URL.
|
||||
* This will return the input URL if either of the following is true:
|
||||
* 1. the input URL is a non about:reader URL
|
||||
* 2. the input URL is an invalid/unparseable about:reader URL
|
||||
*/
|
||||
public static String stripAboutReaderUrl(String url) {
|
||||
if (!AboutPages.isAboutReader(url)) {
|
||||
return url;
|
||||
}
|
||||
|
||||
final String strippedUrl = getUrlFromAboutReader(url);
|
||||
return strippedUrl != null ? strippedUrl : url;
|
||||
}
|
||||
|
||||
public static String getAboutReaderForUrl(String url, int tabId) {
|
||||
String aboutReaderUrl = AboutPages.READER + "?url=" + Uri.encode(url);
|
||||
|
||||
if (tabId >= 0) {
|
||||
aboutReaderUrl += "&tabId=" + tabId;
|
||||
}
|
||||
|
||||
return aboutReaderUrl;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
/* 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 org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import org.mozilla.gecko.AboutPages;
|
||||
import org.mozilla.gecko.EventDispatcher;
|
||||
import org.mozilla.gecko.GeckoApp;
|
||||
import org.mozilla.gecko.GeckoAppShell;
|
||||
import org.mozilla.gecko.GeckoProfile;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
import org.mozilla.gecko.icons.IconRequest;
|
||||
import org.mozilla.gecko.icons.Icons;
|
||||
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.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public final class ReadingListHelper implements NativeEventListener {
|
||||
private static final String LOGTAG = "GeckoReadingListHelper";
|
||||
|
||||
protected final Context context;
|
||||
private final BrowserDB db;
|
||||
|
||||
public ReadingListHelper(Context context, GeckoProfile profile) {
|
||||
this.context = context;
|
||||
this.db = BrowserDB.from(profile);
|
||||
|
||||
GeckoApp.getEventDispatcher().registerGeckoThreadListener((NativeEventListener) this,
|
||||
"Reader:FaviconRequest", "Reader:AddedToCache");
|
||||
}
|
||||
|
||||
public void uninit() {
|
||||
GeckoApp.getEventDispatcher().unregisterGeckoThreadListener((NativeEventListener) this,
|
||||
"Reader:FaviconRequest", "Reader:AddedToCache");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(final String event, final NativeJSObject message,
|
||||
final EventCallback callback) {
|
||||
switch (event) {
|
||||
case "Reader:FaviconRequest": {
|
||||
handleReaderModeFaviconRequest(callback, message.getString("url"));
|
||||
break;
|
||||
}
|
||||
case "Reader:AddedToCache": {
|
||||
// AddedToCache is a one way message: callback will be null, and we therefore shouldn't
|
||||
// attempt to handle it.
|
||||
handleAddedToCache(message.getString("url"), message.getString("path"), message.getInt("size"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gecko (ReaderMode) requests the page favicon to append to the
|
||||
* document head for display.
|
||||
*/
|
||||
private void handleReaderModeFaviconRequest(final EventCallback callback, final String url) {
|
||||
(new UIAsyncTask.WithoutParams<String>(ThreadUtils.getBackgroundHandler()) {
|
||||
@Override
|
||||
public String doInBackground() {
|
||||
// This is a bit ridiculous if you look at the bigger picture: Reader mode extracts
|
||||
// the article content. We insert the content into a new document (about:reader).
|
||||
// Some events are exchanged to lookup the icon URL for the actual website. This
|
||||
// URL is then added to the markup which will then trigger our icon loading code in
|
||||
// the Tab class.
|
||||
//
|
||||
// The Tab class could just lookup and load the icon itself. All it needs to do is
|
||||
// to strip the about:reader URL and perform a normal icon load from cache.
|
||||
//
|
||||
// A more global solution (looking at desktop and iOS) would be to copy the <link>
|
||||
// markup from the original page to the about:reader page and then rely on our normal
|
||||
// icon loading code. This would work even if we do not have anything in the cache
|
||||
// for some kind of reason.
|
||||
|
||||
final IconRequest request = Icons.with(context)
|
||||
.pageUrl(url)
|
||||
.prepareOnly()
|
||||
.build();
|
||||
|
||||
try {
|
||||
request.execute(null).get();
|
||||
if (request.getIconCount() > 0) {
|
||||
return request.getBestIcon().getUrl();
|
||||
}
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPostExecute(String faviconUrl) {
|
||||
JSONObject args = new JSONObject();
|
||||
if (faviconUrl != null) {
|
||||
try {
|
||||
args.put("url", url);
|
||||
args.put("faviconUrl", faviconUrl);
|
||||
} catch (JSONException e) {
|
||||
Log.w(LOGTAG, "Error building JSON favicon arguments.", e);
|
||||
}
|
||||
}
|
||||
callback.sendSuccess(args.toString());
|
||||
}
|
||||
}).execute();
|
||||
}
|
||||
|
||||
private void handleAddedToCache(final String url, final String path, final int size) {
|
||||
final SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(context);
|
||||
|
||||
rch.put(url, path, size);
|
||||
}
|
||||
|
||||
public static void cacheReaderItem(final String url, final int tabID, Context context) {
|
||||
if (AboutPages.isAboutReader(url)) {
|
||||
throw new IllegalArgumentException("Page url must be original (not about:reader) url");
|
||||
}
|
||||
|
||||
SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(context);
|
||||
|
||||
if (!rch.isURLCached(url)) {
|
||||
GeckoAppShell.notifyObservers("Reader:AddToCache", Integer.toString(tabID));
|
||||
}
|
||||
}
|
||||
|
||||
public static void removeCachedReaderItem(final String url, Context context) {
|
||||
if (AboutPages.isAboutReader(url)) {
|
||||
throw new IllegalArgumentException("Page url must be original (not about:reader) url");
|
||||
}
|
||||
|
||||
SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(context);
|
||||
|
||||
if (rch.isURLCached(url)) {
|
||||
GeckoAppShell.notifyObservers("Reader:RemoveFromCache", url);
|
||||
}
|
||||
|
||||
// When removing items from the cache we can probably spare ourselves the async callback
|
||||
// that we use when adding cached items. We know the cached item will be gone, hence
|
||||
// we no longer need to track it in the SavedReaderViewHelper
|
||||
rch.remove(url);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue