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