gecko-dev/mobile/android/base/ReadingListHelper.java

326 строки
12 KiB
Java

/* 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;
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.ReadingListAccessor;
import org.mozilla.gecko.favicons.Favicons;
import org.mozilla.gecko.mozglue.RobocopTarget;
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;
import android.content.ContentValues;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.util.Log;
public final class ReadingListHelper implements NativeEventListener {
private static final String LOGTAG = "GeckoReadingListHelper";
public interface OnReadingListEventListener {
void onAddedToReadingList(String url);
void onRemovedFromReadingList(String url);
void onAlreadyInReadingList(String url);
}
private enum ReadingListEvent {
ADDED,
REMOVED,
ALREADY_EXISTS
}
protected final Context context;
private final BrowserDB db;
private final ReadingListAccessor readingListAccessor;
private final ContentObserver contentObserver;
private final OnReadingListEventListener onReadingListEventListener;
volatile boolean fetchInBackground = true;
public ReadingListHelper(Context context, GeckoProfile profile, OnReadingListEventListener listener) {
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");
contentObserver = new ContentObserver(null) {
@Override
public void onChange(boolean selfChange) {
if (fetchInBackground) {
fetchContent();
}
}
};
this.readingListAccessor.registerContentObserver(context, contentObserver);
onReadingListEventListener = listener;
}
public void uninit() {
EventDispatcher.getInstance().unregisterGeckoThreadListener((NativeEventListener) this,
"Reader:AddToList", "Reader:UpdateList", "Reader:FaviconRequest", "Reader:ListStatusRequest", "Reader:RemoveFromList");
context.getContentResolver().unregisterContentObserver(contentObserver);
}
@Override
public void handleMessage(final String event, final NativeJSObject message,
final EventCallback callback) {
switch(event) {
case "Reader:AddToList": {
handleAddToList(callback, message);
break;
}
case "Reader:UpdateList": {
handleUpdateList(message);
break;
}
case "Reader:FaviconRequest": {
handleReaderModeFaviconRequest(callback, message.getString("url"));
break;
}
case "Reader:RemoveFromList": {
handleRemoveFromList(message.getString("url"));
break;
}
case "Reader:ListStatusRequest": {
handleReadingListStatusRequest(callback, message.getString("url"));
break;
}
}
}
/**
* A page can be added to the ReadingList by long-tap of the page-action
* icon, or by tapping the readinglist-add icon in the ReaderMode banner.
*
* This method will only add new items, not update existing items.
*/
private void handleAddToList(final EventCallback callback, final NativeJSObject message) {
final ContentResolver cr = context.getContentResolver();
final String url = message.getString("url");
// 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 (readingListAccessor.isReadingListItem(cr, url)) {
handleEvent(ReadingListEvent.ALREADY_EXISTS, url);
callback.sendError("URL already in reading list: " + url);
} else {
readingListAccessor.addReadingListItem(cr, values);
handleEvent(ReadingListEvent.ADDED, url);
callback.sendSuccess(url);
}
}
});
}
/**
* Updates a reading list item with new meta data.
*/
private void handleUpdateList(final NativeJSObject message) {
final ContentResolver cr = context.getContentResolver();
final ContentValues values = getContentValues(message);
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
readingListAccessor.updateReadingListItem(cr, values);
}
});
}
/**
* Creates reading list item content values from JS message.
*/
private ContentValues getContentValues(NativeJSObject message) {
final ContentValues values = new ContentValues();
if (message.has("id")) {
values.put(ReadingListItems._ID, message.getInt("id"));
}
// url is actually required...
String url = null;
if (message.has("url")) {
url = message.getString("url");
values.put(ReadingListItems.URL, url);
}
String title = null;
if (message.has("title")) {
title = message.getString("title");
values.put(ReadingListItems.TITLE, title);
}
// TODO: message actually has "length", but that's no use for us. See Bug 1127451.
if (message.has("word_count")) {
values.put(ReadingListItems.WORD_COUNT, message.getInt("word_count"));
}
if (message.has("excerpt")) {
values.put(ReadingListItems.EXCERPT, message.getString("excerpt"));
}
if (message.has("status")) {
final int status = message.getInt("status");
values.put(ReadingListItems.CONTENT_STATUS, status);
if (status == ReadingListItems.STATUS_FETCHED_ARTICLE) {
if (message.has("resolved_title")) {
values.put(ReadingListItems.RESOLVED_TITLE, message.getString("resolved_title"));
} else {
if (title != null) {
values.put(ReadingListItems.RESOLVED_TITLE, title);
}
}
if (message.has("resolved_url")) {
values.put(ReadingListItems.RESOLVED_URL, message.getString("resolved_url"));
} else {
if (url != null) {
values.put(ReadingListItems.RESOLVED_URL, url);
}
}
}
}
return values;
}
/**
* 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() {
return Favicons.getFaviconURLForPageURL(db, context.getContentResolver(), url);
}
@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();
}
/**
* 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() {
readingListAccessor.removeReadingListItemWithURL(context.getContentResolver(), url);
handleEvent(ReadingListEvent.REMOVED, url);
}
});
}
/**
* 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 = 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);
}
// Return the json object to fulfill the promise.
callback.sendSuccess(json.toString());
}
});
}
/**
* Handle various reading list events (and display appropriate toasts).
*/
private void handleEvent(final ReadingListEvent event, final String url) {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
switch(event) {
case ADDED:
onReadingListEventListener.onAddedToReadingList(url);
break;
case REMOVED:
onReadingListEventListener.onRemovedFromReadingList(url);
break;
case ALREADY_EXISTS:
onReadingListEventListener.onAlreadyInReadingList(url);
break;
}
}
});
}
private void fetchContent() {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
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()));
} catch (JSONException e) {
Log.e(LOGTAG, "Failed to fetch reading list content for item");
}
}
} finally {
c.close();
}
}
});
}
@RobocopTarget
/**
* Test code will want to disable background fetches to avoid upsetting
* the test harness. Call this by accessing the instance from BrowserApp.
*/
public void disableBackgroundFetches() {
fetchInBackground = false;
}
}