Bug 959297 - Get description and approx. reading time for reading list items. r=liuche, r=lucasr, r=margaret, r=rnewman
authorSola Ogunsakin <oogunsakin@mozilla.com>
Wed, 12 Mar 2014 14:53:25 -0700
changeset 191793 21c0c5f02e83d73ebfd779bc1a383bf6c68c21f1
parent 191792 f50f63b769ad6f4c90105094c7095f9d2f8dffe9
child 191794 d36aeeed76a9179374151f942897eec81af2b591
push id474
push userasasaki@mozilla.com
push dateMon, 02 Jun 2014 21:01:02 +0000
treeherdermozilla-release@967f4cf1b31c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersliuche, lucasr, margaret, rnewman
bugs959297
milestone30.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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
mobile/android/base/db/BrowserContract.java
mobile/android/base/db/BrowserDB.java
mobile/android/base/db/LocalBrowserDB.java
mobile/android/chrome/content/Readability.js
mobile/android/chrome/content/aboutReader.js
mobile/android/chrome/content/browser.js
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -15,16 +15,17 @@ import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.DynamicToolbar.PinReason;
 import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
 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;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
 import org.mozilla.gecko.gfx.BitmapUtils;
@@ -54,16 +55,17 @@ import org.mozilla.gecko.util.MenuUtils;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UiAsyncTask;
 import org.mozilla.gecko.widget.ButtonToast;
 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;
 import android.content.SharedPreferences;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.graphics.Bitmap;
@@ -380,39 +382,48 @@ abstract public class BrowserApp extends
                     return;
                 }
 
                 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:ListStatusReturn", json.toString()));
             }
         });
     }
 
-    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);
             } else if (result == READER_ADD_DUPLICATE) {
                 showToast(R.string.reading_list_duplicate, Toast.LENGTH_SHORT);
             }
 
             return;
         }
 
         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());
                 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:ListCountUpdated", Integer.toString(count)));
             }
         });
     }
 
+    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
             public void run() {
                 BrowserDB.removeReadingListItemWithURL(getContentResolver(), url);
                 showToast(R.string.reading_list_removed, Toast.LENGTH_SHORT);
 
                 final int count = BrowserDB.getReadingListCount(getContentResolver());
@@ -1122,19 +1133,17 @@ abstract public class BrowserApp extends
                 Telemetry.HistogramAdd("FENNEC_FAVICONS_COUNT", BrowserDB.getCount(getContentResolver(), "favicons"));
                 Telemetry.HistogramAdd("FENNEC_THUMBNAILS_COUNT", BrowserDB.getCount(getContentResolver(), "thumbnails"));
             } else if (event.equals("Reader:ListCountRequest")) {
                 handleReaderListCountRequest();
             } else if (event.equals("Reader:ListStatusRequest")) {
                 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);
             } else if (event.equals("Reader:Share")) {
                 final String title = message.getString("title");
                 final String url = message.getString("url");
                 GeckoAppShell.openUriExternal(url, "text/plain", "", "",
                                               Intent.ACTION_SEND, title);
--- a/mobile/android/base/db/BrowserContract.java
+++ b/mobile/android/base/db/BrowserContract.java
@@ -391,12 +391,15 @@ public class BrowserContract {
         public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/readinglistitem";
 
         public static final String EXCERPT = "excerpt";
         public static final String READ = "read";
         public static final String LENGTH = "length";
         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";
     }
 
 }
