diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js
index f5222d8014fd..85060c1cda81 100644
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -683,6 +683,20 @@ pref("browser.chrome.dynamictoolbar", true);
pref("webgl.disabled", true);
#endif
+// initial web feed readers list
+pref("browser.contentHandlers.types.0.title", "chrome://browser/locale/region.properties");
+pref("browser.contentHandlers.types.0.uri", "chrome://browser/locale/region.properties");
+pref("browser.contentHandlers.types.0.type", "application/vnd.mozilla.maybe.feed");
+pref("browser.contentHandlers.types.1.title", "chrome://browser/locale/region.properties");
+pref("browser.contentHandlers.types.1.uri", "chrome://browser/locale/region.properties");
+pref("browser.contentHandlers.types.1.type", "application/vnd.mozilla.maybe.feed");
+pref("browser.contentHandlers.types.2.title", "chrome://browser/locale/region.properties");
+pref("browser.contentHandlers.types.2.uri", "chrome://browser/locale/region.properties");
+pref("browser.contentHandlers.types.2.type", "application/vnd.mozilla.maybe.feed");
+pref("browser.contentHandlers.types.3.title", "chrome://browser/locale/region.properties");
+pref("browser.contentHandlers.types.3.uri", "chrome://browser/locale/region.properties");
+pref("browser.contentHandlers.types.3.type", "application/vnd.mozilla.maybe.feed");
+
#ifndef RELEASE_BUILD
// Enable Web Audio for Firefox for Android in Nightly and Aurora
pref("media.webaudio.enabled", true);
diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java
index 17ff376d283b..8a560d19de4e 100644
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -199,7 +199,7 @@ abstract public class BrowserApp extends GeckoApp
case PAGE_SHOW:
loadFavicon(tab);
break;
- case LINK_ADDED:
+ case LINK_FAVICON:
// If tab is not loading and the favicon is updated, we
// want to load the image straight away. If tab is still
// loading, we only load the favicon once the page's content
diff --git a/mobile/android/base/BrowserToolbar.java b/mobile/android/base/BrowserToolbar.java
index 793403cf4271..f51eaf91b0b2 100644
--- a/mobile/android/base/BrowserToolbar.java
+++ b/mobile/android/base/BrowserToolbar.java
@@ -204,11 +204,15 @@ public class BrowserToolbar implements ViewSwitcher.ViewFactory,
menu.findItem(R.id.share).setVisible(false);
menu.findItem(R.id.add_to_launcher).setVisible(false);
}
+ if (!tab.getFeedsEnabled()) {
+ menu.findItem(R.id.subscribe).setVisible(false);
+ }
} else {
// if there is no tab, remove anything tab dependent
menu.findItem(R.id.copyurl).setVisible(false);
menu.findItem(R.id.share).setVisible(false);
menu.findItem(R.id.add_to_launcher).setVisible(false);
+ menu.findItem(R.id.subscribe).setVisible(false);
}
}
});
diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java
index 5156e5cb1ea1..5e6d577a0df8 100644
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -2549,6 +2549,19 @@ abstract public class GeckoApp
shareCurrentUrl();
return true;
}
+ case R.id.subscribe: {
+ Tab tab = Tabs.getInstance().getSelectedTab();
+ if (tab != null && tab.getFeedsEnabled()) {
+ JSONObject args = new JSONObject();
+ try {
+ args.put("tabId", tab.getId());
+ } catch (JSONException e) {
+ Log.e(LOGTAG, "error building json arguments");
+ }
+ GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Feeds:Subscribe", args.toString()));
+ }
+ return true;
+ }
case R.id.copyurl: {
Tab tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
diff --git a/mobile/android/base/Tab.java b/mobile/android/base/Tab.java
index b3155b7f18d7..5a1552df6f8d 100644
--- a/mobile/android/base/Tab.java
+++ b/mobile/android/base/Tab.java
@@ -38,6 +38,7 @@ public class Tab {
private Bitmap mFavicon;
private String mFaviconUrl;
private int mFaviconSize;
+ private boolean mFeedsEnabled;
private JSONObject mIdentityData;
private boolean mReaderEnabled;
private BitmapDrawable mThumbnail;
@@ -76,6 +77,7 @@ public class Tab {
mFavicon = null;
mFaviconUrl = null;
mFaviconSize = 0;
+ mFeedsEnabled = false;
mIdentityData = null;
mReaderEnabled = false;
mEnteringReaderMode = false;
@@ -189,6 +191,10 @@ public class Tab {
return mFaviconUrl;
}
+ public boolean getFeedsEnabled() {
+ return mFeedsEnabled;
+ }
+
public String getSecurityMode() {
try {
return mIdentityData.getString("mode");
@@ -312,6 +318,10 @@ public class Tab {
mFaviconSize = 0;
}
+ public void setFeedsEnabled(boolean feedsEnabled) {
+ mFeedsEnabled = feedsEnabled;
+ }
+
public void updateIdentityData(JSONObject identityData) {
mIdentityData = identityData;
}
@@ -525,6 +535,7 @@ public class Tab {
setContentType(message.getString("contentType"));
clearFavicon();
+ setFeedsEnabled(false);
updateTitle(null);
updateIdentityData(null);
setReaderEnabled(false);
diff --git a/mobile/android/base/Tabs.java b/mobile/android/base/Tabs.java
index cc8818de9b64..e6c71370017e 100644
--- a/mobile/android/base/Tabs.java
+++ b/mobile/android/base/Tabs.java
@@ -84,7 +84,8 @@ public class Tabs implements GeckoEventListener {
registerEventListener("Content:PageShow");
registerEventListener("DOMContentLoaded");
registerEventListener("DOMTitleChanged");
- registerEventListener("DOMLinkAdded");
+ registerEventListener("Link:Favicon");
+ registerEventListener("Link:Feed");
registerEventListener("DesktopMode:Changed");
}
@@ -437,9 +438,12 @@ public class Tabs implements GeckoEventListener {
notifyListeners(tab, Tabs.TabEvents.LOADED);
} else if (event.equals("DOMTitleChanged")) {
tab.updateTitle(message.getString("title"));
- } else if (event.equals("DOMLinkAdded")) {
+ } else if (event.equals("Link:Favicon")) {
tab.updateFaviconURL(message.getString("href"), message.getInt("size"));
- notifyListeners(tab, TabEvents.LINK_ADDED);
+ notifyListeners(tab, TabEvents.LINK_FAVICON);
+ } else if (event.equals("Link:Feed")) {
+ tab.setFeedsEnabled(true);
+ notifyListeners(tab, TabEvents.LINK_FEED);
} else if (event.equals("DesktopMode:Changed")) {
tab.setDesktopMode(message.getBoolean("desktopMode"));
notifyListeners(tab, TabEvents.DESKTOP_MODE_CHANGE);
@@ -492,7 +496,8 @@ public class Tabs implements GeckoEventListener {
LOCATION_CHANGE,
MENU_UPDATED,
PAGE_SHOW,
- LINK_ADDED,
+ LINK_FAVICON,
+ LINK_FEED,
SECURITY_CHANGE,
READER_ENABLED,
DESKTOP_MODE_CHANGE
diff --git a/mobile/android/base/locales/en-US/android_strings.dtd b/mobile/android/base/locales/en-US/android_strings.dtd
index 0527f57e4498..8ea11cc1cbd6 100644
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -164,6 +164,7 @@ size. -->
+
diff --git a/mobile/android/base/resources/menu/titlebar_contextmenu.xml b/mobile/android/base/resources/menu/titlebar_contextmenu.xml
index 31735895815e..d0bab72bdd4f 100644
--- a/mobile/android/base/resources/menu/titlebar_contextmenu.xml
+++ b/mobile/android/base/resources/menu/titlebar_contextmenu.xml
@@ -14,6 +14,9 @@
+
+
diff --git a/mobile/android/base/strings.xml.in b/mobile/android/base/strings.xml.in
index 9a594473d19f..bc9ff043dc07 100644
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -171,6 +171,7 @@
&contextmenu_paste;
&contextmenu_copyurl;
&contextmenu_edit_bookmark;
+ &contextmenu_subscribe;
&history_removed;
diff --git a/mobile/android/chrome/content/FeedHandler.js b/mobile/android/chrome/content/FeedHandler.js
new file mode 100644
index 000000000000..0cb513aaa28a
--- /dev/null
+++ b/mobile/android/chrome/content/FeedHandler.js
@@ -0,0 +1,140 @@
+/* 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/. */
+"use strict";
+
+var FeedHandler = {
+ PREF_CONTENTHANDLERS_BRANCH: "browser.contentHandlers.types.",
+ TYPE_MAYBE_FEED: "application/vnd.mozilla.maybe.feed",
+
+ _contentTypes: null,
+
+ getContentHandlers: function fh_getContentHandlers(contentType) {
+ if (!this._contentTypes)
+ this.loadContentHandlers();
+
+ if (!(contentType in this._contentTypes))
+ return [];
+
+ return this._contentTypes[contentType];
+ },
+
+ loadContentHandlers: function fh_loadContentHandlers() {
+ this._contentTypes = {};
+
+ let kids = Services.prefs.getBranch(this.PREF_CONTENTHANDLERS_BRANCH).getChildList("");
+
+ // First get the numbers of the providers by getting all ###.uri prefs
+ let nums = [];
+ for (let i = 0; i < kids.length; i++) {
+ let match = /^(\d+)\.uri$/.exec(kids[i]);
+ if (!match)
+ continue;
+ else
+ nums.push(match[1]);
+ }
+
+ // Sort them, to get them back in order
+ nums.sort(function(a, b) { return a - b; });
+
+ // Now register them
+ for (let i = 0; i < nums.length; i++) {
+ let branch = Services.prefs.getBranch(this.PREF_CONTENTHANDLERS_BRANCH + nums[i] + ".");
+ let vals = branch.getChildList("");
+ if (vals.length == 0)
+ return;
+
+ try {
+ let type = branch.getCharPref("type");
+ let uri = branch.getComplexValue("uri", Ci.nsIPrefLocalizedString).data;
+ let title = branch.getComplexValue("title", Ci.nsIPrefLocalizedString).data;
+
+ if (!(type in this._contentTypes))
+ this._contentTypes[type] = [];
+ this._contentTypes[type].push({ contentType: type, uri: uri, name: title });
+ }
+ catch(ex) {}
+ }
+ },
+
+ observe: function fh_observe(aSubject, aTopic, aData) {
+ if (aTopic === "Feeds:Subscribe") {
+ let args = JSON.parse(aData);
+ let tab = BrowserApp.getTabForId(args.tabId);
+ if (!tab)
+ return;
+
+ let browser = tab.browser;
+ let feeds = browser.feeds;
+ if (feeds == null)
+ return;
+
+ // First, let's decide on which feed to subscribe
+ let feedIndex = -1;
+ if (feeds.length > 1) {
+ // JSON for Prompt
+ let feedResult = {
+ type: "Prompt:Show",
+ multiple: false,
+ selected: [],
+ listitems: []
+ };
+
+ // Build the list of feeds
+ for (let i = 0; i < feeds.length; i++) {
+ let item = {
+ label: feeds[i].title || feeds[i].href,
+ isGroup: false,
+ inGroup: false,
+ disabled: false,
+ id: i
+ };
+ feedResult.listitems.push(item);
+ }
+ feedIndex = JSON.parse(sendMessageToJava(feedResult)).button;
+ } else {
+ // Only a single feed on the page
+ feedIndex = 0;
+ }
+
+ if (feedIndex == -1)
+ return;
+ let feedURL = feeds[feedIndex].href;
+
+ // Next, we decide on which service to send the feed
+ let handlers = this.getContentHandlers(this.TYPE_MAYBE_FEED);
+ if (handlers.length == 0)
+ return;
+
+ // JSON for Prompt
+ let handlerResult = {
+ type: "Prompt:Show",
+ multiple: false,
+ selected: [],
+ listitems: []
+ };
+
+ // Build the list of handlers
+ for (let i = 0; i < handlers.length; ++i) {
+ let item = {
+ label: handlers[i].name,
+ isGroup: false,
+ inGroup: false,
+ disabled: false,
+ id: i
+ };
+ handlerResult.listitems.push(item);
+ }
+ let handlerIndex = JSON.parse(sendMessageToJava(handlerResult)).button;
+ if (handlerIndex == -1)
+ return;
+
+ // Merge the handler URL and the feed URL
+ let readerURL = handlers[handlerIndex].uri;
+ readerURL = readerURL.replace(/%s/gi, encodeURIComponent(feedURL));
+
+ // Open the resultant URL in a new tab
+ BrowserApp.addTab(readerURL, { parentId: BrowserApp.selectedTab.id });
+ }
+ }
+};
diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js
index 47564db55d1d..541c1aa363ac 100644
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -95,6 +95,7 @@ var LazyNotificationGetter = {
["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"],
["FindHelper", ["FindInPage:Find", "FindInPage:Prev", "FindInPage:Next", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"],
["PermissionsHelper", ["Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"],
+ ["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"],
].forEach(function (aScript) {
let [name, notifications, script] = aScript;
XPCOMUtils.defineLazyGetter(window, name, function() {
@@ -3017,11 +3018,11 @@ Tab.prototype = {
if (!target.href || target.disabled)
return;
- // ignore on frames and other documents
+ // Ignore on frames and other documents
if (target.ownerDocument != this.browser.contentDocument)
return;
- // sanitize the rel string
+ // Sanitize the rel string
let list = [];
if (target.rel) {
list = target.rel.toLowerCase().split(/\s+/);
@@ -3032,42 +3033,60 @@ Tab.prototype = {
list.push("[" + rel + "]");
}
- // We only care about icon links
- if (list.indexOf("[icon]") == -1)
- return;
+ if (list.indexOf("[icon]") != -1) {
+ // We want to get the largest icon size possible for our UI.
+ let maxSize = 0;
- // We want to get the largest icon size possible for our UI.
- let maxSize = 0;
+ // We use the sizes attribute if available
+ // see http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon
+ if (target.hasAttribute("sizes")) {
+ let sizes = target.getAttribute("sizes").toLowerCase();
- // We use the sizes attribute if available
- // see http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon
- if (target.hasAttribute("sizes")) {
- let sizes = target.getAttribute("sizes").toLowerCase();
-
- if (sizes == "any") {
- // Since Java expects an integer, use -1 to represent icons with sizes="any"
- maxSize = -1;
- } else {
- let tokens = sizes.split(" ");
- tokens.forEach(function(token) {
- // TODO: check for invalid tokens
- let [w, h] = token.split("x");
- maxSize = Math.max(maxSize, Math.max(w, h));
- });
+ if (sizes == "any") {
+ // Since Java expects an integer, use -1 to represent icons with sizes="any"
+ maxSize = -1;
+ } else {
+ let tokens = sizes.split(" ");
+ tokens.forEach(function(token) {
+ // TODO: check for invalid tokens
+ let [w, h] = token.split("x");
+ maxSize = Math.max(maxSize, Math.max(w, h));
+ });
+ }
}
+
+ let json = {
+ type: "Link:Favicon",
+ tabID: this.id,
+ href: resolveGeckoURI(target.href),
+ charset: target.ownerDocument.characterSet,
+ title: target.title,
+ rel: list.join(" "),
+ size: maxSize
+ };
+ sendMessageToJava(json);
+ } else if (list.indexOf("[alternate]") != -1) {
+ let type = target.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, "");
+ let isFeed = (type == "application/rss+xml" || type == "application/atom+xml");
+
+ if (!isFeed)
+ return;
+
+ try {
+ // urlSecurityCeck will throw if things are not OK
+ ContentAreaUtils.urlSecurityCheck(target.href, target.ownerDocument.nodePrincipal, Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
+
+ if (!this.browser.feeds)
+ this.browser.feeds = [];
+ this.browser.feeds.push({ href: target.href, title: target.title, type: type });
+
+ let json = {
+ type: "Link:Feed",
+ tabID: this.id
+ };
+ sendMessageToJava(json);
+ } catch (e) {}
}
-
- let json = {
- type: "DOMLinkAdded",
- tabID: this.id,
- href: resolveGeckoURI(target.href),
- charset: target.ownerDocument.characterSet,
- title: target.title,
- rel: list.join(" "),
- size: maxSize
- };
-
- sendMessageToJava(json);
break;
}
diff --git a/mobile/android/chrome/jar.mn b/mobile/android/chrome/jar.mn
index 055cbbbf3a8e..284ad8d4cd94 100644
--- a/mobile/android/chrome/jar.mn
+++ b/mobile/android/chrome/jar.mn
@@ -48,6 +48,7 @@ chrome.jar:
content/MasterPassword.js (content/MasterPassword.js)
content/FindHelper.js (content/FindHelper.js)
content/PermissionsHelper.js (content/PermissionsHelper.js)
+ content/FeedHandler.js (content/FeedHandler.js)
% content branding %content/branding/
diff --git a/mobile/locales/en-US/chrome/region.properties b/mobile/locales/en-US/chrome/region.properties
index 167c7e33916e..dcec7a0deb85 100644
--- a/mobile/locales/en-US/chrome/region.properties
+++ b/mobile/locales/en-US/chrome/region.properties
@@ -24,3 +24,10 @@ gecko.handlerService.schemes.mailto.0.name=Yahoo! Mail
gecko.handlerService.schemes.mailto.0.uriTemplate=http://compose.mail.yahoo.com/?To=%s
gecko.handlerService.schemes.mailto.1.name=Gmail
gecko.handlerService.schemes.mailto.1.uriTemplate=https://mail.google.com/mail/?extsrc=mailto&url=%s
+
+# This is the default set of web based feed handlers shown in the reader
+# selection UI
+browser.contentHandlers.types.0.title=My Yahoo!
+browser.contentHandlers.types.0.uri=http://add.my.yahoo.com/rss?url=%s
+browser.contentHandlers.types.1.title=Google
+browser.contentHandlers.types.1.uri=http://fusion.google.com/add?feedurl=%s