From 93a0a6fc56cfd316b3272e9472d06c4728011f6a Mon Sep 17 00:00:00 2001 From: Sola Ogunsakin Date: Wed, 12 Mar 2014 14:53:25 -0700 Subject: [PATCH] Bug 959297 - Get description and approx. reading time for reading list items. r=liuche, r=lucasr, r=margaret, r=rnewman --- mobile/android/base/BrowserApp.java | 19 +++-- mobile/android/base/db/BrowserContract.java | 3 + mobile/android/base/db/BrowserDB.java | 7 +- mobile/android/base/db/LocalBrowserDB.java | 15 ++-- mobile/android/chrome/content/Readability.js | 77 +++++++++++++++++++- mobile/android/chrome/content/aboutReader.js | 2 + mobile/android/chrome/content/browser.js | 22 +++--- 7 files changed, 122 insertions(+), 23 deletions(-) diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java index b98f4200375a..3e811f53cf50 100644 --- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -20,6 +20,7 @@ import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException; import org.mozilla.gecko.animation.PropertyAnimator; import org.mozilla.gecko.animation.ViewHelper; import org.mozilla.gecko.db.BrowserContract.Combined; +import org.mozilla.gecko.db.BrowserContract.ReadingListItems; import org.mozilla.gecko.db.BrowserDB; import org.mozilla.gecko.favicons.Favicons; import org.mozilla.gecko.favicons.LoadFaviconTask; @@ -59,6 +60,7 @@ import org.mozilla.gecko.widget.GeckoActionProvider; import android.app.Activity; import android.app.AlertDialog; +import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; @@ -385,7 +387,7 @@ abstract public class BrowserApp extends GeckoApp }); } - void handleReaderAdded(int result, final String title, final String url) { + private void handleReaderAdded(int result, final ContentValues values) { if (result != READER_ADD_SUCCESS) { if (result == READER_ADD_FAILED) { showToast(R.string.reading_list_failed, Toast.LENGTH_SHORT); @@ -399,7 +401,7 @@ abstract public class BrowserApp extends GeckoApp ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { - BrowserDB.addReadingListItem(getContentResolver(), title, url); + BrowserDB.addReadingListItem(getContentResolver(), values); showToast(R.string.reading_list_added, Toast.LENGTH_SHORT); final int count = BrowserDB.getReadingListCount(getContentResolver()); @@ -408,6 +410,15 @@ abstract public class BrowserApp extends GeckoApp }); } + private ContentValues messageToReadingListContentValues(JSONObject message) { + final ContentValues values = new ContentValues(); + values.put(ReadingListItems.URL, message.optString("url")); + values.put(ReadingListItems.TITLE, message.optString("title")); + values.put(ReadingListItems.LENGTH, message.optInt("length")); + values.put(ReadingListItems.EXCERPT, message.optString("excerpt")); + return values; + } + void handleReaderRemoved(final String url) { ThreadUtils.postToBackgroundThread(new Runnable() { @Override @@ -1127,9 +1138,7 @@ abstract public class BrowserApp extends GeckoApp handleReaderListStatusRequest(message.getString("url")); } else if (event.equals("Reader:Added")) { final int result = message.getInt("result"); - final String title = message.getString("title"); - final String url = message.getString("url"); - handleReaderAdded(result, title, url); + handleReaderAdded(result, messageToReadingListContentValues(message)); } else if (event.equals("Reader:Removed")) { final String url = message.getString("url"); handleReaderRemoved(url); diff --git a/mobile/android/base/db/BrowserContract.java b/mobile/android/base/db/BrowserContract.java index 9462473bdebd..0b809bafa01b 100644 --- a/mobile/android/base/db/BrowserContract.java +++ b/mobile/android/base/db/BrowserContract.java @@ -396,6 +396,9 @@ public class BrowserContract { public static final String DEFAULT_SORT_ORDER = _ID + " DESC"; public static final String[] DEFAULT_PROJECTION = new String[] { _ID, URL, TITLE, EXCERPT, LENGTH }; + // Minimum fields required to create a reading list item. + public static final String[] REQUIRED_FIELDS = { Bookmarks.URL, Bookmarks.TITLE }; + public static final String TABLE_NAME = "reading_list"; } diff --git a/mobile/android/base/db/BrowserDB.java b/mobile/android/base/db/BrowserDB.java index 1a1c15eed8c6..d2558c2fd150 100644 --- a/mobile/android/base/db/BrowserDB.java +++ b/mobile/android/base/db/BrowserDB.java @@ -13,6 +13,7 @@ import org.mozilla.gecko.favicons.decoders.LoadFaviconResult; import org.mozilla.gecko.mozglue.RobocopTarget; import android.content.ContentResolver; +import android.content.ContentValues; import android.database.ContentObserver; import android.database.Cursor; import android.database.CursorWrapper; @@ -98,7 +99,7 @@ public class BrowserDB { @RobocopTarget public void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword); - public void addReadingListItem(ContentResolver cr, String title, String uri); + public void addReadingListItem(ContentResolver cr, ContentValues values); public void removeReadingListItemWithURL(ContentResolver cr, String uri); @@ -271,8 +272,8 @@ public class BrowserDB { sDb.updateBookmark(cr, id, uri, title, keyword); } - public static void addReadingListItem(ContentResolver cr, String title, String uri) { - sDb.addReadingListItem(cr, title, uri); + public static void addReadingListItem(ContentResolver cr, ContentValues values) { + sDb.addReadingListItem(cr, values); } public static void removeReadingListItemWithURL(ContentResolver cr, String uri) { diff --git a/mobile/android/base/db/LocalBrowserDB.java b/mobile/android/base/db/LocalBrowserDB.java index 6e4cc7af54f1..e2c3e5da9153 100644 --- a/mobile/android/base/db/LocalBrowserDB.java +++ b/mobile/android/base/db/LocalBrowserDB.java @@ -699,11 +699,16 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { } @Override - public void addReadingListItem(ContentResolver cr, String title, String uri) { - final ContentValues values = new ContentValues(); + public void addReadingListItem(ContentResolver cr, ContentValues values) { + // Check that required fields are present. + for (String field: ReadingListItems.REQUIRED_FIELDS) { + if (!values.containsKey(field)) { + throw new IllegalArgumentException("Missing required field for reading list item: " + field); + } + } + + // Clear delete flag if necessary values.put(ReadingListItems.IS_DELETED, 0); - values.put(ReadingListItems.URL, uri); - values.put(ReadingListItems.TITLE, title); // Restore deleted record if possible final Uri insertUri = mReadingListUriWithProfile @@ -714,7 +719,7 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface { final int updated = cr.update(insertUri, values, ReadingListItems.URL + " = ? ", - new String[] { uri }); + new String[] { values.getAsString(ReadingListItems.URL) }); debug("Updated " + updated + " rows to new modified time."); } diff --git a/mobile/android/chrome/content/Readability.js b/mobile/android/chrome/content/Readability.js index 7b1478204530..5efaa0c2b401 100644 --- a/mobile/android/chrome/content/Readability.js +++ b/mobile/android/chrome/content/Readability.js @@ -718,6 +718,77 @@ Readability.prototype = { } }, + /** + * Attempts to get the excerpt from these + * sources in the following order: + * - meta description tag + * - open-graph description + * - twitter cards description + * - article's first paragraph + * If no excerpt is found, an empty string will be + * returned. + * + * @param Element - root element of the processed version page + * @return String - excerpt of the article + **/ + _getExcerpt: function(articleContent) { + let values = {}; + let metaElements = this._doc.getElementsByTagName("meta"); + + // Match "description", or Twitter's "twitter:description" (Cards) + // in name attribute. + let namePattern = /^\s*((twitter)\s*:\s*)?description\s*$/gi; + + // Match Facebook's og:description (Open Graph) in property attribute. + let propertyPattern = /^\s*og\s*:\s*description\s*$/gi; + + // Find description tags. + for (let i = 0; i < metaElements.length; i++) { + let element = metaElements[i]; + let elementName = element.getAttribute("name"); + let elementProperty = element.getAttribute("property"); + + let name; + if (namePattern.test(elementName)) { + name = elementName; + } else if (propertyPattern.test(elementProperty)) { + name = elementProperty; + } + + if (name) { + let content = element.getAttribute("content"); + if (content) { + // Convert to lowercase and remove any whitespace + // so we can match below. + name = name.toLowerCase().replace(/\s/g, ''); + values[name] = content.trim(); + } + } + } + + if ("description" in values) { + return values["description"]; + } + + if ("og:description" in values) { + // Use facebook open graph description. + return values["og:description"]; + } + + if ("twitter:description" in values) { + // Use twitter cards description. + return values["twitter:description"]; + } + + // No description meta tags, use the article's first paragraph. + let paragraphs = articleContent.getElementsByTagName("p"); + if (paragraphs.length > 0) { + return paragraphs[0].textContent; + } + + return ""; + }, + /** * Removes script tags from the document. * @@ -1434,9 +1505,13 @@ Readability.prototype = { // }).bind(this), 500); // } + let excerpt = this._getExcerpt(articleContent); + return { title: articleTitle, byline: this._articleByline, dir: this._articleDir, - content: articleContent.innerHTML }; + content: articleContent.innerHTML, + length: articleContent.textContent.length, + excerpt: excerpt }; } }; diff --git a/mobile/android/chrome/content/aboutReader.js b/mobile/android/chrome/content/aboutReader.js index 094ff43ee73e..4c44bc8f57b1 100644 --- a/mobile/android/chrome/content/aboutReader.js +++ b/mobile/android/chrome/content/aboutReader.js @@ -349,6 +349,8 @@ AboutReader.prototype = { result: result, title: this._article.title, url: this._article.url, + length: this._article.length, + excerpt: this._article.excerpt }); }.bind(this)); } else { diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index c432b33ef650..646fb0fabad0 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -7433,33 +7433,36 @@ let Reader = { throw new Error("Reader:Add requires a tabID or an URL as argument"); } - let sendResult = function(result, title) { - this.log("Reader:Add success=" + result + ", url=" + url + ", title=" + title); + let sendResult = function(result, article) { + article = article || {}; + this.log("Reader:Add success=" + result + ", url=" + url + ", title=" + article.title + ", excerpt=" + article.excerpt); sendMessageToJava({ type: "Reader:Added", result: result, - title: title, + title: article.title, url: url, + length: article.length, + excerpt: article.excerpt }); }.bind(this); let handleArticle = function(article) { if (!article) { - sendResult(this.READER_ADD_FAILED, ""); + sendResult(this.READER_ADD_FAILED, null); return; } this.storeArticleInCache(article, function(success) { let result = (success ? this.READER_ADD_SUCCESS : this.READER_ADD_FAILED); - sendResult(result, article.title); + sendResult(result, article); }.bind(this)); }.bind(this); this.getArticleFromCache(urlWithoutRef, function (article) { // If the article is already in reading list, bail if (article) { - sendResult(this.READER_ADD_DUPLICATE, ""); + sendResult(this.READER_ADD_DUPLICATE, null); return; } @@ -7473,13 +7476,14 @@ let Reader = { } case "Reader:Remove": { - this.removeArticleFromCache(aData, function(success) { - this.log("Reader:Remove success=" + success + ", url=" + aData); + let url = aData; + this.removeArticleFromCache(url, function(success) { + this.log("Reader:Remove success=" + success + ", url=" + url); if (success) { sendMessageToJava({ type: "Reader:Removed", - url: aData + url: url }); } }.bind(this));