--- a/mobile/android/base/db/BrowserDB.java
+++ b/mobile/android/base/db/BrowserDB.java
@@ -8,16 +8,17 @@ package org.mozilla.gecko.db;
 import java.util.List;
 
 import org.mozilla.gecko.db.BrowserContract.Bookmarks;
 import org.mozilla.gecko.db.BrowserContract.ExpirePriority;
 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;
 import android.graphics.drawable.BitmapDrawable;
 import android.util.SparseArray;
 
 public class BrowserDB {
     private static boolean sAreContentProvidersEnabled = true;
@@ -93,17 +94,17 @@ public class BrowserDB {
         public void removeBookmark(ContentResolver cr, int id);
 
         @RobocopTarget
         public void removeBookmarksWithURL(ContentResolver cr, String uri);
 
         @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);
 
         public void removeReadingListItem(ContentResolver cr, int id);
 
         public LoadFaviconResult getFaviconForUrl(ContentResolver cr, String uri);
 
         public String getFaviconUrlForHistoryUrl(ContentResolver cr, String url);
@@ -266,18 +267,18 @@ public class BrowserDB {
         sDb.removeBookmarksWithURL(cr, uri);
     }
 
     @RobocopTarget
     public static void updateBookmark(ContentResolver cr, int id, String uri, String title, String keyword) {
         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) {
         sDb.removeReadingListItemWithURL(cr, uri);
     }
 
     public static void removeReadingListItem(ContentResolver cr, int id) {
         sDb.removeReadingListItem(cr, id);
--- a/mobile/android/base/db/LocalBrowserDB.java
+++ b/mobile/android/base/db/LocalBrowserDB.java
@@ -694,32 +694,37 @@ public class LocalBrowserDB implements B
         // Toggling bookmark on an URL should not affect the items in the reading list
         final String[] urlArgs = new String[] { uri, String.valueOf(Bookmarks.FIXED_READING_LIST_ID) };
         final String urlEquals = Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " != ?";
 
         cr.delete(contentUri, urlEquals, urlArgs);
     }
 
     @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
                               .buildUpon()
                               .appendQueryParameter(BrowserContract.PARAM_INSERT_IF_NEEDED, "true")
                               .build();
 
         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.");
     }
 
     @Override
     public void removeReadingListItemWithURL(ContentResolver cr, String uri) {
         cr.delete(mReadingListUriWithProfile, ReadingListItems.URL + " = ? ", new String[] { uri });
     }
--- a/mobile/android/chrome/content/Readability.js
+++ b/mobile/android/chrome/content/Readability.js
@@ -714,16 +714,87 @@ Readability.prototype = {
         }
 
         return articleContent;
       }
     }
   },
 
   /**
+   * 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.
    *
    * @param Element
   **/
   _removeScripts: function(doc) {
     let scripts = doc.getElementsByTagName('script');
     for (let i = scripts.length - 1; i >= 0; i -= 1) {
       scripts[i].nodeValue="";
@@ -1429,14 +1500,18 @@ Readability.prototype = {
     // if (nextPageLink) {
     //   // Append any additional pages after a small timeout so that people
     //   // can start reading without having to wait for this to finish processing.
     //   setTimeout((function() {
     //     this._appendNextPage(nextPageLink);
     //   }).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 };
   }
 };
--- a/mobile/android/chrome/content/aboutReader.js
+++ b/mobile/android/chrome/content/aboutReader.js
@@ -344,16 +344,18 @@ AboutReader.prototype = {
         let json = JSON.stringify({ fromAboutReader: true, url: this._article.url });
         Services.obs.notifyObservers(null, "Reader:Add", json);
 
         gChromeWin.sendMessageToJava({
           type: "Reader:Added",
           result: result,
           title: this._article.title,
           url: this._article.url,
+          length: this._article.length,
+          excerpt: this._article.excerpt
         });
       }.bind(this));
     } else {
       // In addition to removing the article from the cache (handled in
       // browser.js), sending this message will cause the toggle button to be
       // updated (handled in this file).
       Services.obs.notifyObservers(null, "Reader:Remove", this._article.url);
     }
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -7428,63 +7428,67 @@ let Reader = {
         } else if ('url' in args) {
           let uri = Services.io.newURI(args.url, null, null);
           url = uri.spec;
           urlWithoutRef = uri.specIgnoringRef;
         } else {
           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;
           }
 
           if (tabID != null) {
             this.getArticleForTab(tabID, urlWithoutRef, handleArticle);
           } else {
             this.parseDocumentFromURL(urlWithoutRef, handleArticle);
           }
         }.bind(this));
         break;
       }
 
       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));
         break;
       }
 
       case "nsPref:changed": {
         if (aData.startsWith("reader.parse-on-load.")) {