author | Carsten "Tomcat" Book <cbook@mozilla.com> |
Fri, 15 May 2015 17:41:01 +0200 | |
changeset 244041 | 5943d32f35155feb6144f5b06d7e413888d9072e |
parent 244000 | ca67ae37b6113ae9a327eaacc7bd4c182d14faaf (current diff) |
parent 244040 | 1a8343f8ed8336cbba1b236ab9725012c6c73179 (diff) |
child 244042 | 2937420a763331605538d39e7b6d4caf1dd0cd10 |
push id | 59820 |
push user | cbook@mozilla.com |
push date | Fri, 15 May 2015 15:41:47 +0000 |
treeherder | mozilla-inbound@5943d32f3515 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 41.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
|
--- a/b2g/app/b2g.js +++ b/b2g/app/b2g.js @@ -986,17 +986,17 @@ pref("gfx.canvas.skiagl.dynamic-cache", // Limit skia to canvases the size of the device screen or smaller pref("gfx.canvas.max-size-for-skia-gl", -1); // enable fence with readpixels for SurfaceStream pref("gfx.gralloc.fence-with-readpixels", true); // The url of the page used to display network error details. -pref("b2g.neterror.url", "app://system.gaiamobile.org/net_error.html"); +pref("b2g.neterror.url", "net_error.html"); // The origin used for the shared themes uri space. pref("b2g.theme.origin", "app://theme.gaiamobile.org"); pref("dom.mozApps.themable", true); pref("dom.mozApps.selected_theme", "default_theme.gaiamobile.org"); // Enable PAC generator for B2G. pref("network.proxy.pac_generator", true);
--- a/b2g/chrome/content/shell.js +++ b/b2g/chrome/content/shell.js @@ -631,17 +631,35 @@ var shell = { Services.obs.notifyObservers(null, "browser-ui-startup-complete", ""); SystemAppProxy.setIsReady(); if ('pendingChromeEvents' in shell) { shell.pendingChromeEvents.forEach((shell.sendChromeEvent).bind(shell)); } delete shell.pendingChromeEvents; }); - } + + shell.handleCmdLine(); + }, + + handleCmdLine: function shell_handleCmdLine() { + let b2gcmds = Cc["@mozilla.org/commandlinehandler/general-startup;1?type=b2gcmds"] + .getService(Ci.nsISupports); + let args = b2gcmds.wrappedJSObject.cmdLine; + try { + // Returns null if -url is not present + let url = args.handleFlagWithParam("url", false); + if (url) { + this.sendChromeEvent({type: "mozbrowseropenwindow", url}); + args.preventDefault = true; + } + } catch(e) { + // Throws if -url is present with no params + } + }, }; Services.obs.addObserver(function onFullscreenOriginChange(subject, topic, data) { shell.sendChromeEvent({ type: "fullscreenoriginchange", fullscreenorigin: data }); }, "fullscreen-origin-change", false); DOMApplicationRegistry.registryStarted.then(function () {
--- a/b2g/components/B2GAboutRedirector.js +++ b/b2g/components/B2GAboutRedirector.js @@ -7,21 +7,21 @@ const Ci = Components.interfaces; Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); Components.utils.import("resource://gre/modules/Services.jsm"); function debug(msg) { //dump("B2GAboutRedirector: " + msg + "\n"); } function netErrorURL() { - let uri = "app://system.gaiamobile.org/net_error.html"; - try { - uri = Services.prefs.getCharPref("b2g.neterror.url"); - } catch(e) {} - return uri; + let systemManifestURL = Services.prefs.getCharPref("b2g.system_manifest_url"); + systemManifestURL = Services.io.newURI(systemManifestURL, null, null); + let netErrorURL = Services.prefs.getCharPref("b2g.neterror.url"); + netErrorURL = Services.io.newURI(netErrorURL, null, systemManifestURL); + return netErrorURL.spec; } let modules = { certerror: { uri: "chrome://b2g/content/aboutCertError.xhtml", privileged: false, hide: true },
--- a/b2g/components/CommandLine.js +++ b/b2g/components/CommandLine.js @@ -10,16 +10,20 @@ XPCOMUtils.defineLazyModuleGetter(this, function CommandlineHandler() { this.wrappedJSObject = this; } CommandlineHandler.prototype = { handle: function(cmdLine) { this.cmdLine = cmdLine; + let win = Services.wm.getMostRecentWindow("navigator:browser"); + if (win && win.shell) { + win.shell.handleCmdLine(); + } }, helpInfo: "", classID: Components.ID("{385993fe-8710-4621-9fb1-00a09d8bec37}"), QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]), }; this.NSGetFactory = XPCOMUtils.generateNSGetFactory([CommandlineHandler]);
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1327,16 +1327,18 @@ pref("services.sync.prefs.sync.signon.re pref("services.sync.prefs.sync.spellchecker.dictionary", true); pref("services.sync.prefs.sync.xpinstall.whitelist.required", true); #endif // Developer edition preferences #ifdef MOZ_DEV_EDITION sticky_pref("lightweightThemes.selectedThemeID", "firefox-devedition@mozilla.org"); sticky_pref("browser.devedition.theme.enabled", true); +#else +sticky_pref("lightweightThemes.selectedThemeID", ""); #endif // Developer edition promo preferences pref("devtools.devedition.promo.shown", false); pref("devtools.devedition.promo.url", "https://www.mozilla.org/firefox/developer/?utm_source=firefox-dev-tools&utm_medium=firefox-browser&utm_content=betadoorhanger"); // Only potentially show in beta release #if MOZ_UPDATE_CHANNEL == beta
--- a/browser/base/content/browser-context.inc +++ b/browser/base/content/browser-context.inc @@ -74,16 +74,20 @@ <menuitem id="context-sharelink" label="&shareLink.label;" accesskey="&shareLink.accesskey;" oncommand="gContextMenu.shareLink();"/> <menuitem id="context-savelink" label="&saveLinkCmd.label;" accesskey="&saveLinkCmd.accesskey;" oncommand="gContextMenu.saveLink();"/> + <menuitem id="context-savelinktopocket" + label="&saveLinkToPocketCmd.label;" + accesskey="&saveLinkToPocketCmd.accesskey;" + oncommand="gContextMenu.saveLinkToPocket();"/> <menu id="context-marklinkMenu" label="&social.marklinkMenu.label;" accesskey="&social.marklinkMenu.accesskey;"> <menupopup/> </menu> <menuitem id="context-copyemail" label="©EmailCmd.label;" accesskey="©EmailCmd.accesskey;" oncommand="gContextMenu.copyEmail();"/> @@ -264,17 +268,17 @@ oncommand="SocialShare.sharePage();"/> <menuitem id="context-savepage" label="&savePageCmd.label;" accesskey="&savePageCmd.accesskey2;" oncommand="gContextMenu.savePageAs();"/> <menuitem id="context-pocket" label="&saveToPocketCmd.label;" accesskey="&saveToPocketCmd.accesskey;" - oncommand="gContextMenu.saveToPocket();"/> + oncommand="gContextMenu.savePageToPocket();"/> <menu id="context-markpageMenu" label="&social.markpageMenu.label;" accesskey="&social.markpageMenu.accesskey;"> <menupopup/> </menu> <menuseparator id="context-sep-viewbgimage"/> <menuitem id="context-viewbgimage" label="&viewBGImageCmd.label;" accesskey="&viewBGImageCmd.accesskey;"
--- a/browser/base/content/browser-pocket-de.properties +++ b/browser/base/content/browser-pocket-de.properties @@ -6,9 +6,11 @@ # browser.properties in the usual L10N location. pocket-button.label = Pocket pocket-button.tooltiptext = Bei Pocket speichern # From browser-pocket.dtd saveToPocketCmd.label = Seite bei Pocket speichern saveToPocketCmd.accesskey = k +saveLinkToPocketCmd.label = Link in Pocket speichern +saveLinkToPocketCmd.accesskey = o pocketMenuitem.label = Pocket-Liste anzeigen
--- a/browser/base/content/browser-pocket-es-ES.properties +++ b/browser/base/content/browser-pocket-es-ES.properties @@ -6,9 +6,11 @@ # browser.properties in the usual L10N location. pocket-button.label = Pocket pocket-button.tooltiptext = Guardar en Pocket # From browser-pocket.dtd saveToPocketCmd.label = Guardar página en Pocket saveToPocketCmd.accesskey = k +saveLinkToPocketCmd.label = Guardar enlace en Pocket +saveLinkToPocketCmd.accesskey = k pocketMenuitem.label = Ver lista de Pocket
--- a/browser/base/content/browser-pocket-ja.properties +++ b/browser/base/content/browser-pocket-ja.properties @@ -6,9 +6,11 @@ # browser.properties in the usual L10N location. pocket-button.label = Pocket pocket-button.tooltiptext = Pocket に保存 # From browser-pocket.dtd saveToPocketCmd.label = Pocket にページを保存 saveToPocketCmd.accesskey = k +saveLinkToPocketCmd.label = Pocket にリンクを保存 +saveLinkToPocketCmd.accesskey = o pocketMenuitem.label = Pocket のマイリストを表示
--- a/browser/base/content/browser-pocket-ru.properties +++ b/browser/base/content/browser-pocket-ru.properties @@ -6,9 +6,11 @@ # browser.properties in the usual L10N location. pocket-button.label = Pocket pocket-button.tooltiptext = Сохранить в Pocket # From browser-pocket.dtd saveToPocketCmd.label = Сохранить страницу в Pocket saveToPocketCmd.accesskey = х +saveLinkToPocketCmd.label = Сохранить ссылку в Pocket +saveLinkToPocketCmd.accesskey = P pocketMenuitem.label = Показать список Pocket
--- a/browser/base/content/browser-pocket.dtd +++ b/browser/base/content/browser-pocket.dtd @@ -2,9 +2,11 @@ - 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/. --> <!-- This is a temporary file and not meant for localization; later versions - of Firefox include these strings in browser.dtd --> <!ENTITY saveToPocketCmd.label "Save Page to Pocket"> <!ENTITY saveToPocketCmd.accesskey "k"> +<!ENTITY saveLinkToPocketCmd.label "Save Link to Pocket"> +<!ENTITY saveLinkToPocketCmd.accesskey "o"> <!ENTITY pocketMenuitem.label "View Pocket List">
--- a/browser/base/content/nsContextMenu.js +++ b/browser/base/content/nsContextMenu.js @@ -1,16 +1,22 @@ /* vim: set ts=2 sw=2 sts=2 et tw=80: */ # 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/. Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); Components.utils.import("resource://gre/modules/InlineSpellChecker.jsm"); Components.utils.import("resource://gre/modules/BrowserUtils.jsm"); +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", + "resource:///modules/CustomizableUI.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "Pocket", + "resource:///modules/Pocket.jsm"); var gContextMenuContentData = null; function nsContextMenu(aXulMenu, aIsShift) { this.shouldDisplay = true; this.initMenu(aXulMenu, aIsShift); } @@ -173,46 +179,57 @@ nsContextMenu.prototype = { // to be already loaded, since we load it on startup in nsBrowserGlue, // and CastingApps isn't, so check SimpleServiceDiscovery.services first // to avoid needing to load CastingApps.jsm if we don't need to. shouldShowCast = shouldShowCast && this.mediaURL && SimpleServiceDiscovery.services.length > 0 && CastingApps.getServicesForVideo(this.target).length > 0; this.setItemAttr("context-castvideo", "disabled", !shouldShowCast); - let canPocket = false; - if (shouldShow && window.gBrowser && - this.browser.getTabBrowser() == window.gBrowser) { - let uri = this.browser.currentURI; - canPocket = - CustomizableUI.getPlacementOfWidget("pocket-button") && - (uri.schemeIs("http") || uri.schemeIs("https") || - (uri.schemeIs("about") && ReaderMode.getOriginalUrl(uri.spec))); - if (canPocket) { - let locale = Cc["@mozilla.org/chrome/chrome-registry;1"]. - getService(Ci.nsIXULChromeRegistry). - getSelectedLocale("browser"); - if (locale != "en-US") { - if (locale == "ja-JP-mac") - locale = "ja"; - let url = "chrome://browser/content/browser-pocket-" + locale + ".properties"; - let bundle = Services.strings.createBundle(url); - let item = document.getElementById("context-pocket"); - try { - item.setAttribute("label", bundle.GetStringFromName("saveToPocketCmd.label")); - item.setAttribute("accesskey", bundle.GetStringFromName("saveToPocketCmd.accesskey")); - } catch (err) { - // GetStringFromName throws when the bundle doesn't exist. In that - // case, the item will retain the browser-pocket.dtd en-US string that - // it has in the markup. - } + this.initPocketItems(); + }, + + initPocketItems: function CM_initPocketItems() { + var showSaveCurrentPageToPocket = !(this.onTextInput || this.onLink || + this.isContentSelected || this.onImage || + this.onCanvas || this.onVideo || this.onAudio); + let targetURI = (this.onSaveableLink || this.onPlainTextLink) ? this.linkURI : this.browser.currentURI; + let canPocket = CustomizableUI.getPlacementOfWidget("pocket-button") && + window.pktApi && window.pktApi.isUserLoggedIn(); + canPocket = canPocket && (targetURI.schemeIs("http") || targetURI.schemeIs("https") || + (targetURI.schemeIs("about") && ReaderMode.getOriginalUrl(targetURI.spec))); + canPocket = canPocket && window.gBrowser && this.browser.getTabBrowser() == window.gBrowser; + + if (canPocket) { + let locale = Cc["@mozilla.org/chrome/chrome-registry;1"]. + getService(Ci.nsIXULChromeRegistry). + getSelectedLocale("browser"); + if (locale != "en-US") { + if (locale == "ja-JP-mac") + locale = "ja"; + let url = "chrome://browser/content/browser-pocket-" + locale + ".properties"; + let bundle = Services.strings.createBundle(url); + let saveToPocketItem = document.getElementById("context-pocket"); + let saveLinkToPocketItem = document.getElementById("context-savelinktopocket"); + try { + saveToPocketItem.setAttribute("label", bundle.GetStringFromName("saveToPocketCmd.label")); + saveToPocketItem.setAttribute("accesskey", bundle.GetStringFromName("saveToPocketCmd.accesskey")); + saveLinkToPocketItem.setAttribute("label", bundle.GetStringFromName("saveLinkToPocketCmd.label")); + saveLinkToPocketItem.setAttribute("accesskey", bundle.GetStringFromName("saveLinkToPocketCmd.accesskey")); + } catch (err) { + // GetStringFromName throws when the bundle doesn't exist. In that + // case, the item will retain the browser-pocket.dtd en-US string that + // it has in the markup. } } } - this.showItem("context-pocket", canPocket && window.pktApi && window.pktApi.isUserLoggedIn()); + this.showItem("context-pocket", canPocket && showSaveCurrentPageToPocket); + let showSaveLinkToPocket = canPocket && !showSaveCurrentPageToPocket && + (this.onSaveableLink || this.onPlainTextLink); + this.showItem("context-savelinktopocket", showSaveLinkToPocket); }, initViewItems: function CM_initViewItems() { // View source is always OK, unless in directory listing. this.showItem("context-viewpartialsource-selection", this.isContentSelected); this.showItem("context-viewpartialsource-mathml", this.onMathML && !this.isContentSelected); @@ -1656,30 +1673,22 @@ nsContextMenu.prototype = { shareSelect: function CM_shareSelect() { SocialShare.sharePage(null, { url: this.browser.currentURI.spec, text: this.textSelected }, this.target); }, savePageAs: function CM_savePageAs() { saveDocument(this.browser.contentDocumentAsCPOW); }, - saveToPocket: function CM_saveToPocket() { - let pocketWidget = document.getElementById("pocket-button"); - let placement = CustomizableUI.getPlacementOfWidget("pocket-button"); - if (!placement) - return; + saveLinkToPocket: function CM_saveLinkToPocket() { + Pocket.savePage(this.browser, this.linkURL); + }, - if (placement.area == CustomizableUI.AREA_PANEL) { - PanelUI.show().then(function() { - pocketWidget = document.getElementById("pocket-button"); - pocketWidget.doCommand(); - }); - } else { - pocketWidget.doCommand(); - } + savePageToPocket: function CM_saveToPocket() { + Pocket.savePage(this.browser, this.browser.currentURI.spec, this.browser.contentTitle); }, printFrame: function CM_printFrame() { PrintUtils.print(this.target.ownerDocument.defaultView, this.browser); }, switchPageDirection: function CM_switchPageDirection() { this.browser.messageManager.sendAsyncMessage("SwitchDocumentDirection");
--- a/browser/base/content/test/plugins/browser.ini +++ b/browser/base/content/test/plugins/browser.ini @@ -50,17 +50,16 @@ support-files = [browser_clearplugindata.js] skip-if = e10s # bug 1149253 [browser_CTP_context_menu.js] skip-if = toolkit == "gtk2" || toolkit == "gtk3" # fails intermittently on Linux (bug 909342) [browser_CTP_crashreporting.js] skip-if = !crashreporter [browser_CTP_data_urls.js] [browser_CTP_drag_drop.js] -skip-if = e10s # misc. issues, bug 1156871 [browser_CTP_hide_overlay.js] [browser_CTP_iframe.js] skip-if = os == 'linux' || os == 'mac' # Bug 984821 [browser_CTP_multi_allow.js] [browser_CTP_nonplugins.js] [browser_CTP_notificationBar.js] [browser_CTP_outsideScrollArea.js] [browser_CTP_remove_navigate.js]
--- a/browser/components/customizableui/content/panelUI.inc.xul +++ b/browser/components/customizableui/content/panelUI.inc.xul @@ -230,17 +230,16 @@ </vbox> <button id="PanelUI-panic-view-button" label="&panicButton.view.forgetButton;"/> </vbox> </panelview> <panelview id="PanelUI-pocketView" flex="1"> <vbox class="panel-subview-body"> - <iframe id="pocket-panel-iframe" type="content"/> </vbox> </panelview> </panelmultiview> <!-- These menupopups are located here to prevent flickering, see bug 492960 comment 20. --> <menupopup id="customizationPanelItemContextMenu">
--- a/browser/components/customizableui/test/browser_1007336_lwthemes_in_customize_mode.js +++ b/browser/components/customizableui/test/browser_1007336_lwthemes_in_customize_mode.js @@ -46,17 +46,17 @@ add_task(function () { ok(installedThemeId.startsWith(firstLWThemeId), "The second theme in the 'My Themes' section should be the newly installed theme: " + "Installed theme id: " + installedThemeId + "; First theme ID: " + firstLWThemeId); is(header.nextSibling.nextSibling.nextSibling, recommendedHeader, "There should be two themes in the 'My Themes' section"); let defaultTheme = header.nextSibling; defaultTheme.doCommand(); - is(Services.prefs.prefHasUserValue("lightweightThemes.selectedThemeID"), false, "No lwtheme should be selected"); + is(Services.prefs.getCharPref("lightweightThemes.selectedThemeID"), "", "No lwtheme should be selected"); }); add_task(function asyncCleanup() { yield endCustomizing(); Services.prefs.clearUserPref("lightweightThemes.usedThemes"); Services.prefs.clearUserPref("lightweightThemes.recommendedThemes"); }) \ No newline at end of file
--- a/browser/components/pocket/Pocket.jsm +++ b/browser/components/pocket/Pocket.jsm @@ -20,40 +20,47 @@ let Pocket = { get listURL() { return "https://" + Pocket.site + "/?src=ff_ext"; }, /** * Functions related to the Pocket panel UI. */ onPanelViewShowing(event) { let document = event.target.ownerDocument; let window = document.defaultView; - let iframe = document.getElementById('pocket-panel-iframe'); + let iframe = window.pktUI.getPanelFrame(); + let urlToSave = Pocket._urlToSave; + let titleToSave = Pocket._titleToSave; + Pocket._urlToSave = null; + Pocket._titleToSave = null; // ViewShowing fires immediately before it creates the contents, // in lieu of an AfterViewShowing event, just spin the event loop. window.setTimeout(function() { - window.pktUI.pocketButtonOnCommand(); + if (urlToSave) { + window.pktUI.tryToSaveUrl(urlToSave, titleToSave); + } else { + window.pktUI.pocketButtonOnCommand(); + } if (iframe.contentDocument && - iframe.contentDocument.readyState == "complete") - { + iframe.contentDocument.readyState == "complete") { window.pktUI.pocketPanelDidShow(); } else { // iframe didn't load yet. This seems to always be the case when in // the toolbar panel, but never the case for a subview. // XXX this only being fired when it's a _capturing_ listener! iframe.addEventListener("load", Pocket.onFrameLoaded, true); } }, 0); }, onFrameLoaded(event) { let document = event.currentTarget.ownerDocument; let window = document.defaultView; - let iframe = document.getElementById('pocket-panel-iframe'); + let iframe = window.pktUI.getPanelFrame(); iframe.removeEventListener("load", Pocket.onFrameLoaded, true); window.pktUI.pocketPanelDidShow(); }, onPanelViewHiding(event) { let window = event.target.ownerDocument.defaultView; window.pktUI.pocketPanelDidHide(event); @@ -77,9 +84,31 @@ let Pocket = { node.disabled = win.pktApi.isUserLoggedIn() && !locationURI.schemeIs("http") && !locationURI.schemeIs("https") && !(locationURI.schemeIs("about") && locationURI.spec.toLowerCase().startsWith("about:reader?url=")); } } }, + + _urlToSave: null, + _titleToSave: null, + savePage(browser, url, title) { + let document = browser.ownerDocument; + let pocketWidget = document.getElementById("pocket-button"); + let placement = CustomizableUI.getPlacementOfWidget("pocket-button"); + if (!placement) + return; + + this._urlToSave = url; + this._titleToSave = title; + if (placement.area == CustomizableUI.AREA_PANEL) { + let win = document.defaultView; + win.PanelUI.show().then(function() { + pocketWidget = document.getElementById("pocket-button"); + pocketWidget.doCommand(); + }); + } else { + pocketWidget.doCommand(); + } + }, };
--- a/browser/components/pocket/main.js +++ b/browser/components/pocket/main.js @@ -639,17 +639,25 @@ var pktUI = (function() { var panel = frame; while (panel && panel.localName != "panel") { panel = panel.parentNode; } return panel; } function getPanelFrame() { - return document.getElementById('pocket-panel-iframe'); + var frame = document.getElementById('pocket-panel-iframe'); + if (!frame) { + var frameParent = document.getElementById("PanelUI-pocketView").firstChild; + frame = document.createElement("iframe"); + frame.id = 'pocket-panel-iframe'; + frame.setAttribute("type", "content"); + frameParent.appendChild(frame); + } + return frame; } function getSubview() { var frame = getPanelFrame(); var view = frame; while (view && view.localName != "panelview") { view = view.parentNode; } @@ -774,16 +782,17 @@ var pktUI = (function() { } /** * Public functions */ return { onLoad: onLoad, + getPanelFrame: getPanelFrame, pocketButtonOnCommand: pocketButtonOnCommand, pocketPanelDidShow: pocketPanelDidShow, pocketPanelDidHide: pocketPanelDidHide, pocketContextSaveLinkOnCommand, pocketContextSavePageOnCommand, @@ -836,17 +845,17 @@ var pktUIMessaging = (function() { /** * Send a message to the panel's iframe */ function sendMessageToPanel(panelId, messageId, payload) { if (!isPanelIdValid(panelId)) { return; }; - var panelFrame = document.getElementById('pocket-panel-iframe'); + var panelFrame = pktUI.getPanelFrame(); if (!isPocketPanelFrameValid(panelFrame)) { return; } var doc = panelFrame.contentWindow.document; var documentElement = doc.documentElement; // Send message to panel var panelMessageId = prefixedMessageId(panelId + '_' + messageId);
--- a/browser/devtools/performance/test/browser.ini +++ b/browser/devtools/performance/test/browser.ini @@ -10,16 +10,17 @@ support-files = # Commented out tests are profiler tests # that need to be moved over to performance tool [browser_perf-aaa-run-first-leaktest.js] [browser_markers-gc.js] [browser_markers-parse-html.js] [browser_markers-timestamp.js] [browser_perf-allocations-to-samples.js] +[browser_perf-categories-js-calltree.js] [browser_perf-compatibility-01.js] [browser_perf-compatibility-02.js] [browser_perf-compatibility-03.js] [browser_perf-compatibility-04.js] [browser_perf-compatibility-05.js] [browser_perf-compatibility-06.js] [browser_perf-compatibility-07.js] [browser_perf-clear-01.js]
new file mode 100644 --- /dev/null +++ b/browser/devtools/performance/test/browser_perf-categories-js-calltree.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that the categories are shown in the js call tree when platform data + * is enabled. + */ +function spawnTest () { + let { panel } = yield initPerformance(SIMPLE_URL); + let { EVENTS, $, DetailsView, JsCallTreeView } = panel.panelWin; + + // Enable platform data to show the categories. + Services.prefs.setBoolPref(PLATFORM_DATA_PREF, true); + + yield startRecording(panel); + yield busyWait(100); + + let rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED); + yield stopRecording(panel); + yield DetailsView.selectView("js-calltree"); + yield rendered; + + is($(".call-tree-cells-container").hasAttribute("categories-hidden"), false, + "The call tree cells container should show the categories now."); + ok($(".call-tree-category[value=Gecko]"), + "A category node with the label `Gecko` is displayed in the tree."); + + // Disable platform data to show the categories. + Services.prefs.setBoolPref(PLATFORM_DATA_PREF, false); + + is($(".call-tree-cells-container").getAttribute("categories-hidden"), "", + "The call tree cells container should hide the categories now."); + ok(!$(".call-tree-category[value=Gecko]"), + "A category node with the label `Gecko` doesn't exist in the tree anymore."); + + yield teardown(panel); + finish(); +}
--- a/browser/devtools/performance/views/details-js-call-tree.js +++ b/browser/devtools/performance/views/details-js-call-tree.js @@ -118,17 +118,17 @@ let JsCallTreeView = Heritage.extend(Det // tests and JITOptimizationsView. root.on("focus", (_, node) => this.emit("focus", node)); // Clear out other call trees. this.container.innerHTML = ""; root.attachTo(this.container); // When platform data isn't shown, hide the cateogry labels, since they're - // only available for C++ frames. - root.toggleCategories(options.contentOnly); + // only available for C++ frames. Pass *false* to make them invisible. + root.toggleCategories(!options.contentOnly); // Return the CallView for tests return root; }, toString: () => "[object JsCallTreeView]" });
--- a/browser/devtools/shared/timeline/waterfall.js +++ b/browser/devtools/shared/timeline/waterfall.js @@ -17,17 +17,17 @@ loader.lazyImporter(this, "setNamedTimeo "resource:///modules/devtools/ViewHelpers.jsm"); loader.lazyImporter(this, "clearNamedTimeout", "resource:///modules/devtools/ViewHelpers.jsm"); loader.lazyRequireGetter(this, "EventEmitter", "devtools/toolkit/event-emitter"); const HTML_NS = "http://www.w3.org/1999/xhtml"; -const WATERFALL_SIDEBAR_WIDTH = 150; // px +const WATERFALL_SIDEBAR_WIDTH = 200; // px const WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT = 30; const WATERFALL_FLUSH_OUTSTANDING_MARKERS_DELAY = 75; // ms const FIND_OPTIMAL_TICK_INTERVAL_MAX_ITERS = 100; const WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms const WATERFALL_HEADER_TICKS_SPACING_MIN = 50; // px const WATERFALL_HEADER_TEXT_PADDING = 3; // px
--- a/browser/modules/DirectoryLinksProvider.jsm +++ b/browser/modules/DirectoryLinksProvider.jsm @@ -246,20 +246,21 @@ let DirectoryLinksProvider = { _removePrefsObserver: function DirectoryLinksProvider_removeObserver() { for (let pref in this._observedPrefs) { let prefName = this._observedPrefs[pref]; Services.prefs.removeObserver(prefName, this); } }, _cacheSuggestedLinks: function(link) { - if (!link.frecent_sites || "sponsored" == link.type) { - // Don't cache links that don't have the expected 'frecent_sites' or are sponsored. + // Don't cache links that don't have the expected 'frecent_sites' + if (!link.frecent_sites) { return; } + for (let suggestedSite of link.frecent_sites) { let suggestedMap = this._suggestedLinks.get(suggestedSite) || new Map(); suggestedMap.set(link.url, link); this._setupStartEndTime(link); this._suggestedLinks.set(suggestedSite, suggestedMap); } },
--- a/browser/modules/test/xpcshell/test_DirectoryLinksProvider.js +++ b/browser/modules/test/xpcshell/test_DirectoryLinksProvider.js @@ -404,16 +404,19 @@ add_task(function test_updateSuggestedTi yield promiseCleanDirectoryLinksProvider(); DirectoryLinksProvider.getFrecentSitesName = origGetFrecentSitesName; NewTabUtils.isTopPlacesSite = origIsTopPlacesSite; NewTabUtils.getProviderLinks = origGetProviderLinks; DirectoryLinksProvider._getCurrentTopSiteCount = origCurrentTopSiteCount; }); add_task(function test_suggestedLinksMap() { + let origGetFrecentSitesName = DirectoryLinksProvider.getFrecentSitesName; + DirectoryLinksProvider.getFrecentSitesName = () => "testing map"; + let data = {"suggested": [suggestedTile1, suggestedTile2, suggestedTile3, suggestedTile4], "directory": [someOtherSite]}; let dataURI = 'data:application/json,' + JSON.stringify(data); yield promiseSetupDirectoryLinksProvider({linksURL: dataURI}); let links = yield fetchData(); // Ensure the suggested tiles were not considered directory tiles. do_check_eq(links.length, 1); @@ -422,27 +425,34 @@ add_task(function test_suggestedLinksMap // Check for correctly saved suggested tiles data. expected_data = { "taxact.com": [suggestedTile1, suggestedTile2, suggestedTile3], "hrblock.com": [suggestedTile1, suggestedTile2], "1040.com": [suggestedTile1, suggestedTile3], "taxslayer.com": [suggestedTile1, suggestedTile2, suggestedTile3], "freetaxusa.com": [suggestedTile2, suggestedTile3], + "sponsoredtarget.com": [suggestedTile4], }; - do_check_eq([...DirectoryLinksProvider._suggestedLinks.keys()].indexOf("sponsoredtarget.com"), -1); + + let suggestedSites = [...DirectoryLinksProvider._suggestedLinks.keys()]; + do_check_eq(suggestedSites.indexOf("sponsoredtarget.com"), 5); + do_check_eq(suggestedSites.length, Object.keys(expected_data).length); DirectoryLinksProvider._suggestedLinks.forEach((suggestedLinks, site) => { let suggestedLinksItr = suggestedLinks.values(); for (let link of expected_data[site]) { - isIdentical(suggestedLinksItr.next().value, link); + let linkCopy = JSON.parse(JSON.stringify(link)); + linkCopy.targetedName = "testing map"; + isIdentical(suggestedLinksItr.next().value, linkCopy); } }) yield promiseCleanDirectoryLinksProvider(); + DirectoryLinksProvider.getFrecentSitesName = origGetFrecentSitesName; }); add_task(function test_topSitesWithSuggestedLinks() { let origGetFrecentSitesName = DirectoryLinksProvider.getFrecentSitesName; DirectoryLinksProvider.getFrecentSitesName = () => ""; let topSites = ["site0.com", "1040.com", "site2.com", "hrblock.com", "site4.com", "freetaxusa.com", "site6.com"]; let origIsTopPlacesSite = NewTabUtils.isTopPlacesSite;
--- a/dom/base/contentAreaDropListener.js +++ b/dom/base/contentAreaDropListener.js @@ -1,13 +1,14 @@ /* 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/. */ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); +Components.utils.import("resource://gre/modules/osfile.jsm"); const Cc = Components.classes; const Ci = Components.interfaces; // This component is used for handling dragover and drop of urls. // // It checks to see whether a drop of a url is allowed. For instance, a url // cannot be dropped if it is not a valid uri or the source of the drag cannot @@ -37,23 +38,19 @@ ContentAreaDropListener.prototype = case "text/x-moz-url": return dt.getData(type).split("\n"); } } // For shortcuts, we want to check for the file type last, so that the // url pointed to in one of the url types is found first before the file // type, which points to the actual file. - let file = dt.mozGetDataAt("application/x-moz-file", 0); - if (file instanceof Ci.nsIFile) { - let ioService = Cc["@mozilla.org/network/io-service;1"]. - getService(Ci.nsIIOService); - let fileHandler = ioService.getProtocolHandler("file") - .QueryInterface(Ci.nsIFileProtocolHandler); - return [fileHandler.getURLSpecFromFile(file), file.leafName]; + let files = dt.files; + if (files && files.length) { + return [OS.Path.toFileURI(files[0].mozFullPath), files[0].name]; } return [ ]; }, _validateURI: function(dataTransfer, uriString, disallowInherit) { if (!uriString)
--- a/dom/base/nsISelectionController.idl +++ b/dom/base/nsISelectionController.idl @@ -11,17 +11,17 @@ typedef short SelectionType; typedef short SelectionRegion; %} interface nsIContent; interface nsIDOMNode; interface nsISelection; interface nsISelectionDisplay; -[scriptable, uuid(7835DE46-DB36-4BB7-8684-1049A0C13049)] +[scriptable, uuid(82c3a9df-9bd6-4da2-b561-d85a9eec5caa)] interface nsISelectionController : nsISelectionDisplay { const short SELECTION_NONE=0; const short SELECTION_NORMAL=1; const short SELECTION_SPELLCHECK=2; const short SELECTION_IME_RAWINPUT=4; const short SELECTION_IME_SELECTEDRAWTEXT=8; const short SELECTION_IME_CONVERTEDTEXT=16; @@ -261,14 +261,19 @@ interface nsISelectionController : nsISe * @param aNode textNode to test * @param aStartOffset offset in dom to first char of textnode to test * @param aEndOffset offset in dom to last char of textnode to test * @param aReturnBool boolean returned TRUE if visible FALSE if not */ boolean checkVisibility(in nsIDOMNode node, in short startOffset, in short endOffset); [noscript,nostdcall] boolean checkVisibilityContent(in nsIContent node, in short startOffset, in short endOffset); + /** + * Returns the current visibility status of the selection carets, and allows + * the visibility to be turned off, or on (if a selection exists). + */ + attribute boolean selectionCaretsVisibility; }; %{ C++ #define NS_ISELECTIONCONTROLLER_CID \ { 0x513b9460, 0xd56a, 0x4c4e, \ { 0xb6, 0xf9, 0x0b, 0x8a, 0xe4, 0x37, 0x2a, 0x3b }} %}
--- a/dom/html/nsTextEditorState.cpp +++ b/dom/html/nsTextEditorState.cpp @@ -240,16 +240,19 @@ public: NS_IMETHOD CompleteMove(bool aForward, bool aExtend) override; NS_IMETHOD ScrollPage(bool aForward) override; NS_IMETHOD ScrollLine(bool aForward) override; NS_IMETHOD ScrollCharacter(bool aRight) override; NS_IMETHOD SelectAll(void) override; NS_IMETHOD CheckVisibility(nsIDOMNode *node, int16_t startOffset, int16_t EndOffset, bool* _retval) override; virtual nsresult CheckVisibilityContent(nsIContent* aNode, int16_t aStartOffset, int16_t aEndOffset, bool* aRetval) override; + NS_IMETHOD GetSelectionCaretsVisibility(bool* aOutVisibility) override; + NS_IMETHOD SetSelectionCaretsVisibility(bool aVisibility) override; + private: nsRefPtr<nsFrameSelection> mFrameSelection; nsCOMPtr<nsIContent> mLimiter; nsIScrollableFrame *mScrollFrame; nsWeakPtr mPresShellWeak; }; NS_IMPL_CYCLE_COLLECTING_ADDREF(nsTextInputSelectionImpl) @@ -635,16 +638,46 @@ nsTextInputSelectionImpl::CheckVisibilit if (shell) { return shell->CheckVisibility(node,startOffset,EndOffset, _retval); } return NS_ERROR_FAILURE; } +NS_IMETHODIMP +nsTextInputSelectionImpl::GetSelectionCaretsVisibility(bool* aOutVisibility) +{ + if (!mPresShellWeak) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult result; + nsCOMPtr<nsISelectionController> shell = do_QueryReferent(mPresShellWeak, &result); + if (shell) { + return shell->GetSelectionCaretsVisibility(aOutVisibility); + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsTextInputSelectionImpl::SetSelectionCaretsVisibility(bool aVisibility) +{ + if (!mPresShellWeak) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult result; + nsCOMPtr<nsISelectionController> shell = do_QueryReferent(mPresShellWeak, &result); + if (shell) { + return shell->SetSelectionCaretsVisibility(aVisibility); + } + return NS_ERROR_FAILURE; +} + nsresult nsTextInputSelectionImpl::CheckVisibilityContent(nsIContent* aNode, int16_t aStartOffset, int16_t aEndOffset, bool* aRetval) { if (!mPresShellWeak) { return NS_ERROR_NOT_INITIALIZED;
--- a/layout/base/SelectionCarets.cpp +++ b/layout/base/SelectionCarets.cpp @@ -52,54 +52,59 @@ static const char* kSelectionCaretsLogMo #define SELECTIONCARETS_LOG_STATIC(message, ...) \ PR_LOG(gSelectionCaretsLog, PR_LOG_DEBUG, \ ("SelectionCarets: %s:%d : " message "\n", __FUNCTION__, __LINE__, \ ##__VA_ARGS__)); // We treat mouse/touch move as "REAL" move event once its move distance // exceed this value, in CSS pixel. static const int32_t kMoveStartTolerancePx = 5; -// Time for trigger scroll end event, in miliseconds. -static const int32_t kScrollEndTimerDelay = 300; NS_IMPL_ISUPPORTS(SelectionCarets, nsIReflowObserver, nsISelectionListener, nsIScrollObserver, nsISupportsWeakReference) /*static*/ int32_t SelectionCarets::sSelectionCaretsInflateSize = 0; /*static*/ bool SelectionCarets::sSelectionCaretDetectsLongTap = true; +/*static*/ bool SelectionCarets::sCaretManagesAndroidActionbar = false; +/*static*/ bool SelectionCarets::sSelectionCaretObservesCompositions = false; SelectionCarets::SelectionCarets(nsIPresShell* aPresShell) : mPresShell(aPresShell) , mActiveTouchId(-1) , mCaretCenterToDownPointOffsetY(0) , mDragMode(NONE) , mUseAsyncPanZoom(false) , mInAsyncPanZoomGesture(false) , mEndCaretVisible(false) , mStartCaretVisible(false) , mSelectionVisibleInScrollFrames(true) , mVisible(false) + , mActionBarViewID(0) { MOZ_ASSERT(NS_IsMainThread()); if (!gSelectionCaretsLog) { gSelectionCaretsLog = PR_NewLogModule(kSelectionCaretsLogModuleName); } SELECTIONCARETS_LOG("Constructor, PresShell=%p", mPresShell); static bool addedPref = false; if (!addedPref) { Preferences::AddIntVarCache(&sSelectionCaretsInflateSize, "selectioncaret.inflatesize.threshold"); Preferences::AddBoolVarCache(&sSelectionCaretDetectsLongTap, "selectioncaret.detects.longtap", true); + Preferences::AddBoolVarCache(&sCaretManagesAndroidActionbar, + "caret.manages-android-actionbar"); + Preferences::AddBoolVarCache(&sSelectionCaretObservesCompositions, + "selectioncaret.observes.compositions"); addedPref = true; } } void SelectionCarets::Init() { nsPresContext* presContext = mPresShell->GetPresContext(); @@ -315,16 +320,21 @@ SelectionCarets::SetVisibility(bool aVis mVisible = aVisible; SELECTIONCARETS_LOG("Set visibility %s", (mVisible ? "shown" : "hidden")); dom::Element* startElement = mPresShell->GetSelectionCaretsStartElement(); SetElementVisibility(startElement, mVisible && mStartCaretVisible); dom::Element* endElement = mPresShell->GetSelectionCaretsEndElement(); SetElementVisibility(endElement, mVisible && mEndCaretVisible); + + // Update the Android Actionbar visibility if in use. + if (sCaretManagesAndroidActionbar) { + TouchCaret::UpdateAndroidActionBarVisibility(mVisible, mActionBarViewID); + } } void SelectionCarets::SetStartFrameVisibility(bool aVisible) { mStartCaretVisible = aVisible; SELECTIONCARETS_LOG("Set start frame visibility %s", (mStartCaretVisible ? "shown" : "hidden")); @@ -1122,22 +1132,56 @@ SelectionCarets::NotifySelectionChanged( { SELECTIONCARETS_LOG("aSel (%p), Reason=%d", aSel, aReason); if (aSel != GetSelection()) { SELECTIONCARETS_LOG("Return for selection mismatch!"); return NS_OK; } - if (!aReason || (aReason & (nsISelectionListener::DRAG_REASON | - nsISelectionListener::KEYPRESS_REASON | - nsISelectionListener::MOUSEDOWN_REASON))) { - SetVisibility(false); + // Update SelectionCaret visibility. + if (sSelectionCaretObservesCompositions) { + // When observing selection change notifications generated for example + // by Android soft-keyboard compositions, we can only obtain visibility + // after mouse-up by long-tap, or final caret-drag. + if (!mVisible) { + if (aReason & nsISelectionListener::MOUSEUP_REASON) { + UpdateSelectionCarets(); + } + } else { + // If already visible, we hide immediately for some known + // event-reasons: drag, keypress, or mouse down. + if (aReason & (nsISelectionListener::DRAG_REASON | + nsISelectionListener::KEYPRESS_REASON | + nsISelectionListener::MOUSEDOWN_REASON)) { + SetVisibility(false); + } else { + // Else we look further at the selection status, as currently + // style-composition changes don't provide reason codes. + UpdateSelectionCarets(); + } + } } else { - UpdateSelectionCarets(); + // Default logic, mainly employed by b2g, isn't aware of soft-keyboard + // selection change compositions. + if (!aReason || (aReason & (nsISelectionListener::DRAG_REASON | + nsISelectionListener::KEYPRESS_REASON | + nsISelectionListener::MOUSEDOWN_REASON))) { + SetVisibility(false); + } else { + UpdateSelectionCarets(); + } + } + + // Maybe trigger Android ActionBar updates. + if (mVisible && sCaretManagesAndroidActionbar) { + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (os) { + os->NotifyObservers(nullptr, "ActionBar:UpdateState", nullptr); + } } DispatchSelectionStateChangedEvent(static_cast<Selection*>(aSel), GetSelectionStates(aReason)); return NS_OK; } static void @@ -1159,17 +1203,20 @@ DispatchScrollViewChangeEvent(nsIPresShe } } void SelectionCarets::AsyncPanZoomStarted() { if (mVisible) { mInAsyncPanZoomGesture = true; - SetVisibility(false); + // Hide selection carets if not using ActionBar. + if (!sCaretManagesAndroidActionbar) { + SetVisibility(false); + } SELECTIONCARETS_LOG("Dispatch scroll started"); DispatchScrollViewChangeEvent(mPresShell, dom::ScrollState::Started); } else { nsRefPtr<dom::Selection> selection = GetSelection(); if (selection && selection->RangeCount() && selection->IsCollapsed()) { mInAsyncPanZoomGesture = true; DispatchScrollViewChangeEvent(mPresShell, dom::ScrollState::Started); @@ -1195,17 +1242,21 @@ SelectionCarets::AsyncPanZoomStopped() } } void SelectionCarets::ScrollPositionChanged() { if (mVisible) { if (!mUseAsyncPanZoom) { - SetVisibility(false); + // Hide selection carets if not using ActionBar. + if (!sCaretManagesAndroidActionbar) { + SetVisibility(false); + } + //TODO: handling scrolling for selection bubble when APZ is off // Dispatch event to notify gaia to hide selection bubble. // Positions will be updated when scroll is end, so no need to calculate // and keep scroll positions here. An arbitrary (0, 0) is sent instead. DispatchScrollViewChangeEvent(mPresShell, dom::ScrollState::Started); SELECTIONCARETS_LOG("Launch scroll end detector"); LaunchScrollEndDetector(); @@ -1281,20 +1332,21 @@ void SelectionCarets::LaunchScrollEndDetector() { if (!mScrollEndDetectorTimer) { mScrollEndDetectorTimer = do_CreateInstance("@mozilla.org/timer;1"); } MOZ_ASSERT(mScrollEndDetectorTimer); - SELECTIONCARETS_LOG("Will fire scroll end after %d ms", kScrollEndTimerDelay); + SELECTIONCARETS_LOG("Will fire scroll end after %d ms", + TouchCaret::sScrollEndTimerDelay); mScrollEndDetectorTimer->InitWithFuncCallback(FireScrollEnd, this, - kScrollEndTimerDelay, + TouchCaret::sScrollEndTimerDelay, nsITimer::TYPE_ONE_SHOT); } void SelectionCarets::CancelScrollEndDetector() { if (!mScrollEndDetectorTimer) { return;
--- a/layout/base/SelectionCarets.h +++ b/layout/base/SelectionCarets.h @@ -96,31 +96,31 @@ public: * Get from pref "selectioncaret.inflatesize.threshold". This will inflate size of * caret frame when we checking if user click on caret or not. In app units. */ static int32_t SelectionCaretsInflateSize() { return sSelectionCaretsInflateSize; } -private: - virtual ~SelectionCarets(); - - SelectionCarets() = delete; - /** * Set visibility for selection caret. */ void SetVisibility(bool aVisible); /** * Update selection caret position base on current selection range. */ void UpdateSelectionCarets(); +private: + virtual ~SelectionCarets(); + + SelectionCarets() = delete; + /** * Select a word base on current position, which activates only if element is * selectable. Triggered by long tap event. */ nsresult SelectWord(); /** * Move selection base on current touch/mouse point @@ -262,12 +262,17 @@ private: bool mEndCaretVisible; bool mStartCaretVisible; bool mSelectionVisibleInScrollFrames; bool mVisible; // Preference static int32_t sSelectionCaretsInflateSize; static bool sSelectionCaretDetectsLongTap; + static bool sCaretManagesAndroidActionbar; + static bool sSelectionCaretObservesCompositions; + + // Unique ID of current Mobile ActionBar view. + uint32_t mActionBarViewID; }; } // namespace mozilla #endif //SelectionCarets_h__
--- a/layout/base/TouchCaret.cpp +++ b/layout/base/TouchCaret.cpp @@ -51,50 +51,100 @@ static const char* kTouchCaretLogModuleN ("TouchCaret: %s:%d : " message "\n", __FUNCTION__, __LINE__, \ ##__VA_ARGS__)); // Click on the boundary of input/textarea will place the caret at the // front/end of the content. To advoid this, we need to deflate the content // boundary by 61 app units (1 pixel + 1 app unit). static const int32_t kBoundaryAppUnits = 61; -NS_IMPL_ISUPPORTS(TouchCaret, nsISelectionListener) +NS_IMPL_ISUPPORTS(TouchCaret, + nsISelectionListener, + nsIScrollObserver, + nsISupportsWeakReference) /*static*/ int32_t TouchCaret::sTouchCaretInflateSize = 0; /*static*/ int32_t TouchCaret::sTouchCaretExpirationTime = 0; +/*static*/ bool TouchCaret::sCaretManagesAndroidActionbar = false; +/*static*/ bool TouchCaret::sTouchcaretExtendedvisibility = false; + +/*static*/ uint32_t TouchCaret::sActionBarViewCount = 0; TouchCaret::TouchCaret(nsIPresShell* aPresShell) : mState(TOUCHCARET_NONE), mActiveTouchId(-1), mCaretCenterToDownPointOffsetY(0), + mInAsyncPanZoomGesture(false), mVisible(false), - mIsValidTap(false) + mIsValidTap(false), + mActionBarViewID(0) { MOZ_ASSERT(NS_IsMainThread()); if (!gTouchCaretLog) { gTouchCaretLog = PR_NewLogModule(kTouchCaretLogModuleName); } TOUCHCARET_LOG("Constructor, PresShell=%p", aPresShell); static bool addedTouchCaretPref = false; if (!addedTouchCaretPref) { Preferences::AddIntVarCache(&sTouchCaretInflateSize, "touchcaret.inflatesize.threshold"); Preferences::AddIntVarCache(&sTouchCaretExpirationTime, "touchcaret.expiration.time"); + Preferences::AddBoolVarCache(&sCaretManagesAndroidActionbar, + "caret.manages-android-actionbar"); + Preferences::AddBoolVarCache(&sTouchcaretExtendedvisibility, + "touchcaret.extendedvisibility"); addedTouchCaretPref = true; } // The presshell owns us, so no addref. mPresShell = do_GetWeakReference(aPresShell); MOZ_ASSERT(mPresShell, "Hey, pres shell should support weak refs"); } +void +TouchCaret::Init() +{ + nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell); + if (!presShell) { + return; + } + + nsPresContext* presContext = presShell->GetPresContext(); + MOZ_ASSERT(presContext, "PresContext should be given in PresShell::Init()"); + + nsIDocShell* docShell = presContext->GetDocShell(); + if (!docShell) { + return; + } + + docShell->AddWeakScrollObserver(this); + mDocShell = static_cast<nsDocShell*>(docShell); +} + +void +TouchCaret::Terminate() +{ + nsRefPtr<nsDocShell> docShell(mDocShell.get()); + if (docShell) { + docShell->RemoveWeakScrollObserver(this); + } + + if (mScrollEndDetectorTimer) { + mScrollEndDetectorTimer->Cancel(); + mScrollEndDetectorTimer = nullptr; + } + + mDocShell = WeakPtr<nsDocShell>(); + mPresShell = nullptr; +} + TouchCaret::~TouchCaret() { TOUCHCARET_LOG("Destructor"); MOZ_ASSERT(NS_IsMainThread()); if (mTouchCaretExpirationTimer) { mTouchCaretExpirationTimer->Cancel(); mTouchCaretExpirationTimer = nullptr; @@ -169,16 +219,48 @@ TouchCaret::SetVisibility(bool aVisible) ErrorResult err; touchCaretElement->ClassList()->Toggle(NS_LITERAL_STRING("hidden"), dom::Optional<bool>(!mVisible), err); TOUCHCARET_LOG("Set visibility %s", (mVisible ? "shown" : "hidden")); // Set touch caret expiration time. mVisible ? LaunchExpirationTimer() : CancelExpirationTimer(); + + // If after a TouchCaret visibility change we become hidden, ensure + // the Android ActionBar handler is notified to close the current view. + if (!mVisible && sCaretManagesAndroidActionbar) { + UpdateAndroidActionBarVisibility(false, mActionBarViewID); + } +} + +/** + * Open or close the Android TextSelection ActionBar, based on visibility. + * Each time we're called to open the actionbar, we increment / assign a + * unique view ID and return it to the caller. The ID is returned on calls + * to close the actionbar to ensure we don't close the shared view if it + * was already force closed by a subsequent callers open request. + */ +/* static */void +TouchCaret::UpdateAndroidActionBarVisibility(bool aVisibility, uint32_t& aViewID) +{ + // Are we openning a new view? + if (aVisibility) { + // Assign a new view ID. + aViewID = ++sActionBarViewCount; + } + + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (os) { + nsString topic = (aVisibility) ? + NS_LITERAL_STRING("ActionBar:OpenNew") : NS_LITERAL_STRING("ActionBar:Close"); + nsAutoString viewCount; + viewCount.AppendInt(aViewID); + os->NotifyObservers(nullptr, NS_ConvertUTF16toUTF8(topic).get(), viewCount.get()); + } } nsRect TouchCaret::GetTouchFrameRect() { nsCOMPtr<nsIPresShell> presShell = do_QueryReferent(mPresShell); if (!presShell) { return nsRect(); @@ -357,21 +439,110 @@ TouchCaret::NotifySelectionChanged(nsIDO // Also hide touch caret when gecko or javascript collapse the selection. if (aReason & nsISelectionListener::KEYPRESS_REASON || aReason & nsISelectionListener::COLLAPSETOSTART_REASON || aReason & nsISelectionListener::COLLAPSETOEND_REASON) { TOUCHCARET_LOG("KEYPRESS_REASON"); SetVisibility(false); } else { SyncVisibilityWithCaret(); + + // Is the TouchCaret visible and we're showing/hiding the actionbar? + if (mVisible && sCaretManagesAndroidActionbar) { + // A selection change due to touch tap opens the actionbar. + if (aReason & nsISelectionListener::MOUSEUP_REASON) { + UpdateAndroidActionBarVisibility(true, mActionBarViewID); + } else { + // Update the ActionBar state for caret-specific selection changes. + // Ignore transient selection composition changes that occur while + // the TouchCaret is also visible. + bool isCollapsed; + if (NS_SUCCEEDED(aSel->GetIsCollapsed(&isCollapsed)) && isCollapsed) { + nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService(); + if (os) { + os->NotifyObservers(nullptr, "ActionBar:UpdateState", nullptr); + } + } + } + } } return NS_OK; } +/** + * Used to update caret position after PanZoom stops for + * extended caret visibility. Never needed by MOZ_WIDGET_GONK. + */ +void +TouchCaret::AsyncPanZoomStarted() +{ + if (mVisible) { + if (sTouchcaretExtendedvisibility) { + mInAsyncPanZoomGesture = true; + } + } +} + +void +TouchCaret::AsyncPanZoomStopped() +{ + if (mInAsyncPanZoomGesture) { + mInAsyncPanZoomGesture = false; + UpdatePosition(); + } +} + +/** + * Used to update caret position after Scroll stops for + * extended caret visibility. Never needed by MOZ_WIDGET_GONK. + */ +void +TouchCaret::ScrollPositionChanged() +{ + if (mVisible) { + if (sTouchcaretExtendedvisibility) { + // Launch scroll end detector. + LaunchScrollEndDetector(); + } + } +} + +void +TouchCaret::LaunchScrollEndDetector() +{ + if (!mScrollEndDetectorTimer) { + mScrollEndDetectorTimer = do_CreateInstance("@mozilla.org/timer;1"); + } + MOZ_ASSERT(mScrollEndDetectorTimer); + + mScrollEndDetectorTimer->InitWithFuncCallback(FireScrollEnd, + this, + sScrollEndTimerDelay, + nsITimer::TYPE_ONE_SHOT); +} + +void +TouchCaret::CancelScrollEndDetector() +{ + if (mScrollEndDetectorTimer) { + mScrollEndDetectorTimer->Cancel(); + } +} + + +/* static */void +TouchCaret::FireScrollEnd(nsITimer* aTimer, void* aTouchCaret) +{ + nsRefPtr<TouchCaret> self = static_cast<TouchCaret*>(aTouchCaret); + NS_PRECONDITION(aTimer == self->mScrollEndDetectorTimer, + "Unexpected timer"); + self->UpdatePosition(); +} + void TouchCaret::SyncVisibilityWithCaret() { TOUCHCARET_LOG("SyncVisibilityWithCaret"); if (!IsDisplayable()) { SetVisibility(false); return; @@ -442,27 +613,33 @@ TouchCaret::IsDisplayable() } nsRect focusRect; nsIFrame* focusFrame = caret->GetGeometry(&focusRect); if (!focusFrame) { TOUCHCARET_LOG("Focus frame is not valid!"); return false; } - if (focusRect.IsEmpty()) { - TOUCHCARET_LOG("Focus rect is empty!"); - return false; - } dom::Element* editingHost = focusFrame->GetContent()->GetEditingHost(); if (!editingHost) { TOUCHCARET_LOG("Cannot get editing host!"); return false; } + // No further checks required if extended TouchCaret visibility. + if (sTouchcaretExtendedvisibility) { + return true; + } + + if (focusRect.IsEmpty()) { + TOUCHCARET_LOG("Focus rect is empty!"); + return false; + } + if (!nsContentUtils::HasNonEmptyTextContent( editingHost, nsContentUtils::eRecurseIntoChildren)) { TOUCHCARET_LOG("The content is empty!"); return false; } if (mState != TOUCHCARET_TOUCHDRAG_ACTIVE && !nsLayoutUtils::IsRectVisibleInScrollFrames(focusFrame, focusRect)) { @@ -832,16 +1009,22 @@ TouchCaret::HandleMouseDownEvent(WidgetM SetSelectionDragState(true); // Cache distence of the event point to the center of touch caret. mCaretCenterToDownPointOffsetY = GetCaretYCenterPosition() - point.y; // Enter TOUCHCARET_MOUSEDRAG_ACTIVE state and cancel the timer. SetState(TOUCHCARET_MOUSEDRAG_ACTIVE); CancelExpirationTimer(); status = nsEventStatus_eConsumeNoDefault; } else { + // Mousedown events that miss HitTest can be caused by soft-keyboard + // auto-suggestions. If extended visibility, update the caret position. + if (sTouchcaretExtendedvisibility) { + UpdatePositionIfNeeded(); + break; + } // Set touch caret invisible if HisTest fails. Bypass event. SetVisibility(false); status = nsEventStatus_eIgnore; } } else { // Set touch caret invisible if not left button down event. SetVisibility(false); status = nsEventStatus_eIgnore; @@ -892,18 +1075,24 @@ TouchCaret::HandleTouchDownEvent(WidgetT CancelExpirationTimer(); status = nsEventStatus_eConsumeNoDefault; break; } } // No touch is on the touch caret. Set touch caret invisible, and bypass // the event. if (mActiveTouchId == -1) { - SetVisibility(false); - status = nsEventStatus_eIgnore; + // Check touch caret visibility style. + if (sTouchcaretExtendedvisibility) { + // Update position on events associated with scroll and pan-zoom. + UpdatePositionIfNeeded(); + } else { + SetVisibility(false); + status = nsEventStatus_eIgnore; + } } } break; case TOUCHCARET_MOUSEDRAG_ACTIVE: case TOUCHCARET_TOUCHDRAG_ACTIVE: case TOUCHCARET_TOUCHDRAG_INACTIVE: // Consume NS_TOUCH_START event.
--- a/layout/base/TouchCaret.h +++ b/layout/base/TouchCaret.h @@ -22,28 +22,35 @@ class nsIPresShell; namespace mozilla { /** * The TouchCaret places a touch caret according to caret position when the * caret is shown. * TouchCaret is also responsible for touch caret visibility. Touch caret * won't be shown when timer expires or while key event causes selection change. */ -class TouchCaret final : public nsISelectionListener +class TouchCaret final : public nsISelectionListener, + public nsIScrollObserver, + public nsSupportsWeakReference { public: explicit TouchCaret(nsIPresShell* aPresShell); NS_DECL_ISUPPORTS NS_DECL_NSISELECTIONLISTENER - void Terminate() - { - mPresShell = nullptr; - } + void Init(); + void Terminate(); + + // nsIScrollObserver + virtual void ScrollPositionChanged() override; + + // AsyncPanZoom started/stopped callbacks from nsIScrollObserver + virtual void AsyncPanZoomStarted() override; + virtual void AsyncPanZoomStopped() override; /** * Handle mouse and touch event only. * Depends on visibility and position of touch caret, HandleEvent may consume * that input event and return nsEventStatus_eConsumeNoDefault to the caller. * In that case, caller should stop bubble up that input event. */ nsEventStatus HandleEvent(WidgetEvent* aEvent); @@ -55,16 +62,21 @@ public: /** * GetVisibility will get the visibility of the touch caret. */ bool GetVisibility() const { return mVisible; } + /** + * Open or close the Android TextSelection ActionBar based on visibility. + */ + static void UpdateAndroidActionBarVisibility(bool aVisibility, uint32_t& aViewID); + private: // Hide default constructor. TouchCaret() = delete; ~TouchCaret(); bool IsDisplayable(); @@ -263,27 +275,49 @@ private: */ static int32_t TouchCaretInflateSize() { return sTouchCaretInflateSize; } static int32_t TouchCaretExpirationTime() { return sTouchCaretExpirationTime; } + void LaunchScrollEndDetector(); + void CancelScrollEndDetector(); + static void FireScrollEnd(nsITimer* aTimer, void* aSelectionCarets); + + // This timer is used for detecting scroll end. We don't have + // scroll end event now, so we will fire this event with a + // const time when we scroll. So when timer triggers, we treat it + // as scroll end event. + nsCOMPtr<nsITimer> mScrollEndDetectorTimer; + nsWeakPtr mPresShell; + WeakPtr<nsDocShell> mDocShell; + + // True if AsyncPanZoom is started + bool mInAsyncPanZoomGesture; // Touch caret visibility bool mVisible; // Use for detecting single tap on touch caret. bool mIsValidTap; // Touch caret timer nsCOMPtr<nsITimer> mTouchCaretExpirationTimer; // Preference static int32_t sTouchCaretInflateSize; static int32_t sTouchCaretExpirationTime; + static bool sCaretManagesAndroidActionbar; + static bool sTouchcaretExtendedvisibility; // The auto scroll timer's interval in miliseconds. friend class SelectionCarets; static const int32_t sAutoScrollTimerDelay = 30; + // Time for trigger scroll end event, in miliseconds. + static const int32_t sScrollEndTimerDelay = 300; + + // Unique ID of current Mobile ActionBar view. + static uint32_t sActionBarViewCount; + uint32_t mActionBarViewID; }; } //namespace mozilla #endif //mozilla_TouchCaret_h__
--- a/layout/base/nsCaret.cpp +++ b/layout/base/nsCaret.cpp @@ -41,16 +41,19 @@ using namespace mozilla; using namespace mozilla::dom; using namespace mozilla::gfx; // The bidi indicator hangs off the caret to one side, to show which // direction the typing is in. It needs to be at least 2x2 to avoid looking like // an insignificant dot static const int32_t kMinBidiIndicatorPixels = 2; +/*static*/ bool nsCaret::sSelectionCaretEnabled = false; +/*static*/ bool nsCaret::sSelectionCaretsAffectCaret = false; + /** * Find the first frame in an in-order traversal of the frame subtree rooted * at aFrame which is either a text frame logically at the end of a line, * or which is aStopAtFrame. Return null if no such frame is found. We don't * descend into the children of non-eLineParticipant frames. */ static nsIFrame* CheckForTrailingTextFrameRecursive(nsIFrame* aFrame, nsIFrame* aStopAtFrame) @@ -139,16 +142,25 @@ nsresult nsCaret::Init(nsIPresShell *inP mPresShell = do_GetWeakReference(inPresShell); // the presshell owns us, so no addref NS_ASSERTION(mPresShell, "Hey, pres shell should support weak refs"); mShowDuringSelection = LookAndFeel::GetInt(LookAndFeel::eIntID_ShowCaretDuringSelection, mShowDuringSelection ? 1 : 0) != 0; + static bool addedCaretPref = false; + if (!addedCaretPref) { + Preferences::AddBoolVarCache(&sSelectionCaretEnabled, + "selectioncaret.enabled"); + Preferences::AddBoolVarCache(&sSelectionCaretsAffectCaret, + "selectioncaret.visibility.affectscaret"); + addedCaretPref = true; + } + // get the selection from the pres shell, and set ourselves up as a selection // listener nsCOMPtr<nsISelectionController> selCon = do_QueryReferent(mPresShell); if (!selCon) return NS_ERROR_FAILURE; nsCOMPtr<nsISelection> domSelection; @@ -248,27 +260,42 @@ void nsCaret::SetVisible(bool inMakeVisi } bool nsCaret::IsVisible() { if (!mVisible) { return false; } - if (!mShowDuringSelection) { + if (!mShowDuringSelection && + !(sSelectionCaretEnabled && sSelectionCaretsAffectCaret)) { Selection* selection = GetSelectionInternal(); if (!selection) { return false; } bool isCollapsed; if (NS_FAILED(selection->GetIsCollapsed(&isCollapsed)) || !isCollapsed) { return false; } } + // The Android IME can have a visible caret when there is a composition + // selection, due to auto-suggest/auto-correct styling (underlining), + // but never when the SelectionCarets are visible. + if (sSelectionCaretEnabled && sSelectionCaretsAffectCaret) { + nsCOMPtr<nsISelectionController> selCon = do_QueryReferent(mPresShell); + if (selCon) { + bool visible = false; + selCon->GetSelectionCaretsVisibility(&visible); + if (visible) { + return false; + } + } + } + if (IsMenuPopupHidingCaret()) { return false; } return true; } void nsCaret::SetCaretReadOnly(bool inMakeReadonly)
--- a/layout/base/nsCaret.h +++ b/layout/base/nsCaret.h @@ -225,11 +225,15 @@ protected: * the selection is not collapsed. */ bool mShowDuringSelection; /** * mIgnoreUserModify is true when the caret should be shown even when * it's in non-user-modifiable content. */ bool mIgnoreUserModify; + + // Preference + static bool sSelectionCaretEnabled; + static bool sSelectionCaretsAffectCaret; }; #endif //nsCaret_h__
--- a/layout/base/nsPresShell.cpp +++ b/layout/base/nsPresShell.cpp @@ -894,16 +894,17 @@ PresShell::Init(nsIDocument* aDocument, // setup the preference style rules (no forced reflow), and do it // before creating any frames. SetPreferenceStyleRules(false); if (TouchCaretPrefEnabled() && !AccessibleCaretEnabled()) { // Create touch caret handle mTouchCaret = new TouchCaret(this); + mTouchCaret->Init(); } if (SelectionCaretPrefEnabled() && !AccessibleCaretEnabled()) { // Create selection caret handle mSelectionCarets = new SelectionCarets(this); mSelectionCarets->Init(); } @@ -2549,16 +2550,36 @@ PresShell::CheckVisibilityContent(nsICon return NS_ERROR_INVALID_ARG; } *aRetval = false; DoCheckVisibility(mPresContext, aNode, aStartOffset, aEndOffset, aRetval); return NS_OK; } +NS_IMETHODIMP +PresShell::GetSelectionCaretsVisibility(bool* aOutVisibility) +{ + *aOutVisibility = (SelectionCaretPrefEnabled() && mSelectionCarets->GetVisibility()); + return NS_OK; +} + +NS_IMETHODIMP +PresShell::SetSelectionCaretsVisibility(bool aVisibility) +{ + if (SelectionCaretPrefEnabled() && mSelectionCarets) { + if (aVisibility) { + mSelectionCarets->UpdateSelectionCarets(); + } else { + mSelectionCarets->SetVisibility(false); + } + } + return NS_OK; +} + //end implementations nsISelectionController nsIFrame* nsIPresShell::GetRootFrameExternal() const { return mFrameConstructor->GetRootFrame(); }
--- a/layout/base/nsPresShell.h +++ b/layout/base/nsPresShell.h @@ -274,16 +274,19 @@ public: NS_IMETHOD ScrollCharacter(bool aRight) override; NS_IMETHOD CompleteScroll(bool aForward) override; NS_IMETHOD CompleteMove(bool aForward, bool aExtend) override; NS_IMETHOD SelectAll() override; NS_IMETHOD CheckVisibility(nsIDOMNode *node, int16_t startOffset, int16_t EndOffset, bool *_retval) override; virtual nsresult CheckVisibilityContent(nsIContent* aNode, int16_t aStartOffset, int16_t aEndOffset, bool* aRetval) override; + NS_IMETHOD GetSelectionCaretsVisibility(bool* aOutVisibility) override; + NS_IMETHOD SetSelectionCaretsVisibility(bool aVisibility) override; + // nsIDocumentObserver NS_DECL_NSIDOCUMENTOBSERVER_BEGINUPDATE NS_DECL_NSIDOCUMENTOBSERVER_ENDUPDATE NS_DECL_NSIDOCUMENTOBSERVER_BEGINLOAD NS_DECL_NSIDOCUMENTOBSERVER_ENDLOAD NS_DECL_NSIDOCUMENTOBSERVER_CONTENTSTATECHANGED NS_DECL_NSIDOCUMENTOBSERVER_DOCUMENTSTATESCHANGED NS_DECL_NSIDOCUMENTOBSERVER_STYLESHEETADDED
--- a/mobile/android/app/mobile.js +++ b/mobile/android/app/mobile.js @@ -218,16 +218,19 @@ pref("extensions.getLocales.get.url", "" pref("extensions.compatability.locales.buildid", "0"); /* blocklist preferences */ pref("extensions.blocklist.enabled", true); pref("extensions.blocklist.interval", 86400); pref("extensions.blocklist.url", "https://blocklist.addons.mozilla.org/blocklist/3/%APP_ID%/%APP_VERSION%/%PRODUCT%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%PING_COUNT%/%TOTAL_PING_COUNT%/%DAYS_SINCE_LAST_PING%/"); pref("extensions.blocklist.detailsURL", "https://www.mozilla.com/%LOCALE%/blocklist/"); +/* Don't let XPIProvider install distribution add-ons; we do our own thing on mobile. */ +pref("extensions.installDistroAddons", false); + /* block popups by default, and notify the user about blocked popups */ pref("dom.disable_open_during_load", true); pref("privacy.popups.showBrowserMessage", true); /* disable opening windows with the dialog feature */ pref("dom.disable_window_open_dialog_feature", true); pref("dom.disable_window_showModalDialog", true); pref("dom.disable_window_print", true); @@ -869,10 +872,35 @@ pref("reader.toolbar.vertical", false); // Whether or not to display buttons related to reading list in reader view. pref("browser.readinglist.enabled", true); // Telemetry settings. // Whether to use the unified telemetry behavior, requires a restart. pref("toolkit.telemetry.unified", false); +// Turn off selection caret by default +pref("selectioncaret.enabled", false); + // Selection carets never fall-back to internal LongTap detector. pref("selectioncaret.detects.longtap", false); + +// Selection carets override caret visibility. +pref("selectioncaret.visibility.affectscaret", true); + +// Selection caret visibility observes composition +// selections generated by soft keyboard managers. +pref("selectioncaret.observes.compositions", true); + +// Turn off touch caret by default. +pref("touchcaret.enabled", false); + +// TouchCaret never auto-hides. +pref("touchcaret.expiration.time", 0); + +// Touch caret stays visible under a wider range of conditions +// than the default b2g. We can display the caret in empty editables +// for example, and do not auto-hide until loss of focus. +pref("touchcaret.extendedvisibility", true); + +// The TouchCaret and the SelectionCarets will indicate when the +// TextSelection actionbar is to be openned or closed. +pref("caret.manages-android-actionbar", true);
--- a/mobile/android/base/Tab.java +++ b/mobile/android/base/Tab.java @@ -18,29 +18,31 @@ import org.json.JSONObject; import org.mozilla.gecko.db.BrowserDB; import org.mozilla.gecko.db.URLMetadata; import org.mozilla.gecko.favicons.Favicons; import org.mozilla.gecko.favicons.LoadFaviconTask; import org.mozilla.gecko.favicons.OnFaviconLoadedListener; import org.mozilla.gecko.favicons.RemoteFavicon; import org.mozilla.gecko.gfx.BitmapUtils; import org.mozilla.gecko.gfx.Layer; +import org.mozilla.gecko.mozglue.RobocopTarget; import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState; import org.mozilla.gecko.util.ThreadUtils; import android.content.ContentResolver; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.drawable.BitmapDrawable; import android.os.Build; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.widget.Toast; +import org.mozilla.gecko.widget.SiteLogins; public class Tab { private static final String LOGTAG = "GeckoTab"; private static Pattern sColorPattern; private final int mId; private final BrowserDB mDB; private long mLastUsed; @@ -52,16 +54,17 @@ public class Tab { private String mFaviconUrl; private String mApplicationId; // Intended to be null after explicit user action. // The set of all available Favicons for this tab, sorted by attractiveness. final TreeSet<RemoteFavicon> mAvailableFavicons = new TreeSet<>(); private boolean mHasFeeds; private boolean mHasOpenSearch; private final SiteIdentity mSiteIdentity; + private SiteLogins mSiteLogins; private BitmapDrawable mThumbnail; private final int mParentId; private final boolean mExternal; private boolean mBookmark; private boolean mIsInReadingList; private int mFaviconLoadId; private String mContentType; private boolean mHasTouchListeners; @@ -139,16 +142,17 @@ public class Tab { private ContentResolver getContentResolver() { return mAppContext.getContentResolver(); } public void onDestroy() { Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.CLOSED); } + @RobocopTarget public int getId() { return mId; } public synchronized void onChange() { mLastUsed = System.currentTimeMillis(); } @@ -275,16 +279,20 @@ public class Tab { public boolean hasOpenSearch() { return mHasOpenSearch; } public SiteIdentity getSiteIdentity() { return mSiteIdentity; } + public SiteLogins getSiteLogins() { + return mSiteLogins; + } + public boolean isBookmark() { return mBookmark; } public boolean isInReadingList() { return mIsInReadingList; } @@ -489,16 +497,20 @@ public class Tab { public void setHasOpenSearch(boolean hasOpenSearch) { mHasOpenSearch = hasOpenSearch; } public void updateIdentityData(JSONObject identityData) { mSiteIdentity.update(identityData); } + public void setSiteLogins(SiteLogins siteLogins) { + mSiteLogins = siteLogins; + } + void updateBookmark() { if (getURL() == null) { return; } ThreadUtils.postToBackgroundThread(new Runnable() { @Override public void run() { @@ -686,16 +698,17 @@ public class Tab { setContentType(message.getString("contentType")); updateUserRequested(message.getString("userRequested")); mBaseDomain = message.optString("baseDomain"); setHasFeeds(false); setHasOpenSearch(false); mSiteIdentity.reset(); + setSiteLogins(null); setZoomConstraints(new ZoomConstraints(true)); setHasTouchListeners(false); setBackgroundColor(DEFAULT_BACKGROUND_COLOR); setErrorType(ErrorType.NONE); setLoadProgressIfLoading(LOAD_PROGRESS_LOCATION_CHANGE); Tabs.getInstance().notifyListeners(this, Tabs.TabEvents.LOCATION_CHANGE, oldUrl); }
--- a/mobile/android/base/TextSelection.java +++ b/mobile/android/base/TextSelection.java @@ -32,16 +32,17 @@ import org.json.JSONObject; import java.util.Timer; import java.util.TimerTask; import android.util.Log; import android.view.View; class TextSelection extends Layer implements GeckoEventListener { private static final String LOGTAG = "GeckoTextSelection"; + private static final int SHUTDOWN_DELAY_MS = 250; private final TextSelectionHandle anchorHandle; private final TextSelectionHandle caretHandle; private final TextSelectionHandle focusHandle; private final DrawListener mDrawListener; private boolean mDraggingHandles; @@ -86,26 +87,32 @@ class TextSelection extends Layer implem } }; // Only register listeners if we have valid start/middle/end handles if (anchorHandle == null || caretHandle == null || focusHandle == null) { Log.e(LOGTAG, "Failed to initialize text selection because at least one handle is null"); } else { EventDispatcher.getInstance().registerGeckoThreadListener(this, + "TextSelection:ActionbarInit", + "TextSelection:ActionbarStatus", + "TextSelection:ActionbarUninit", "TextSelection:ShowHandles", "TextSelection:HideHandles", "TextSelection:PositionHandles", "TextSelection:Update", "TextSelection:DraggingHandle"); } } void destroy() { EventDispatcher.getInstance().unregisterGeckoThreadListener(this, + "TextSelection:ActionbarInit", + "TextSelection:ActionbarStatus", + "TextSelection:ActionbarUninit", "TextSelection:ShowHandles", "TextSelection:HideHandles", "TextSelection:PositionHandles", "TextSelection:Update", "TextSelection:DraggingHandle"); } private TextSelectionHandle getHandle(String name) { @@ -162,17 +169,17 @@ class TextSelection extends Layer implem // Remove draw-listener and text selection layer LayerView layerView = GeckoAppShell.getLayerView(); if (layerView != null) { layerView.removeDrawListener(mDrawListener); layerView.removeLayer(TextSelection.this); } mActionModeTimerTask = new ActionModeTimerTask(); - mActionModeTimer.schedule(mActionModeTimerTask, 250); + mActionModeTimer.schedule(mActionModeTimerTask, SHUTDOWN_DELAY_MS); anchorHandle.setVisibility(View.GONE); caretHandle.setVisibility(View.GONE); focusHandle.setVisibility(View.GONE); } else if (event.equals("TextSelection:PositionHandles")) { final JSONArray positions = message.getJSONArray("positions"); for (int i=0; i < positions.length(); i++) { @@ -180,17 +187,38 @@ class TextSelection extends Layer implem final int left = position.getInt("left"); final int top = position.getInt("top"); final boolean rtl = position.getBoolean("rtl"); TextSelectionHandle handle = getHandle(position.getString("handle")); handle.setVisibility(position.getBoolean("hidden") ? View.GONE : View.VISIBLE); handle.positionFromGecko(left, top, rtl); } + + } else if (event.equals("TextSelection:ActionbarInit")) { + // Init / Open the action bar. Note the current selectionID, + // cancel any pending actionBar close. + selectionID = message.getString("selectionID"); + mCurrentItems = null; + if (mActionModeTimerTask != null) { + mActionModeTimerTask.cancel(); + } + + } else if (event.equals("TextSelection:ActionbarStatus")) { + // Update the actionBar actions as provided by Gecko. + showActionMode(message.getJSONArray("actions")); + + } else if (event.equals("TextSelection:ActionbarUninit")) { + // Uninit the actionbar. Schedule a cancellable close + // action to avoid UI jank. (During SelectionAll for ex). + mCurrentItems = null; + mActionModeTimerTask = new ActionModeTimerTask(); + mActionModeTimer.schedule(mActionModeTimerTask, SHUTDOWN_DELAY_MS); } + } catch (JSONException e) { Log.e(LOGTAG, "JSON exception", e); } } }); } private void showActionMode(final JSONArray items) {
--- a/mobile/android/base/home/HomeConfig.java +++ b/mobile/android/base/home/HomeConfig.java @@ -102,25 +102,27 @@ public final class HomeConfig { public static class PanelConfig implements Parcelable { private final PanelType mType; private final String mTitle; private final String mId; private final LayoutType mLayoutType; private final List<ViewConfig> mViews; private final AuthConfig mAuthConfig; private final EnumSet<Flags> mFlags; + private final int mPosition; static final String JSON_KEY_TYPE = "type"; static final String JSON_KEY_TITLE = "title"; static final String JSON_KEY_ID = "id"; static final String JSON_KEY_LAYOUT = "layout"; static final String JSON_KEY_VIEWS = "views"; static final String JSON_KEY_AUTH_CONFIG = "authConfig"; static final String JSON_KEY_DEFAULT = "default"; static final String JSON_KEY_DISABLED = "disabled"; + static final String JSON_KEY_POSITION = "position"; public enum Flags { DEFAULT_PANEL, DISABLED_PANEL } public PanelConfig(JSONObject json) throws JSONException, IllegalArgumentException { final String panelType = json.optString(JSON_KEY_TYPE, null); @@ -166,32 +168,35 @@ public final class HomeConfig { if (json.optBoolean(JSON_KEY_DEFAULT, false)) { mFlags.add(Flags.DEFAULT_PANEL); } if (json.optBoolean(JSON_KEY_DISABLED, false)) { mFlags.add(Flags.DISABLED_PANEL); } + mPosition = json.optInt(JSON_KEY_POSITION, -1); + validate(); } @SuppressWarnings("unchecked") public PanelConfig(Parcel in) { mType = (PanelType) in.readParcelable(getClass().getClassLoader()); mTitle = in.readString(); mId = in.readString(); mLayoutType = (LayoutType) in.readParcelable(getClass().getClassLoader()); mViews = new ArrayList<ViewConfig>(); in.readTypedList(mViews, ViewConfig.CREATOR); mAuthConfig = (AuthConfig) in.readParcelable(getClass().getClassLoader()); mFlags = (EnumSet<Flags>) in.readSerializable(); + mPosition = in.readInt(); validate(); } public PanelConfig(PanelConfig panelConfig) { mType = panelConfig.mType; mTitle = panelConfig.mTitle; mId = panelConfig.mId; @@ -202,37 +207,39 @@ public final class HomeConfig { if (viewConfigs != null) { for (ViewConfig viewConfig : viewConfigs) { mViews.add(new ViewConfig(viewConfig)); } } mAuthConfig = panelConfig.mAuthConfig; mFlags = panelConfig.mFlags.clone(); + mPosition = panelConfig.mPosition; validate(); } public PanelConfig(PanelType type, String title, String id) { this(type, title, id, EnumSet.noneOf(Flags.class)); } public PanelConfig(PanelType type, String title, String id, EnumSet<Flags> flags) { - this(type, title, id, null, null, null, flags); + this(type, title, id, null, null, null, flags, -1); } public PanelConfig(PanelType type, String title, String id, LayoutType layoutType, - List<ViewConfig> views, AuthConfig authConfig, EnumSet<Flags> flags) { + List<ViewConfig> views, AuthConfig authConfig, EnumSet<Flags> flags, int position) { mType = type; mTitle = title; mId = id; mLayoutType = layoutType; mViews = views; mAuthConfig = authConfig; mFlags = flags; + mPosition = position; validate(); } private void validate() { if (mType == null) { throw new IllegalArgumentException("Can't create PanelConfig with null type"); } @@ -309,16 +316,20 @@ public final class HomeConfig { mFlags.remove(Flags.DISABLED_PANEL); } } public AuthConfig getAuthConfig() { return mAuthConfig; } + public int getPosition() { + return mPosition; + } + public JSONObject toJSON() throws JSONException { final JSONObject json = new JSONObject(); json.put(JSON_KEY_TYPE, mType.toString()); json.put(JSON_KEY_TITLE, mTitle); json.put(JSON_KEY_ID, mId); if (mLayoutType != null) { @@ -345,16 +356,18 @@ public final class HomeConfig { if (mFlags.contains(Flags.DEFAULT_PANEL)) { json.put(JSON_KEY_DEFAULT, true); } if (mFlags.contains(Flags.DISABLED_PANEL)) { json.put(JSON_KEY_DISABLED, true); } + json.put(JSON_KEY_POSITION, mPosition); + return json; } @Override public boolean equals(Object o) { if (o == null) { return false; } @@ -385,16 +398,17 @@ public final class HomeConfig { public void writeToParcel(Parcel dest, int flags) { dest.writeParcelable(mType, 0); dest.writeString(mTitle); dest.writeString(mId); dest.writeParcelable(mLayoutType, 0); dest.writeTypedList(mViews); dest.writeParcelable(mAuthConfig, 0); dest.writeSerializable(mFlags); + dest.writeInt(mPosition); } public static final Creator<PanelConfig> CREATOR = new Creator<PanelConfig>() { @Override public PanelConfig createFromParcel(final Parcel in) { return new PanelConfig(in); } @@ -1267,17 +1281,23 @@ public final class HomeConfig { throw new IllegalStateException("Can't install a disabled panel: " + panelConfig.getId()); } boolean installed = false; final String id = panelConfig.getId(); if (!mConfigMap.containsKey(id)) { mConfigMap.put(id, panelConfig); - mConfigOrder.add(id); + + final int position = panelConfig.getPosition(); + if (position < 0 || position >= mConfigOrder.size()) { + mConfigOrder.add(id); + } else { + mConfigOrder.add(position, id); + } mEnabledCount++; if (mEnabledCount == 1 || panelConfig.isDefault()) { setDefault(panelConfig.getId()); } installed = true;
--- a/mobile/android/base/locales/en-US/android_strings.dtd +++ b/mobile/android/base/locales/en-US/android_strings.dtd @@ -374,16 +374,21 @@ size. --> where normally a username would be displayed. In this case, no username was found, and this placeholder contains brackets to indicate this is not actually a username, but rather a placeholder --> <!ENTITY doorhanger_login_no_username "[No username]"> <!ENTITY doorhanger_login_edit_title "Edit login"> <!ENTITY doorhanger_login_edit_username_hint "Username"> <!ENTITY doorhanger_login_edit_password_hint "Password"> <!ENTITY doorhanger_login_edit_toggle "Show password"> <!ENTITY doorhanger_login_edit_toast_error "Failed to save login"> +<!ENTITY doorhanger_login_select_message "Copy password from &formatS;?"> +<!ENTITY doorhanger_login_select_toast_copy "Password copied to clipboard"> +<!ENTITY doorhanger_login_select_toast_copy_error "Couldn\'t copy password"> +<!ENTITY doorhanger_login_select_action_text "Select another login"> +<!ENTITY doorhanger_login_select_title "Copy password from"> <!ENTITY pref_titlebar_mode "Title bar"> <!ENTITY pref_titlebar_mode_title "Show page title"> <!ENTITY pref_titlebar_mode_url "Show page address"> <!-- Localization note (pref_scroll_title_bar2): Label for setting that controls whether or not the dynamic toolbar is enabled. --> <!ENTITY pref_scroll_title_bar2 "Full-screen browsing"> @@ -434,16 +439,17 @@ size. --> <!ENTITY button_ok "OK"> <!ENTITY button_cancel "Cancel"> <!ENTITY button_yes "Yes"> <!ENTITY button_no "No"> <!ENTITY button_clear_data "Clear data"> <!ENTITY button_set "Set"> <!ENTITY button_clear "Clear"> <!ENTITY button_remember "Remember"> +<!ENTITY button_copy "Copy"> <!ENTITY firstrun_panel_title_welcome "Welcome"> <!ENTITY home_top_sites_title "Top Sites"> <!-- Localization note (home_top_sites_add): This string is used as placeholder text underneath empty thumbnails in the Top Sites page on about:home. --> <!ENTITY home_top_sites_add "Add a site">
--- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -513,16 +513,17 @@ gbjar.sources += [ 'widget/FlowLayout.java', 'widget/GeckoActionProvider.java', 'widget/GeckoPopupMenu.java', 'widget/GeckoSwipeRefreshLayout.java', 'widget/GeckoViewFlipper.java', 'widget/IconTabWidget.java', 'widget/LoginDoorHanger.java', 'widget/ResizablePathDrawable.java', + 'widget/SiteLogins.java', 'widget/SquaredImageView.java', 'widget/SwipeDismissListViewTouchListener.java', 'widget/TabThumbnailWrapper.java', 'widget/ThumbnailView.java', 'widget/TwoWayView.java', 'ZoomConstraints.java', 'ZoomedView.java', ]
--- a/mobile/android/base/resources/layout/login_doorhanger.xml +++ b/mobile/android/base/resources/layout/login_doorhanger.xml @@ -30,17 +30,17 @@ <TextView android:id="@+id/doorhanger_message" android:focusable="true" android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingBottom="@dimen/doorhanger_section_padding_large" android:textAppearance="@style/TextAppearance.DoorHanger.Medium"/> - <TextView android:id="@+id/doorhanger_login" + <TextView android:id="@+id/doorhanger_link" android:layout_width="match_parent" android:layout_height="wrap_content" android:textAppearance="@style/TextAppearance.DoorHanger.Medium" android:textColor="@color/link_blue" android:paddingBottom="@dimen/doorhanger_section_padding_large" android:visibility="gone"/> </LinearLayout>
--- a/mobile/android/base/resources/layout/site_identity.xml +++ b/mobile/android/base/resources/layout/site_identity.xml @@ -29,57 +29,52 @@ <LinearLayout android:id="@+id/site_identity_known_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="gone" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textSize="14sp" - android:textColor="@color/placeholder_active_grey" + android:textAppearance="@style/TextAppearance.DoorHanger.Medium.Light" android:text="@string/identity_connected_to"/> <TextView android:id="@+id/host" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textSize="20sp" - android:textColor="@color/placeholder_active_grey" + android:textAppearance="@style/TextAppearance.DoorHanger.Medium" android:textStyle="bold"/> <TextView android:id="@+id/owner_label" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textSize="14sp" - android:textColor="@color/placeholder_active_grey" + android:textAppearance="@style/TextAppearance.DoorHanger.Medium.Light" android:text="@string/identity_run_by" - android:paddingTop="12dip"/> + android:paddingTop="@dimen/doorhanger_section_padding_small"/> <TextView android:id="@+id/owner" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textColor="@color/placeholder_active_grey" - android:textSize="16sp" + android:textAppearance="@style/TextAppearance.DoorHanger.Medium" android:textStyle="bold"/> <TextView android:id="@+id/verifier" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textSize="14sp" - android:textColor="@color/placeholder_active_grey" - android:paddingTop="12dip"/> + android:textAppearance="@style/TextAppearance.DoorHanger.Medium.Light" + android:paddingTop="@dimen/doorhanger_section_padding_small"/> </LinearLayout> <TextView android:id="@+id/site_settings_link" android:layout_width="match_parent" android:layout_height="wrap_content" - android:textAppearance="@style/TextAppearance.DoorHanger.Small" + android:textAppearance="@style/TextAppearance.DoorHanger.Medium" android:textColor="@color/link_blue" - android:paddingTop="@dimen/doorhanger_section_padding_small" - android:paddingBottom="@dimen/doorhanger_section_padding_small" + android:paddingTop="@dimen/doorhanger_section_padding_large" + android:paddingBottom="@dimen/doorhanger_padding" android:text="@string/contextmenu_site_settings"/> </LinearLayout> </LinearLayout> <View android:id="@+id/divider_doorhanger" android:layout_width="match_parent" android:layout_height="1dp" android:background="@color/divider_light"
--- a/mobile/android/base/resources/layout/site_identity_unknown.xml +++ b/mobile/android/base/resources/layout/site_identity_unknown.xml @@ -7,20 +7,18 @@ android:id="@+id/site_identity_unknown_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:visibility="gone"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textSize="14sp" - android:textColor="@color/placeholder_active_grey" + android:textAppearance="@style/TextAppearance.DoorHanger.Medium" android:text="@string/identity_no_info"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textSize="14sp" - android:textColor="@color/placeholder_active_grey" + android:textAppearance="@style/TextAppearance.DoorHanger.Medium" android:text="@string/identity_not_encrypted" - android:paddingTop="12dip"/> + android:paddingTop="@dimen/doorhanger_section_padding_small"/> </LinearLayout>
--- a/mobile/android/base/strings.xml.in +++ b/mobile/android/base/strings.xml.in @@ -341,16 +341,21 @@ <string name="contextmenu_add_search_engine">&contextmenu_add_search_engine;</string> <string name="doorhanger_login_no_username">&doorhanger_login_no_username;</string> <string name="doorhanger_login_edit_title">&doorhanger_login_edit_title;</string> <string name="doorhanger_login_edit_username_hint">&doorhanger_login_edit_username_hint;</string> <string name="doorhanger_login_edit_password_hint">&doorhanger_login_edit_password_hint;</string> <string name="doorhanger_login_edit_toggle">&doorhanger_login_edit_toggle;</string> <string name="doorhanger_login_edit_toast_error">&doorhanger_login_edit_toast_error;</string> + <string name="doorhanger_login_select_message">&doorhanger_login_select_message;</string> + <string name="doorhanger_login_select_toast_copy">&doorhanger_login_select_toast_copy;</string> + <string name="doorhanger_login_select_toast_copy_error">&doorhanger_login_select_toast_copy_error;</string> + <string name="doorhanger_login_select_action_text">&doorhanger_login_select_action_text;</string> + <string name="doorhanger_login_select_title">&doorhanger_login_select_title;</string> <string name="pref_titlebar_mode">&pref_titlebar_mode;</string> <string name="pref_titlebar_mode_title">&pref_titlebar_mode_title;</string> <string name="pref_titlebar_mode_url">&pref_titlebar_mode_url;</string> <string name="pref_scroll_title_bar2">&pref_scroll_title_bar2;</string> <string name="pref_scroll_title_bar_summary">&pref_scroll_title_bar_summary;</string> @@ -370,16 +375,17 @@ <string name="button_ok">&button_ok;</string> <string name="button_cancel">&button_cancel;</string> <string name="button_clear_data">&button_clear_data;</string> <string name="button_set">&button_set;</string> <string name="button_clear">&button_clear;</string> <string name="button_yes">&button_yes;</string> <string name="button_no">&button_no;</string> <string name="button_remember">&button_remember;</string> + <string name="button_copy">&button_copy;</string> <string name="firstrun_panel_title_welcome">&firstrun_panel_title_welcome;</string> <string name="home_title">&home_title;</string> <string name="home_top_sites_title">&home_top_sites_title;</string> <string name="home_top_sites_add">&home_top_sites_add;</string> <string name="home_history_title">&home_history_title;</string> <string name="home_clear_history_button">&home_clear_history_button;</string>
--- a/mobile/android/base/toolbar/BrowserToolbar.java +++ b/mobile/android/base/toolbar/BrowserToolbar.java @@ -849,16 +849,17 @@ public abstract class BrowserToolbar ext } public View getDoorHangerAnchor() { return urlDisplayLayout; } public void onDestroy() { Tabs.unregisterOnTabsChangedListener(this); + urlDisplayLayout.destroy(); } public boolean openOptionsMenu() { if (!hasSoftMenuButton) { return false; } // Initialize the popup.
--- a/mobile/android/base/toolbar/SiteIdentityPopup.java +++ b/mobile/android/base/toolbar/SiteIdentityPopup.java @@ -1,76 +1,92 @@ /* 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.toolbar; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.widget.Toast; +import org.json.JSONException; +import org.json.JSONArray; import org.mozilla.gecko.AboutPages; +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.EventDispatcher; import org.mozilla.gecko.R; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.SiteIdentity; import org.mozilla.gecko.SiteIdentity.SecurityMode; import org.mozilla.gecko.SiteIdentity.MixedMode; import org.mozilla.gecko.SiteIdentity.TrackingMode; import org.mozilla.gecko.Tab; import org.mozilla.gecko.Tabs; +import org.mozilla.gecko.util.GeckoEventListener; +import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.widget.AnchoredPopup; import org.mozilla.gecko.widget.DoorHanger; import org.mozilla.gecko.widget.DoorHanger.OnButtonClickListener; import org.json.JSONObject; import android.content.Context; import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; import org.mozilla.gecko.widget.DoorhangerConfig; +import org.mozilla.gecko.widget.SiteLogins; /** * SiteIdentityPopup is a singleton class that displays site identity data in * an arrow panel popup hanging from the lock icon in the browser toolbar. */ -public class SiteIdentityPopup extends AnchoredPopup { - public static enum ButtonType { DISABLE, ENABLE, KEEP_BLOCKING }; +public class SiteIdentityPopup extends AnchoredPopup implements GeckoEventListener { + + public static enum ButtonType { DISABLE, ENABLE, KEEP_BLOCKING, CANCEL, COPY }; private static final String LOGTAG = "GeckoSiteIdentityPopup"; private static final String MIXED_CONTENT_SUPPORT_URL = "https://support.mozilla.org/kb/how-does-insecure-content-affect-safety-android"; private static final String TRACKING_CONTENT_SUPPORT_URL = "https://support.mozilla.org/kb/firefox-android-tracking-protection"; + // Placeholder string. + private final static String FORMAT_S = "%s"; + private SiteIdentity mSiteIdentity; private LinearLayout mIdentity; private LinearLayout mIdentityKnownContainer; private LinearLayout mIdentityUnknownContainer; private TextView mHost; private TextView mOwnerLabel; private TextView mOwner; private TextView mVerifier; private View mDivider; private DoorHanger mMixedContentNotification; private DoorHanger mTrackingContentNotification; + private DoorHanger mSelectLoginDoorhanger; - private final OnButtonClickListener mButtonClickListener; + private final OnButtonClickListener mContentButtonClickListener; public SiteIdentityPopup(Context context) { super(context); - mButtonClickListener = new PopupButtonListener(); + mContentButtonClickListener = new ContentNotificationButtonListener(); + EventDispatcher.getInstance().registerGeckoThreadListener(this, "Doorhanger:Logins"); } @Override protected void init() { super.init(); // Make the popup focusable so it doesn't inadvertently trigger click events elsewhere // which may reshow the popup (see bug 785156) @@ -109,16 +125,143 @@ public class SiteIdentityPopup extends A final boolean isIdentityKnown = (siteIdentity.getSecurityMode() != SecurityMode.UNKNOWN); toggleIdentityKnownContainerVisibility(isIdentityKnown); if (isIdentityKnown) { updateIdentityInformation(siteIdentity); } } + @Override + public void handleMessage(String event, JSONObject geckoObject) { + if ("Doorhanger:Logins".equals(event)) { + try { + final Tab selectedTab = Tabs.getInstance().getSelectedTab(); + if (selectedTab != null) { + final JSONObject data = geckoObject.getJSONObject("data"); + addLoginsToTab(data); + } + if (isShowing()) { + addSelectLoginDoorhanger(selectedTab); + } + } catch (JSONException e) { + Log.e(LOGTAG, "Error accessing logins in Doorhanger:Logins message", e); + } + } + } + + private void addLoginsToTab(JSONObject data) throws JSONException { + final JSONObject titleObj = data.getJSONObject("title"); + final JSONArray logins = data.getJSONArray("logins"); + + final SiteLogins siteLogins = new SiteLogins(titleObj, logins); + Tabs.getInstance().getSelectedTab().setSiteLogins(siteLogins); + } + + private void addSelectLoginDoorhanger(Tab tab) throws JSONException { + final SiteLogins siteLogins = tab.getSiteLogins(); + if (siteLogins == null) { + return; + } + + final JSONArray logins = siteLogins.getLogins(); + if (logins.length() == 0) { + return; + } + + final JSONObject login = (JSONObject) logins.get(0); + + // Create button click listener for copying a password to the clipboard. + final OnButtonClickListener buttonClickListener = new OnButtonClickListener() { + @Override + public void onButtonClick(JSONObject response, DoorHanger doorhanger) { + try { + final int buttonId = response.getInt("callback"); + if (buttonId == ButtonType.COPY.ordinal()) { + final ClipboardManager manager = (ClipboardManager) mContext.getSystemService(Context.CLIPBOARD_SERVICE); + String password; + if (response.has("password")) { + // Click listener being called from List Dialog. + password = response.optString("password"); + } else { + password = login.getString("password"); + } + if (AppConstants.Versions.feature11Plus) { + manager.setPrimaryClip(ClipData.newPlainText("password", password)); + } else { + manager.setText(password); + } + Toast.makeText(mContext, R.string.doorhanger_login_select_toast_copy, Toast.LENGTH_SHORT).show(); + } + dismiss(); + } catch (JSONException e) { + Log.e(LOGTAG, "Error handling Select login button click", e); + Toast.makeText(mContext, R.string.doorhanger_login_select_toast_copy_error, Toast.LENGTH_SHORT).show(); + } + } + }; + + final DoorhangerConfig config = new DoorhangerConfig(DoorHanger.Type.LOGIN, buttonClickListener); + + // Set buttons. + config.appendButton(mContext.getString(R.string.button_cancel), ButtonType.CANCEL.ordinal()); + config.appendButton(mContext.getString(R.string.button_copy), ButtonType.COPY.ordinal()); + + // Set message. + String username = ((JSONObject) logins.get(0)).getString("username"); + if (TextUtils.isEmpty(username)) { + username = mContext.getString(R.string.doorhanger_login_no_username); + } + + final String message = mContext.getString(R.string.doorhanger_login_select_message).replace(FORMAT_S, username); + config.setMessage(message); + + // Set options. + final JSONObject options = new JSONObject(); + final JSONObject titleObj = siteLogins.getTitle(); + options.put("title", titleObj); + + // Add action text only if there are other logins to select. + if (logins.length() > 1) { + + final JSONObject actionText = new JSONObject(); + actionText.put("type", "SELECT"); + + final JSONObject bundle = new JSONObject(); + bundle.put("logins", logins); + + actionText.put("bundle", bundle); + options.put("actionText", actionText); + } + + config.setOptions(options); + + ThreadUtils.postToUiThread(new Runnable() { + @Override + public void run() { + if (!mInflated) { + init(); + } + + removeSelectLoginDoorhanger(); + + mSelectLoginDoorhanger = DoorHanger.Get(mContext, config); + mContent.addView(mSelectLoginDoorhanger); + mDivider.setVisibility(View.VISIBLE); + } + }); + } + + private void removeSelectLoginDoorhanger() { + if (mSelectLoginDoorhanger != null) { + mContent.removeView(mSelectLoginDoorhanger); + mSelectLoginDoorhanger = null; + } + } + private void toggleIdentityKnownContainerVisibility(final boolean isIdentityKnown) { if (isIdentityKnown) { mIdentityKnownContainer.setVisibility(View.VISIBLE); mIdentityUnknownContainer.setVisibility(View.GONE); } else { mIdentityKnownContainer.setVisibility(View.GONE); mIdentityUnknownContainer.setVisibility(View.VISIBLE); } @@ -147,50 +290,49 @@ public class SiteIdentityPopup extends A final String encrypted = siteIdentity.getEncrypted(); mVerifier.setText(verifier + "\n" + encrypted); } private void addMixedContentNotification(boolean blocked) { // Remove any existing mixed content notification. removeMixedContentNotification(); - final DoorhangerConfig config = new DoorhangerConfig(DoorHanger.Type.MIXED_CONTENT, mButtonClickListener); + final DoorhangerConfig config = new DoorhangerConfig(DoorHanger.Type.MIXED_CONTENT, mContentButtonClickListener); int icon; if (blocked) { icon = R.drawable.shield_enabled_doorhanger; config.setMessage(mContext.getString(R.string.blocked_mixed_content_message_top) + "\n\n" + mContext.getString(R.string.blocked_mixed_content_message_bottom)); } else { icon = R.drawable.shield_disabled_doorhanger; config.setMessage(mContext.getString(R.string.loaded_mixed_content_message)); } config.setLink(mContext.getString(R.string.learn_more), MIXED_CONTENT_SUPPORT_URL, "\n\n"); addNotificationButtons(config, blocked); mMixedContentNotification = DoorHanger.Get(mContext, config); mMixedContentNotification.setIcon(icon); - mContent.addView(mMixedContentNotification); mDivider.setVisibility(View.VISIBLE); } private void removeMixedContentNotification() { if (mMixedContentNotification != null) { mContent.removeView(mMixedContentNotification); mMixedContentNotification = null; } } private void addTrackingContentNotification(boolean blocked) { // Remove any existing tracking content notification. removeTrackingContentNotification(); - final DoorhangerConfig config = new DoorhangerConfig(DoorHanger.Type.TRACKING, mButtonClickListener); + final DoorhangerConfig config = new DoorhangerConfig(DoorHanger.Type.TRACKING, mContentButtonClickListener); int icon; if (blocked) { icon = R.drawable.shield_enabled_doorhanger; config.setMessage(mContext.getString(R.string.blocked_tracking_content_message_top) + "\n\n" + mContext.getString(R.string.blocked_tracking_content_message_bottom)); } else { icon = R.drawable.shield_disabled_doorhanger; @@ -255,16 +397,22 @@ public class SiteIdentityPopup extends A addMixedContentNotification(mixedMode == MixedMode.MIXED_CONTENT_BLOCKED); } final TrackingMode trackingMode = mSiteIdentity.getTrackingMode(); if (trackingMode != TrackingMode.UNKNOWN) { addTrackingContentNotification(trackingMode == TrackingMode.TRACKING_CONTENT_BLOCKED); } + try { + addSelectLoginDoorhanger(selectedTab); + } catch (JSONException e) { + Log.e(LOGTAG, "Error adding selectLogin doorhanger", e); + } + showDividers(); super.show(); } // Show the right dividers private void showDividers() { final int count = mContent.getChildCount(); @@ -284,25 +432,30 @@ public class SiteIdentityPopup extends A } } if (lastVisibleDoorHanger != null) { lastVisibleDoorHanger.hideDivider(); } } + void destroy() { + EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "Doorhanger:Logins"); + } + @Override public void dismiss() { super.dismiss(); removeMixedContentNotification(); removeTrackingContentNotification(); + removeSelectLoginDoorhanger(); mDivider.setVisibility(View.GONE); } - private class PopupButtonListener implements OnButtonClickListener { + private class ContentNotificationButtonListener implements OnButtonClickListener { @Override public void onButtonClick(JSONObject response, DoorHanger doorhanger) { GeckoEvent e = GeckoEvent.createBroadcastEvent("Session:Reload", response.toString()); GeckoAppShell.sendEventToGecko(e); dismiss(); } } }
--- a/mobile/android/base/toolbar/ToolbarDisplayLayout.java +++ b/mobile/android/base/toolbar/ToolbarDisplayLayout.java @@ -611,9 +611,13 @@ public class ToolbarDisplayLayout extend boolean dismissSiteIdentityPopup() { if (mSiteIdentityPopup != null && mSiteIdentityPopup.isShowing()) { mSiteIdentityPopup.dismiss(); return true; } return false; } + + void destroy() { + mSiteIdentityPopup.destroy(); + } }
--- a/mobile/android/base/widget/DefaultDoorHanger.java +++ b/mobile/android/base/widget/DefaultDoorHanger.java @@ -1,17 +1,16 @@ /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- * 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.widget; import android.util.Log; -import android.view.LayoutInflater; import android.widget.Button; import org.mozilla.gecko.R; import org.mozilla.gecko.prompts.PromptInput; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -107,21 +106,18 @@ public class DefaultDoorHanger extends D if (!TextUtils.isEmpty(checkBoxText)) { mCheckBox = (CheckBox) findViewById(R.id.doorhanger_checkbox); mCheckBox.setText(checkBoxText); mCheckBox.setVisibility(VISIBLE); } } @Override - protected Button createButtonInstance(final String text, final int id) { - final Button button = (Button) LayoutInflater.from(getContext()).inflate(R.layout.doorhanger_button, null); - button.setText(text); - - button.setOnClickListener(new Button.OnClickListener() { + protected OnClickListener makeOnButtonClickListener(final int id) { + return new Button.OnClickListener() { @Override public void onClick(View v) { final JSONObject response = new JSONObject(); try { // TODO: Bug 1149359 - Split this into each Doorhanger Type class. switch (mType) { case MIXED_CONTENT: response.put("allowContent", (id == SiteIdentityPopup.ButtonType.DISABLE.ordinal())); @@ -144,24 +140,23 @@ public class DefaultDoorHanger extends D if (doorHangerInputs != null) { JSONObject inputs = new JSONObject(); for (PromptInput input : doorHangerInputs) { inputs.put(input.getId(), input.getValue()); } response.put("inputs", inputs); } } - mOnButtonClickListener.onButtonClick(response, DefaultDoorHanger.this); } catch (JSONException e) { Log.e(LOGTAG, "Error creating onClick response", e); } + + mOnButtonClickListener.onButtonClick(response, DefaultDoorHanger.this); } - }); - - return button; + }; } private void styleInput(PromptInput input, View view) { if (input instanceof PromptInput.MenulistInput) { styleDropdownInputs(input, view); } view.setPadding(0, 0, 0, mResources.getDimensionPixelSize(R.dimen.doorhanger_padding)); }
--- a/mobile/android/base/widget/DoorHanger.java +++ b/mobile/android/base/widget/DoorHanger.java @@ -95,21 +95,16 @@ public abstract class DoorHanger extends resource = R.layout.doorhanger; } LayoutInflater.from(context).inflate(resource, this); mDivider = findViewById(R.id.divider_doorhanger); mIcon = (ImageView) findViewById(R.id.doorhanger_icon); mMessage = (TextView) findViewById(R.id.doorhanger_message); - // TODO: Bug 1149359 - split this into DoorHanger subclasses. - if (type == Type.TRACKING || type == Type.MIXED_CONTENT) { - mMessage.setTextAppearance(getContext(), R.style.TextAppearance_DoorHanger_Small); - } - mType = type; mButtonsContainer = (LinearLayout) findViewById(R.id.doorhanger_buttons); mOnButtonClickListener = config.getButtonClickListener(); mDividerColor = mResources.getColor(R.color.divider_light); setOrientation(VERTICAL); } @@ -127,17 +122,16 @@ public abstract class DoorHanger extends final long timeout = options.optLong("timeout"); if (timeout > 0) { mTimeout = timeout; } } protected void setButtons(DoorhangerConfig config) { final JSONArray buttons = config.getButtons(); - final OnButtonClickListener listener = config.getButtonClickListener(); for (int i = 0; i < buttons.length(); i++) { try { final JSONObject buttonObject = buttons.getJSONObject(i); final String label = buttonObject.getString("label"); final int callbackId = buttonObject.getInt("callback"); addButtonToLayout(label, callbackId); } catch (JSONException e) { Log.e(LOGTAG, "Error creating doorhanger button", e); @@ -209,17 +203,24 @@ public abstract class DoorHanger extends divider.setOrientation(Divider.Orientation.VERTICAL); divider.setBackgroundColor(mDividerColor); mButtonsContainer.addView(divider); } mButtonsContainer.addView(button, sButtonParams); } - protected abstract Button createButtonInstance(String text, int id); + protected Button createButtonInstance(String text, int id) { + final Button button = (Button) LayoutInflater.from(getContext()).inflate(R.layout.doorhanger_button, null); + button.setText(text); + button.setOnClickListener(makeOnButtonClickListener(id)); + return button; + } + + protected abstract OnClickListener makeOnButtonClickListener(final int id); /* * Checks with persistence and timeout options to see if it's okay to remove a doorhanger. * * @param isShowing Whether or not this doorhanger is currently visible to the user. * (e.g. the DoorHanger view might be VISIBLE, but its parent could be hidden) */ public boolean shouldRemove(boolean isShowing) {
--- a/mobile/android/base/widget/LoginDoorHanger.java +++ b/mobile/android/base/widget/LoginDoorHanger.java @@ -2,16 +2,18 @@ * 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.widget; import android.app.AlertDialog; import android.app.Dialog; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.text.method.PasswordTransformationMethod; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -19,33 +21,36 @@ import android.widget.Button; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import ch.boye.httpclientandroidlib.util.TextUtils; import org.json.JSONException; import org.json.JSONObject; +import org.json.JSONArray; +import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.R; import org.mozilla.gecko.favicons.Favicons; import org.mozilla.gecko.favicons.OnFaviconLoadedListener; +import org.mozilla.gecko.toolbar.SiteIdentityPopup; public class LoginDoorHanger extends DoorHanger { private static final String LOGTAG = "LoginDoorHanger"; - private enum ActionType { EDIT }; + private enum ActionType { EDIT, SELECT } private final TextView mTitle; - private final TextView mLogin; + private final TextView mLink; private int mCallbackID; public LoginDoorHanger(Context context, DoorhangerConfig config) { super(context, config, Type.LOGIN); mTitle = (TextView) findViewById(R.id.doorhanger_title); - mLogin = (TextView) findViewById(R.id.doorhanger_login); + mLink = (TextView) findViewById(R.id.doorhanger_link); loadConfig(config); } @Override protected void loadConfig(DoorhangerConfig config) { setOptions(config.getOptions()); setMessage(config.getMessage()); @@ -83,58 +88,48 @@ public class LoginDoorHanger extends Doo final JSONObject actionText = options.optJSONObject("actionText"); addActionText(actionText); } @Override protected Button createButtonInstance(final String text, final int id) { // HACK: Confirm button will the the rightmost/last button added. Bug 1147064 should add differentiation of the two. mCallbackID = id; + return super.createButtonInstance(text, id); + } - final Button button = (Button) LayoutInflater.from(getContext()).inflate(R.layout.doorhanger_button, null); - button.setText(text); - - button.setOnClickListener(new Button.OnClickListener() { + @Override + protected OnClickListener makeOnButtonClickListener(final int id) { + return new Button.OnClickListener() { @Override public void onClick(View v) { final JSONObject response = new JSONObject(); try { response.put("callback", id); } catch (JSONException e) { Log.e(LOGTAG, "Error making doorhanger response message", e); } mOnButtonClickListener.onButtonClick(response, LoginDoorHanger.this); } - }); - - return button; + }; } /** * Add sub-text to the doorhanger and add the click action. * * If the parsing the action from the JSON throws, the text is left visible, but there is no * click action. * @param actionTextObj JSONObject containing blob for making an action. */ private void addActionText(JSONObject actionTextObj) { if (actionTextObj == null) { - mLogin.setVisibility(View.GONE); + mLink.setVisibility(View.GONE); return; } - boolean hasUsername = true; - String text = actionTextObj.optString("text"); - if (TextUtils.isEmpty(text)) { - hasUsername = false; - text = mResources.getString(R.string.doorhanger_login_no_username); - } - mLogin.setText(text); - mLogin.setVisibility(View.VISIBLE); - // Make action. try { final JSONObject bundle = actionTextObj.getJSONObject("bundle"); final ActionType type = ActionType.valueOf(actionTextObj.getString("type")); final AlertDialog.Builder builder = new AlertDialog.Builder(mContext); switch (type) { case EDIT: @@ -177,27 +172,73 @@ public class LoginDoorHanger extends Doo } }); builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); + String text = actionTextObj.optString("text"); + if (TextUtils.isEmpty(text)) { + text = mResources.getString(R.string.doorhanger_login_no_username); + } + mLink.setText(text); + mLink.setVisibility(View.VISIBLE); + break; + + case SELECT: + try { + builder.setTitle(mResources.getString(R.string.doorhanger_login_select_title)); + final JSONArray logins = bundle.getJSONArray("logins"); + final int numLogins = logins.length(); + final CharSequence[] usernames = new CharSequence[numLogins]; + final String[] passwords = new String[numLogins]; + final String noUser = mResources.getString(R.string.doorhanger_login_no_username); + for (int i = 0; i < numLogins; i++) { + final JSONObject login = (JSONObject) logins.get(i); + String user = login.getString("username"); + if (TextUtils.isEmpty(user)) { + user = noUser; + } + usernames[i] = user; + passwords[i] = login.getString("password"); + } + builder.setItems(usernames, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + final JSONObject response = new JSONObject(); + try { + response.put("callback", SiteIdentityPopup.ButtonType.COPY.ordinal()); + response.put("password", passwords[which]); + } catch (JSONException e) { + Log.e(LOGTAG, "Error making login select dialog JSON", e); + } + mOnButtonClickListener.onButtonClick(response, LoginDoorHanger.this); + } + }); + builder.setNegativeButton(R.string.button_cancel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + mLink.setText(R.string.doorhanger_login_select_action_text); + mLink.setVisibility(View.VISIBLE); + } catch (JSONException e) { + Log.e(LOGTAG, "Problem creating list of logins"); + } + break; } final Dialog dialog = builder.create(); - mLogin.setOnClickListener(new OnClickListener() { + mLink.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { dialog.show(); } }); } catch (JSONException e) { - // Log an error, but leave the text visible if there was a username. Log.e(LOGTAG, "Error fetching actionText from JSON", e); - if (!hasUsername) { - mLogin.setVisibility(View.GONE); - } } } }
new file mode 100644 --- /dev/null +++ b/mobile/android/base/widget/SiteLogins.java @@ -0,0 +1,22 @@ +package org.mozilla.gecko.widget; + +import org.json.JSONArray; +import org.json.JSONObject; + +public class SiteLogins { + private final JSONObject titleObj; + private final JSONArray logins; + + public SiteLogins(JSONObject titleObj, JSONArray logins) { + this.logins = logins; + this.titleObj = titleObj; + } + + public JSONArray getLogins() { + return logins; + } + + public JSONObject getTitle() { + return titleObj; + } +}
new file mode 100644 --- /dev/null +++ b/mobile/android/chrome/content/ActionBarHandler.js @@ -0,0 +1,638 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +/* 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"; + +// Notifications we observe. +const NOTIFICATIONS = [ + "ActionBar:UpdateState", + "TextSelection:Action", + "TextSelection:End", +]; + +const DEFER_INIT_DELAY_MS = 50; // Delay period before _init() begins. +const PHONE_REGEX = /^\+?[0-9\s,-.\(\)*#pw]{1,30}$/; // Are we a phone #? + + +/** + * ActionBarHandler Object and methods. Interface between Gecko Text Selection code + * (TouchCaret, SelectionCarets, etc) and the Mobile ActionBar UI. + */ +var ActionBarHandler = { + // Error codes returned from _init(). + START_TOUCH_ERROR: { + NO_CONTENT_WINDOW: "No valid content Window found.", + NONE: "", + }, + + _selectionID: null, // Unique Selection ID, assigned each time we _init(). + _actionBarActions: null, // Most-recent set of actions sent to ActionBar. + + /** + * ActionBarHandler notification observers. + */ + observe: function(subject, topic, data) { + switch (topic) { + + // Gecko opens the ActionBarHandler. + case "ActionBar:OpenNew": { + // Always close, then re-open. + this._uninit(false); + this._init(data); + break; + } + + // Gecko closes the ActionBarHandler. + case "ActionBar:Close": { + if (this._selectionID === data) { + this._uninit(false); + } + break; + } + + // Update ActionBar when text selection changes. + case "ActionBar:UpdateState": { + this._sendActionBarActions(); + break; + } + + // User click an ActionBar button. + case "TextSelection:Action": { + for (let type in this.actions) { + let action = this.actions[type]; + if (action.id == data) { + action.action(this._targetElement, this._contentWindow); + break; + } + } + break; + } + + // Provide selected text to FindInPageBar on request. + case "TextSelection:Get": { + Messaging.sendRequest({ + type: "TextSelection:Data", + requestId: data, + text: this._getSelectedText(), + }); + break; + } + + // User closed ActionBar by clicking "checkmark" button. + case "TextSelection:End": { + // End the requested selection only. + if (this._selectionID === JSON.parse(data).selectionID) { + this._uninit(); + } + break; + } + } + }, + + /** + * Called when Gecko TouchCaret or SelectionCarets become visible. + */ + _init: function(actionBarID) { + let [element, win] = this._getSelectionTargets(); + if (!win) { + return this.START_TOUCH_ERROR.NO_CONTENT_WINDOW; + } + + // Hold the ActionBar ID provided by Gecko. + this._selectionID = actionBarID; + [this._targetElement, this._contentWindow] = [element, win]; + + // Add notification observers. + NOTIFICATIONS.forEach(notification => { + Services.obs.addObserver(this, notification, false); + }); + + // Open the ActionBar, send it's initial actions list. + Messaging.sendRequest({ + type: "TextSelection:ActionbarInit", + selectionID: this._selectionID, + }); + this._sendActionBarActions(true); + + return this.START_TOUCH_ERROR.NONE; + }, + + /** + * Determines the window containing the selection, and its + * editable element if present. + */ + _getSelectionTargets: function() { + let [element, win] = [Services.focus.focusedElement, Services.focus.focusedWindow]; + if (!element) { + // No focused editable. + return [null, win]; + } + + // Return focused editable text element and its window. + if (((element instanceof HTMLInputElement) && element.mozIsTextField(false)) || + (element instanceof HTMLTextAreaElement) || + element.isContentEditable) { + return [element, win]; + } + + // Focused element can't contain text. + return [null, win]; + }, + + /** + * Called when Gecko TouchCaret or SelectionCarets become hidden, + * ActionBar is closed by user "close" request, or as a result of object + * methods such as SELECT_ALL, PASTE, etc. + */ + _uninit: function(clearSelection = true) { + // Bail if there's no active selection. + if (!this._selectionID) { + return; + } + + // Remove notification observers. + NOTIFICATIONS.forEach(notification => { + Services.obs.removeObserver(this, notification); + }); + + // Close the ActionBar. + Messaging.sendRequest({ + type: "TextSelection:ActionbarUninit", + }); + + // Clear the selection ID to complete the uninit(), but leave our reference + // to selectionTargets (_targetElement, _contentWindow) in case we need + // a final clearSelection(). + this._selectionID = null; + + // Clear selection required if triggered by self, or TextSelection icon + // actions. If called by Gecko TouchCaret/SelectionCarets state change, + // visibility state is already correct. + if (clearSelection) { + this._clearSelection(); + } + }, + + /** + * Final UI cleanup when Actionbar is closed by icon click, or where + * we terminate selection state after before/after actionbar actions + * (Cut, Copy, Paste, Search, Share, Call). + */ + _clearSelection: function(element = this._targetElement, win = this._contentWindow) { + // Commit edit compositions, and clear focus from editables. + if (element) { + let imeSupport = this._getEditor(element, win).QueryInterface(Ci.nsIEditorIMESupport); + if (imeSupport.composing) { + imeSupport.forceCompositionEnd(); + } + element.blur(); + } + + // Remove Selection from non-editables and now-unfocused contentEditables. + if (!element || element.isContentEditable) { + this._getSelection().removeAllRanges(); + } + }, + + /** + * Called to determine current ActionBar actions and send to TextSelection + * handler. By default we only send if current action state differs from + * the previous. + * @param By default we only send an ActionBarStatus update message if + * there is a change from the previous state. sendAlways can be + * set by init() for example, where we want to always send the + * current state. + */ + _sendActionBarActions: function(sendAlways) { + let actions = this._getActionBarActions(); + + if (sendAlways || actions !== this._actionBarActions) { + Messaging.sendRequest({ + type: "TextSelection:ActionbarStatus", + actions: actions, + }); + } + + this._actionBarActions = actions; + }, + + /** + * Determine and return current ActionBar state. + */ + _getActionBarActions: function(element = this._targetElement, win = this._contentWindow) { + let actions = []; + + for (let type in this.actions) { + let action = this.actions[type]; + if (action.selector.matches(element, win)) { + let a = { + id: action.id, + label: this._getActionValue(action, "label", "", element), + icon: this._getActionValue(action, "icon", "drawable://ic_status_logo", element), + order: this._getActionValue(action, "order", 0, element), + showAsAction: this._getActionValue(action, "showAsAction", true, element), + }; + actions.push(a); + } + } + actions.sort((a, b) => b.order - a.order); + + return actions; + }, + + /** + * Provides a value from an action. If the action defines the value as a function, + * we return the result of calling the function. Otherwise, we return the value + * itself. If the value isn't defined for this action, will return a default. + */ + _getActionValue: function(obj, name, defaultValue, element) { + if (!(name in obj)) + return defaultValue; + + if (typeof obj[name] == "function") + return obj[name](element); + + return obj[name]; + }, + + /** + * Actionbar callback methods. + */ + actions: { + + SELECT_ALL: { + id: "selectall_action", + label: Strings.browser.GetStringFromName("contextmenu.selectAll"), + icon: "drawable://ab_select_all", + order: 5, + + selector: { + matches: function(element, win) { + // For editable, check its length. For default contentWindow, assume + // true, else there'd been nothing to long-press to open ActionBar. + return (element) ? element.textLength != 0 : true; + }, + }, + + action: function(element, win) { + // Some Mobile keyboards such as SwiftKeyboard, provide auto-suggest + // style highlights via composition selections in editables. + if (element) { + // If we have an active composition string, commit it, and + // ensure proper element focus. + let imeSupport = ActionBarHandler._getEditor(element, win). + QueryInterface(Ci.nsIEditorIMESupport); + if (imeSupport.composing) { + element.blur(); + element.focus(); + } + } + + // Close ActionBarHandler, then selectAll, and display handles. + ActionBarHandler._getSelectAllController(element, win).selectAll(); + ActionBarHandler._getSelectionController(element, win). + selectionCaretsVisibility = true; + + UITelemetry.addEvent("action.1", "actionbar", null, "select_all"); + }, + }, + + CUT: { + id: "cut_action", + label: Strings.browser.GetStringFromName("contextmenu.cut"), + icon: "drawable://ab_cut", + order: 4, + + selector: { + matches: function(element, win) { + // Can't cut from non-editable. + if (!element) { + return false; + } + // Don't allow "cut" from password fields. + if (element instanceof Ci.nsIDOMHTMLInputElement && + !element.mozIsTextField(true)) { + return false; + } + // Don't allow "cut" from disabled/readonly fields. + if (element.disabled || element.readOnly) { + return false; + } + // Allow if selected text exists. + return (ActionBarHandler._getSelectedText().length > 0); + }, + }, + + action: function(element, win) { + // First copy the selection text to the clipboard. + let selectedText = ActionBarHandler._getSelectedText(); + let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]. + getService(Ci.nsIClipboardHelper); + clipboard.copyString(selectedText, win.document); + + let msg = Strings.browser.GetStringFromName("selectionHelper.textCopied"); + NativeWindow.toast.show(msg, "short"); + + // Then cut the selection text. + ActionBarHandler._getSelection(element, win).deleteFromDocument(); + + ActionBarHandler._uninit(); + UITelemetry.addEvent("action.1", "actionbar", null, "cut"); + }, + }, + + COPY: { + id: "copy_action", + label: Strings.browser.GetStringFromName("contextmenu.copy"), + icon: "drawable://ab_copy", + order: 3, + + selector: { + matches: function(element, win) { + // Don't allow "copy" from password fields. + if (element instanceof Ci.nsIDOMHTMLInputElement && + !element.mozIsTextField(true)) { + return false; + } + // Allow if selected text exists. + return (ActionBarHandler._getSelectedText().length > 0); + }, + }, + + action: function(element, win) { + let selectedText = ActionBarHandler._getSelectedText(); + let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]. + getService(Ci.nsIClipboardHelper); + clipboard.copyString(selectedText, win.document); + + let msg = Strings.browser.GetStringFromName("selectionHelper.textCopied"); + NativeWindow.toast.show(msg, "short"); + + ActionBarHandler._uninit(); + UITelemetry.addEvent("action.1", "actionbar", null, "copy"); + }, + }, + + PASTE: { + id: "paste_action", + label: Strings.browser.GetStringFromName("contextmenu.paste"), + icon: "drawable://ab_paste", + order: 2, + + selector: { + matches: function(element, win) { + // Can't paste into non-editable. + if (!element) { + return false; + } + // Can't paste into disabled/readonly fields. + if (element.disabled || element.readOnly) { + return false; + } + // Can't paste if Clipboard empty. + let flavors = ["text/unicode"]; + return Services.clipboard.hasDataMatchingFlavors(flavors, flavors.length, + Ci.nsIClipboard.kGlobalClipboard); + }, + }, + + action: function(element, win) { + // Paste the clipboard, then close the ActionBarHandler and ActionBar. + ActionBarHandler._getEditor(element, win). + paste(Ci.nsIClipboard.kGlobalClipboard); + ActionBarHandler._uninit(); + UITelemetry.addEvent("action.1", "actionbar", null, "paste"); + }, + }, + + CALL: { + id: "call_action", + label: Strings.browser.GetStringFromName("contextmenu.call"), + icon: "drawable://phone", + order: 1, + + selector: { + matches: function(element, win) { + return (ActionBarHandler._getSelectedPhoneNumber() != null); + }, + }, + + action: function(element, win) { + BrowserApp.loadURI("tel:" + + ActionBarHandler._getSelectedPhoneNumber()); + + ActionBarHandler._uninit(); + UITelemetry.addEvent("action.1", "actionbar", null, "call"); + }, + }, + + SEARCH: { + id: "search_action", + label: Strings.browser.formatStringFromName("contextmenu.search", + [Services.search.defaultEngine.name], 1), + icon: "drawable://ab_search", + order: 1, + + selector: { + matches: function(element, win) { + // Allow if selected text exists. + return (ActionBarHandler._getSelectedText().length > 0); + }, + }, + + action: function(element, win) { + let selectedText = ActionBarHandler._getSelectedText(); + ActionBarHandler._uninit(); + + // Set current tab as parent of new tab, + // and set new tab as private if the parent is. + let searchSubmission = Services.search.defaultEngine.getSubmission(selectedText); + let parent = BrowserApp.selectedTab; + let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(parent.browser); + BrowserApp.addTab(searchSubmission.uri.spec, + { parentId: parent.id, + selected: true, + isPrivate: isPrivate, + } + ); + + UITelemetry.addEvent("action.1", "actionbar", null, "search"); + }, + }, + + SEARCH_ADD: { + id: "search_add_action", + label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine2"), + icon: "drawable://ab_add_search_engine", + order: 0, + + selector: { + matches: function(element, win) { + if(!(element instanceof HTMLInputElement)) { + return false; + } + let form = element.form; + if (!form || element.type == "password") { + return false; + } + let method = form.method.toUpperCase(); + return (method == "GET" || method == "") || + (form.enctype != "text/plain") && (form.enctype != "multipart/form-data"); + }, + }, + + action: function(element, win) { + UITelemetry.addEvent("action.1", "actionbar", null, "add_search_engine"); + SearchEngines.addEngine(element); + }, + }, + + SHARE: { + id: "share_action", + label: Strings.browser.GetStringFromName("contextmenu.share"), + icon: "drawable://ic_menu_share", + order: 0, + + selector: { + matches: function(element, win) { + if (!ParentalControls.isAllowed(ParentalControls.SHARE)) { + return false; + } + // Allow if selected text exists. + return (ActionBarHandler._getSelectedText().length > 0); + }, + }, + + action: function(element, win) { + Messaging.sendRequest({ + type: "Share:Text", + text: ActionBarHandler._getSelectedText(), + }); + + ActionBarHandler._uninit(); + UITelemetry.addEvent("action.1", "actionbar", null, "share"); + }, + }, + }, + + /** + * Provides UUID service for generating action ID's. + */ + get _idService() { + delete this._idService; + return this._idService = Cc["@mozilla.org/uuid-generator;1"]. + getService(Ci.nsIUUIDGenerator); + }, + + /** + * The targetElement holds an editable element containing a + * selection or a caret. + */ + get _targetElement() { + if (this._targetElementRef) + return this._targetElementRef.get(); + return null; + }, + + set _targetElement(element) { + this._targetElementRef = Cu.getWeakReference(element); + }, + + /** + * The contentWindow holds the selection, or the targetElement + * if it's an editable. + */ + get _contentWindow() { + if (this._contentWindowRef) + return this._contentWindowRef.get(); + return null; + }, + + set _contentWindow(aContentWindow) { + this._contentWindowRef = Cu.getWeakReference(aContentWindow); + }, + + /** + * Provides the currently selected text, for either an editable, + * or for the default contentWindow. + */ + _getSelectedText: function() { + // Can be called from FindInPageBar "TextSelection:Get", when there + // is no active selection. + if (!this._selectionID) { + return ""; + } + + let selection = this._getSelection(); + + // Textarea can contain LF, etc. + if (this._targetElement instanceof Ci.nsIDOMHTMLTextAreaElement) { + let flags = Ci.nsIDocumentEncoder.OutputPreformatted | + Ci.nsIDocumentEncoder.OutputRaw; + return selection.QueryInterface(Ci.nsISelectionPrivate). + toStringWithFormat("text/plain", flags, 0); + } + + // Selection text gets trimmed up. + return selection.toString().trim(); + }, + + /** + * Provides the nsISelection for either an editor, or from the + * default window. + */ + _getSelection: function(element = this._targetElement, win = this._contentWindow) { + return (element instanceof Ci.nsIDOMNSEditableElement) ? + this._getEditor(element).selection : + win.getSelection(); + }, + + /** + * Returns an nsEditor or nsHTMLEditor. + */ + _getEditor: function(element = this._targetElement, win = this._contentWindow) { + if (element instanceof Ci.nsIDOMNSEditableElement) { + return element.QueryInterface(Ci.nsIDOMNSEditableElement).editor; + } + + return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation). + QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIEditingSession). + getEditorForWindow(win); + }, + + /** + * Returns a selection controller. + */ + _getSelectionController: function(element = this._targetElement, win = this._contentWindow) { + if (element instanceof Ci.nsIDOMNSEditableElement) { + return this._getEditor(element, win).selectionController; + } + + return win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation). + QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay). + QueryInterface(Ci.nsISelectionController); + }, + + /** + * For selectAll(), provides the editor, or the default window selection Controller. + */ + _getSelectAllController: function(element = this._targetElement, win = this._contentWindow) { + let editor = this._getEditor(element, win); + return (editor) ? + editor : this._getSelectionController(element, win); + }, + + /** + * Call / Phone Helper methods. + */ + _getSelectedPhoneNumber: function() { + let selectedText = this._getSelectedText().trim(); + return this._isPhoneNumber(selectedText) ? + selectedText : null; + }, + + _isPhoneNumber: function(selectedText) { + return (PHONE_REGEX.test(selectedText)); + }, +};
--- a/mobile/android/chrome/content/SelectionHandler.js +++ b/mobile/android/chrome/content/SelectionHandler.js @@ -3,44 +3,54 @@ * 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"; // Define elements that bound phone number containers. const PHONE_NUMBER_CONTAINERS = "td,div"; const DEFER_CLOSE_TRIGGER_MS = 125; // Grace period delay before deferred _closeSelection() +// Gecko TouchCaret/SelectionCaret pref names. +const PREF_GECKO_TOUCHCARET_ENABLED = "touchcaret.enabled"; +const PREF_GECKO_SELECTIONCARETS_ENABLED = "selectioncaret.enabled"; + var SelectionHandler = { // Successful startSelection() or attachCaret(). ERROR_NONE: "", // Error codes returned during startSelection(). START_ERROR_INVALID_MODE: "Invalid selection mode requested.", START_ERROR_NONTEXT_INPUT: "Target element by definition contains no text.", START_ERROR_NO_WORD_SELECTED: "No word selected at point.", START_ERROR_SELECT_WORD_FAILED: "Word selection at point failed.", START_ERROR_SELECT_ALL_PARAGRAPH_FAILED: "Select-All Paragraph failed.", START_ERROR_NO_SELECTION: "Selection performed, but nothing resulted.", START_ERROR_PROXIMITY: "Selection target and result seem unrelated.", + START_ERROR_SELECTIONCARETS_ENABLED: "Native selectionCarets requested while Gecko enabled.", // Error codes returned during attachCaret(). ATTACH_ERROR_INCOMPATIBLE: "Element disabled, handled natively, or not editable.", + ATTACH_ERROR_TOUCHCARET_ENABLED: "Native touchCaret requested while Gecko enabled.", HANDLE_TYPE_ANCHOR: "ANCHOR", HANDLE_TYPE_CARET: "CARET", HANDLE_TYPE_FOCUS: "FOCUS", TYPE_NONE: 0, TYPE_CURSOR: 1, TYPE_SELECTION: 2, SELECT_ALL: 0, SELECT_AT_POINT: 1, + // Gecko TouchCaret/SelectionCaret pref values. + _touchCaretEnabledValue: null, + _selectionCaretEnabledValue: null, + // Keeps track of data about the dimensions of the selection. Coordinates // stored here are relative to the _contentWindow window. _cache: { anchorPt: {}, focusPt: {} }, _targetIsRTL: false, _anchorIsRTL: false, _focusIsRTL: false, _activeType: 0, // TYPE_NONE @@ -86,16 +96,40 @@ var SelectionHandler = { // Provides UUID service for selection ID's. get _idService() { delete this._idService; return this._idService = Cc["@mozilla.org/uuid-generator;1"]. getService(Ci.nsIUUIDGenerator); }, + // Are we supporting Gecko or Native touchCarets? + get _touchCaretEnabled() { + if (this._touchCaretEnabledValue == null) { + this._touchCaretEnabledValue = Services.prefs.getBoolPref(PREF_GECKO_TOUCHCARET_ENABLED); + Services.prefs.addObserver(PREF_GECKO_TOUCHCARET_ENABLED, function() { + SelectionHandler._touchCaretEnabledValue = + Services.prefs.getBoolPref(PREF_GECKO_TOUCHCARET_ENABLED); + }, false); + } + return this._touchCaretEnabledValue; + }, + + // Are we supporting Gecko or Native selectionCarets? + get _selectionCaretEnabled() { + if (this._selectionCaretEnabledValue == null) { + this._selectionCaretEnabledValue = Services.prefs.getBoolPref(PREF_GECKO_SELECTIONCARETS_ENABLED); + Services.prefs.addObserver(PREF_GECKO_SELECTIONCARETS_ENABLED, function() { + SelectionHandler._selectionCaretEnabledValue = + Services.prefs.getBoolPref(PREF_GECKO_SELECTIONCARETS_ENABLED); + }, false); + } + return this._selectionCaretEnabledValue; + }, + _addObservers: function sh_addObservers() { Services.obs.addObserver(this, "Gesture:SingleTap", false); Services.obs.addObserver(this, "Tab:Selected", false); Services.obs.addObserver(this, "TextSelection:Move", false); Services.obs.addObserver(this, "TextSelection:Position", false); Services.obs.addObserver(this, "TextSelection:End", false); Services.obs.addObserver(this, "TextSelection:Action", false); Services.obs.addObserver(this, "TextSelection:LayerReflow", false); @@ -373,16 +407,21 @@ var SelectionHandler = { * @param aOptions list of options describing how to start selection * Options include: * mode - SELECT_ALL to select everything in the target * element, or SELECT_AT_POINT to select a word. * x - The x-coordinate for SELECT_AT_POINT. * y - The y-coordinate for SELECT_AT_POINT. */ startSelection: function sh_startSelection(aElement, aOptions = { mode: SelectionHandler.SELECT_ALL }) { + // Disable Native touchCarets if Gecko enabled. + if (this._selectionCaretEnabled) { + return this.START_ERROR_SELECTIONCARETS_ENABLED; + } + // Clear out any existing active selection this._closeSelection(); if (this._isNonTextInputElement(aElement)) { return this.START_ERROR_NONTEXT_INPUT; } const focus = Services.focus.focusedWindow; @@ -832,16 +871,21 @@ var SelectionHandler = { /* * Called by BrowserEventHandler when the user taps in a form input. * Initializes SelectionHandler and positions the caret handle. * * @param aX, aY tap location in client coordinates. */ attachCaret: function sh_attachCaret(aElement) { + // Disable Native attachCaret() if Gecko touchCarets are enabled. + if (this._touchCaretEnabled) { + return this.ATTACH_ERROR_TOUCHCARET_ENABLED; + } + // Clear out any existing active selection this._closeSelection(); // Ensure it isn't disabled, isn't handled by Android native dialog, and is editable text element if (aElement.disabled || InputWidgetHelper.hasInputWidget(aElement) || !this.isElementEditableText(aElement)) { return this.ATTACH_ERROR_INCOMPATIBLE; }
--- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -149,16 +149,22 @@ let lazilyLoadedObserverScripts = [ ["FindHelper", ["FindInPage:Opened", "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"], ["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"], ["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"], ["EmbedRT", ["GeckoView:ImportScript"], "chrome://browser/content/EmbedRT.js"], ["Reader", ["Reader:FetchContent", "Reader:Added", "Reader:Removed"], "chrome://browser/content/Reader.js"], ]; +if (AppConstants.NIGHTLY_BUILD) { + lazilyLoadedObserverScripts.push( + ["ActionBarHandler", ["ActionBar:OpenNew", "ActionBar:Close", "TextSelection:Get"], + "chrome://browser/content/ActionBarHandler.js"] + ); +} if (AppConstants.MOZ_WEBRTC) { lazilyLoadedObserverScripts.push( ["WebrtcUI", ["getUserMedia:request", "recording-device-events"], "chrome://browser/content/WebrtcUI.js"]) } lazilyLoadedObserverScripts.forEach(function (aScript) { let [name, notifications, script] = aScript; XPCOMUtils.defineLazyGetter(window, name, function() { @@ -4213,23 +4219,32 @@ Tab.prototype = { this.browser.addEventListener("pagehide", listener, true); } if (docURI.startsWith("about:reader")) { // Update the page action to show the "reader active" icon. Reader.updatePageAction(this); } - break; } case "DOMFormHasPassword": { LoginManagerContent.onDOMFormHasPassword(aEvent, this.browser.contentWindow); + + // Send logins for this hostname to Java. + let hostname = aEvent.target.baseURIObject.prePath; + let foundLogins = Services.logins.findLogins({}, hostname, "", ""); + if (foundLogins.length > 0) { + let displayHost = IdentityHandler.getEffectiveHost(); + let title = { text: displayHost, resource: hostname }; + let selectObj = { title: title, logins: foundLogins }; + Messaging.sendRequest({ type: "Doorhanger:Logins", data: selectObj }); + } break; } case "DOMMetaAdded": let target = aEvent.originalTarget; let browser = BrowserApp.getBrowserForDocument(target.ownerDocument); switch (target.name) { @@ -6245,16 +6260,22 @@ var XPInstallObserver = { }]; } NativeWindow.doorhanger.show(message, aTopic, buttons, tab.id); break; } }, onInstallEnded: function(aInstall, aAddon) { + // Don't create a notification for distribution add-ons. + if (Distribution.pendingAddonInstalls.has(aInstall)) { + Distribution.pendingAddonInstalls.delete(aInstall); + return; + } + let needsRestart = false; if (aInstall.existingAddon && (aInstall.existingAddon.pendingOperations & AddonManager.PENDING_UPGRADE)) needsRestart = true; else if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL) needsRestart = true; if (needsRestart) { this.showRestartPrompt(); @@ -6269,16 +6290,23 @@ var XPInstallObserver = { callback: () => { BrowserApp.addTab("about:addons#" + aAddon.id, { parentId: BrowserApp.selectedTab.id }); }, } }); } } }, onInstallFailed: function(aInstall) { + // Don't create a notification for distribution add-ons. + if (Distribution.pendingAddonInstalls.has(aInstall)) { + Cu.reportError("Error installing distribution add-on: " + aInstall.addon.id); + Distribution.pendingAddonInstalls.delete(aInstall); + return; + } + NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsFail"), "short"); }, onDownloadProgress: function xpidm_onDownloadProgress(aInstall) {}, onDownloadFailed: function(aInstall) { this.onInstallFailed(aInstall); }, @@ -7678,16 +7706,17 @@ var Distribution = { } catch (e) { console.log("Unable to reinit search service."); } // Fall through. case "Distribution:Set": // Reload the default prefs so we can observe "prefservice:after-app-defaults" Services.prefs.QueryInterface(Ci.nsIObserver).observe(null, "reload-default-prefs", null); + this.installDistroAddons(); break; case "prefservice:after-app-defaults": this.getPrefs(); break; case "Campaign:Set": { // Update the prefs for this session @@ -7800,17 +7829,71 @@ var Distribution = { } catch (e) { Cu.reportError("Distribution: Could not parse JSON: " + e); } }).then(null, function onError(reason) { if (!(reason instanceof OS.File.Error && reason.becauseNoSuchFile)) { Cu.reportError("Distribution: Could not read from " + aFile.leafName + " file"); } }); - } + }, + + // Track pending installs so we can avoid showing notifications for them. + pendingAddonInstalls: new Set(), + + installDistroAddons: Task.async(function* () { + const PREF_ADDONS_INSTALLED = "distribution.addonsInstalled"; + try { + let installed = Services.prefs.getBoolPref(PREF_ADDONS_INSTALLED); + if (installed) { + return; + } + } catch (e) { + Services.prefs.setBoolPref(PREF_ADDONS_INSTALLED, true); + } + + let distroPath; + try { + distroPath = FileUtils.getDir("XREAppDist", ["extensions"]).path; + + let info = yield OS.File.stat(distroPath); + if (!info.isDir) { + return; + } + } catch (e) { + return; + } + + let it = new OS.File.DirectoryIterator(distroPath); + try { + yield it.forEach(entry => { + // Only support extensions that are zipped in .xpi files. + if (entry.isDir || !entry.name.endsWith(".xpi")) { + dump("Ignoring distribution add-on that isn't an XPI: " + entry.path); + return; + } + + new Promise((resolve, reject) => { + AddonManager.getInstallForFile(new FileUtils.File(entry.path), resolve); + }).then(install => { + let id = entry.name.substring(0, entry.name.length - 4); + if (install.addon.id !== id) { + Cu.reportError("File entry " + entry.path + " contains an add-on with an incorrect ID"); + return; + } + this.pendingAddonInstalls.add(install); + install.install(); + }).catch(e => { + Cu.reportError("Error installing distribution add-on: " + entry.path + ": " + e); + }); + }); + } finally { + it.close(); + } + }) }; var Tabs = { _enableTabExpiration: false, _domains: new Set(), init: function() { // On low-memory platforms, always allow tab expiration. On high-mem
--- a/mobile/android/chrome/jar.mn +++ b/mobile/android/chrome/jar.mn @@ -30,16 +30,17 @@ chrome.jar: content/languages.properties (content/languages.properties) content/browser.xul (content/browser.xul) content/browser.js (content/browser.js) content/bindings/checkbox.xml (content/bindings/checkbox.xml) content/bindings/settings.xml (content/bindings/settings.xml) content/netError.xhtml (content/netError.xhtml) content/SelectHelper.js (content/SelectHelper.js) content/SelectionHandler.js (content/SelectionHandler.js) + content/ActionBarHandler.js (content/ActionBarHandler.js) content/WebappRT.js (content/WebappRT.js) content/EmbedRT.js (content/EmbedRT.js) content/InputWidgetHelper.js (content/InputWidgetHelper.js) content/WebrtcUI.js (content/WebrtcUI.js) content/MemoryObserver.js (content/MemoryObserver.js) content/ConsoleAPI.js (content/ConsoleAPI.js) content/PluginHelper.js (content/PluginHelper.js) content/OfflineApps.js (content/OfflineApps.js)
--- a/mobile/android/confvars.sh +++ b/mobile/android/confvars.sh @@ -95,12 +95,14 @@ MOZ_ANDROID_SHARE_OVERLAY=1 # Enable the Mozilla Location Service stumbler. MOZ_ANDROID_MLS_STUMBLER=1 # Enable adding to the system downloads list in pre-release builds. MOZ_ANDROID_DOWNLOADS_INTEGRATION=1 # Enable Tab Queue -MOZ_ANDROID_TAB_QUEUE=1 +if test "$NIGHTLY_BUILD"; then + MOZ_ANDROID_TAB_QUEUE=1 +fi # Use the low-memory GC tuning. export JS_GC_SMALL_CHUNK_SIZE=1
--- a/mobile/android/installer/package-manifest.in +++ b/mobile/android/installer/package-manifest.in @@ -541,16 +541,30 @@ @BINPATH@/res/table-remove-row-active.gif @BINPATH@/res/table-remove-row-hover.gif @BINPATH@/res/table-remove-row.gif @BINPATH@/res/grabber.gif @BINPATH@/res/dtd/* @BINPATH@/res/html/* @BINPATH@/res/language.properties @BINPATH@/res/entityTables/* +#ifdef NIGHTLY_BUILD +@BINPATH@/res/text_caret.png +@BINPATH@/res/text_caret@1.5x.png +@BINPATH@/res/text_caret@2.25x.png +@BINPATH@/res/text_caret@2x.png +@BINPATH@/res/text_caret_tilt_left.png +@BINPATH@/res/text_caret_tilt_left@1.5x.png +@BINPATH@/res/text_caret_tilt_left@2.25x.png +@BINPATH@/res/text_caret_tilt_left@2x.png +@BINPATH@/res/text_caret_tilt_right.png +@BINPATH@/res/text_caret_tilt_right@1.5x.png +@BINPATH@/res/text_caret_tilt_right@2.25x.png +@BINPATH@/res/text_caret_tilt_right@2x.png +#endif #ifndef MOZ_ANDROID_EXCLUDE_FONTS @BINPATH@/res/fonts/* #endif ; svg @BINPATH@/res/svg.css @BINPATH@/components/dom_svg.xpt
--- a/mobile/android/modules/Home.jsm +++ b/mobile/android/modules/Home.jsm @@ -310,16 +310,17 @@ let HomePanels = (function () { INTENT: "intent" }); function Panel(id, options) { this.id = id; this.title = options.title; this.layout = options.layout; this.views = options.views; + this.default = !!options.default; if (!this.id || !this.title) { throw "Home.panels: Can't create a home panel without an id and title!"; } if (!this.layout) { // Use FRAME layout by default this.layout = Layout.FRAME; @@ -373,16 +374,20 @@ let HomePanels = (function () { buttonText: options.auth.buttonText }; // Include optional image URL if it is specified. if (options.auth.imageUrl) { this.authConfig.imageUrl = options.auth.imageUrl; } } + + if (options.position && typeof options.position === "number") { + this.position = options.position; + } } let _generatePanel = function(id) { let options = _registeredPanels[id](); return new Panel(id, options); }; // Helper function used to see if a value is in an object.
index 7c299823caeed7a27055cc121d79b5b9cbeadea2..9a0c5e6c95f5348cea461e4c60340c96a36a4b15 GIT binary patch literal 5094 zc$}4&2{@E{`^PPjELkF1OSWWR5>jI+5@XFUvJEq1H=4m%!kl8r-q;C8q(x;ZS;pR2 zv&&wHgD7kE^>5C3k2CcCJH5|y{jPbQnd|qt@8^E5`QG<eUz_p>EgA7BzMi8&MshHa zF_A$XQ0_<v8xMB}gsY^nDLt8am%cG^Et9j7>F;f2C8OCp2@s$f=~k}8TWt^sCl?6P z2_@l(Lbwt^OiNq_#6<*!MUGSK(T>tw?X-8J+S^aLxAgZDt2>m~sl#-bmpjZAwReU3 zGe^{3eyAUE)I3B{{0W8hdkJ?K%3Z|;;p5=!43R(}?IgV19Ef3j_CjDg{|ZAl4~H{D ztgT0^L)@%pJSU^)8r?Ohu`EK=)V$Oj*sQHB+$io5fuj7yXgiZG^#l()rCN?8RbV{G zO;)Pl4%K<8udb;hiGKAezb|hC40hJxpH7r-%DEJMoM20OqdGoQGk)XAX-x&)uUaVu zv8$85{ag36#f(2#`AGOzqP@YZo3&HucwHRup{2*U<=FWI-nqHs{<IG@)Wp*xy|`Dx z!LEq<&Y7)(XXfsMk$QeNs#|7TYSS676t`4e-tOP-ea#rQ0zC1sYy{uhg6w6hmrS#) zR@U2Uz6Z}Nh3QN+sZRG=(%r^#xTi9=74B^I>wMo?F;X<?O&;ny$$80Ez51i*Vve5p z<&LZhehB=@4%TKUSrpmwcpCBe+7+Q8*4N(3>w{ZzU9;+y(>1;|cG=WV0Hw_pi|Cih zb3s$b(X&)N7A0}~Ci&BM`lj9?cKu5ISJuCltK}BGTB>ZwHyB+(x1hs@W&D<}6t%-P zQ(xfvwcDC2dgCsAh8mdo)IP`Na7^mR3+Q(CI=jp&rFgVtT<=&NJI$w^pOC<?nkwng zypDEj+8y8IHLd`nD{kACZ<LkiM{K+=cPtp5I$FiAO?3*4@;wnZh>?zNYObnc2!GLB zHi{!W<Su^{E9YiTWgC_Au`zNT&02Na17`OngG(`?(|uVsA#fwzV!&81z*lxsKx6i9 z!TX$@xo&TZQM^NIO46{4-J9WZe&1TY*}KLQ?1jq~8k<4<0QTLq!A_9;W?RcJR~K?+ zn0=Qgc}>G>xv)2TAU<mH<8xFce&bg4-COy3-3cW?pu)1<00etq&q%b5x`Ef6C+2Kp z$;QN^UMHxdcp_)+`Ul_&Bi@yJcDn}~tms~CR8g`eBl|AJ1UR^9Gc}p#k{~IuHL@mx zw%veKUK3025k)m#3y^DWrfu)5o7vT>f#=uCXGEbUkml}}eeh-?Ijdu;SBioWw<z8( z-_+Yq=_(!bD~#>;)0zv^bB(B<Y~+?4Dt2iV?+6o2n;x7f^;hVrt*8ck9))^Gd}+IZ zzqAtXD!=oQV@ZUs$q@26p|9XxbHJ|Iyo;j*czb)JSw{Lh;@dHkIqWwu6c5w=xO74i zl`FF|qZDoBnJFf>G(3FQ5OKRZ_p7*7SSx@#;96VpNc}n|5Wg-P>M<!_-=w^be4DKa zStUP_3gEp_+0SgyNEhBTc4umaPsQ>)6N(~JrlGr|w%1tlA{F-ovp)XLqWT9p)tKOM zznPBgrOFoC&ExY3aL<6XJf!hnTiN%OvdeCDb9E7ksmfE5;vi!mu-&A;S$oowXJf|6 z(=jVYfvQJ$XCZVmj&JUJuVM{cZC{R9;m!4|X>Fl6+dAVj*QGFI(eLxJm<ej)yrz7$ zl*ytGfCQbw2BB9}hUXj-*>Ko}Qz0Cl4b&jAmnyVcpzGIdPSrP2ch=Acreov)bRkFo z)WF*cYvP(m$jI2K$Vk-7KEwQg!txweFE=C%4nx9RZDIdtEcCpT0dWx(Dk56iiBg(A zXmyjVE?Rt&Up>J&BUZx1C;DXRfSn+-0w~&#hjVdQRG8buz7rm^m9}(c-BjdpI;A4l zk|EFI7~b*^O6pC^)yT*yAAf@vnSAUEz^mOB%nVr@<C&%)E_(aRB8<<+$RF}Vq*omi z33cFOeBy0<MhVLxMd0ncIw}p*xCqTpp@7~vUBo|JyinA}OqD0due}1~szbKE3+ymN zQ2K+}M|2;#C1GR?i#-)j#{*G4Aw?}O{QS%d^<F<K25zHFfQx$JJ9CdW0Pd9e{#L`j zRfX;TN29Bj-T;D)pG<QR3n)-7)jF|PB6$kZCgPE1d+nz!3I&58ZSCEhJ?tF*(?<;U z1YQ?tdh_o-^4~#ce-5%mxVZeMcOK^b^=3uPkO$tZ*e~&5u6uVd2`?9CB3Z&r(}1{$ z<Zh4)YE+(SC(Y`!3@X*rjDio-!iMCxRp(aAdv|LjB_9S}5Pj=Rd2w|ZUq>y0cA0~3 z$QTI~Sb8pho6U>>ePX=SHg3pWD>v^Om-5vo=v@VK;s@dKraSUaY2z5tCZ9xl_}_s( z1?s2Y6Zij^(-$5z=Ts>6{#E&iiiJt#@w0|bAHL2E2+<at(kxsn&IL8{eLG{LR~kGA zSAd)Ik=tTI@P%PZ&qY7?h1fS#J-$T9&wW#pF7Fc#l$bApFfp+wjHcrY+lnkb*Z?|- z=2#RuJo}a}p5{iHu2h0SOuvc>zNSBsC55LRo!z>$D%blo=*qfy(V**2C=&}a?g9O9 z+3B;=DjaH*6kYy1s%AR|?OHpR0V1<cb^0LL1zEBoFRM>z4_zSi^L>*0J)rm0TCu}% z-n_(aX;rDBcFF=K7B`{LHG9x`R%3X6q||Xpytpl$_~^|c!kPAYc^(@4F~+gcTtED< z(xsSiZ(O64Fq9rI#xPHA6q_dmu{4$^=Qp$A7$e}wEeyt3*+Qn|djWr>^FJ~z&g1QG zTaE`_?dV}8_qiI~S)FuK@{d2{!(|CVY5{f^G&x=n7#&L01_iil^&B!{<Ncq%43bUl zeTH2#VelXWo*mfwGU=D`V6bPPr<fxuCC^HQK5En8k#o@myd`H<EiyjE1;EkXpEZQU zl$glP>I2CIHcv@+2#7l%l<wftI7Zp;mbSP`TktAvfzCx(5K030=y>clpEm-9)s@c_ z*=N(RiuIWV$fQO#CzzBLePlpl?{tRf>5P*VrLvr3VL9nC&PK^yEr^NXxi!hgDadqj zx@(;EQrP6>G?voj&{h0O7l-ILkH3u9%9qfRk>658l-~%2dk3GxoJCjV2z75%*I07C zwiq7#pqY1(1}^L`aetvb<jDKxST0HRqBQD=Gu79SL&=&^i+7l3obAi?oswJ`gV+@n z#mQyRn69^kWHBYp0aM{7Wg(^{%{mvx2n1i#$v1B^$4e%mHp8v6;I6IkZ7gQ0C0lQu zYP!f$Zb)%{N6`1;!T=xEw9<o5(!9ZDBuDi^@JqYjlpHluUr)p2KE2X-=`9q|V?O|X zq*=?cme6(X%grEb=&5%Z8OngGi?0`pIU8)(*!7@+%?UD-y8U2*a>2*%*k=sDF`C|5 zyu!J!JRsjkvGQ{tU|BU(kYg-{U)!UYlbZ!&WtW9=kqNlEY=99cDeKjJ0f`F^oEN&) z#?){FoL7Y1gr7xwOSN2uDq<MeDI%GDl%~eLg6YseWEVnQ#-Xf9qb78I^W>$6i`KU@ zUukp_R7NraUkG_6`Ih231ne@(oFd;E4KS??y{|yDwSQAIlvitno;TIK#dbysHKgef zZMz&6V#0J=wo>eU%3}Hko0nVTq=~%w?DvN8zAJ?CP+qLWg{3l?51!o$d`G5fbw;f$ z$EtGOc;TMbE9cd{LBA!B1-)vWFKLL|7(AlMTHpJJspqSRsvK21i)J_Q^pzvW?rn6w zwhw%1y_z{2RLLPxyE7;_IO7-iXnlx*{!?vwq|@L~J%ufnUvlT>v+{-X&pbK(&8W(8 zN6RR{_gqXMMe}V7<0$v2?-@-i=LcyMrd=sAb6@ypXf1#*Ku#*tRDh0EynJLuUtqYU zUf87N%Vczq4rTnibZCj=@;RQP!ELMT0w4wcd0<myF58J)KBH>lZKtWKuEvgBgJfit zJ~vU&MvqCxYk#-m%<~wopXH*1umq{n_l=Xip#T7mF>W?0rZt4@9?!bB3T8^OQ=n@i zEX+H(Z3H}c7Y*pf-JRxG1~fzjY+EmuDR4EOd##u%%12kJ;F$Vl%)WV@T$xGTSwE~M zXI`&!5+6CrU33~Vwwo&_lJ@1^nZ?<iwKtz1_!*QawS->W{?y$)N9;|468h+*y~a*Q zPuiPY{h<e13il5!z{AdN&xOFCD2F{avfoTFaPXn#r2O<?a}xL$k~6{<;!Hv$SR8J5 zZHIC@|BFPJtGKbzK3Coje5mam=3f4vxCejSA9#pi{tgv3VW_YFl$u%P_nT(1nb60v ziLq8@HgT~ri3u+0IIy{4EaGVvr4cSR?O}S1)sdUo&Nk7G(KeX9;UB1@N5%EPBsqqX zU7}H}QH$z1nw%pp>82SjDg6lsYNKUfgY<-3`oKeRaTyP!uNQ-Y&)IQ6JV0j2CEhk? zy<25!X2nhz$>zN-wki2@sDp1dJ-fg6l+xtH_fAg1M*07FZu-A3Rc2I)ABP+`&AtQw z3-gcvCiTu-B$%w<E*-`kcH=brZk#l?A8j{jZb^rs2TVB4z6mEylW4(7$6Lh%X%7E{ z_zQ-dX5WyLh9u6-qyr((fsm<GWdAlr)9g>tq_z2Rnk22w5H;D)XG@y>S@37jvj?EU zM}G?4pED1$|CusL#YgNw9ninTCY?EH_GeDgC-_mwNT0xm;c%40vPQEnYoyd4MU9k7 z_!sIw5=XNyaisJg1&)+{^7yaO|CT$NeYqp8J~0NV1YAB)eb@gf#($MQntkch*QTN- Qa_RR@%f0^@l971#f5Dgkz5oCK
--- a/mobile/android/tests/browser/robocop/robocop.ini +++ b/mobile/android/tests/browser/robocop/robocop.ini @@ -163,16 +163,17 @@ skip-if = android_version == "10" || and [testJavascriptBridge] [testNativeCrypto] [testReaderModeTitle] [testSessionHistory] # disabled on Android 4.3, bug 1144879 skip-if = android_version == "18" [testStateWhileLoading] +[testSelectionCarets] # testSelectionHandler disabled on Android 2.3 by trailing skip-if, due to bug 980074 # also disabled on Android 4.3, bug 1144882 [testSelectionHandler] skip-if = android_version == "10" || android_version == "18" # testInputSelections disabled on Android 2.3 by trailing skip-if, due to bug 980074 [testInputSelections] skip-if = android_version == "10"
--- a/mobile/android/tests/browser/robocop/testDistribution.java +++ b/mobile/android/tests/browser/robocop/testDistribution.java @@ -48,16 +48,18 @@ import android.util.Log; * bookmarks.json * searchplugins/ * common/ * engine.xml * suggestedsites/ * locales/ * en-US/ * suggestedsites.json + * extensions/ + * distribution.test@mozilla.org.xpi */ public class testDistribution extends ContentProviderTest { private static final String CLASS_REFERRER_RECEIVER = "org.mozilla.gecko.distribution.ReferrerReceiver"; private static final String ACTION_INSTALL_REFERRER = "com.android.vending.INSTALL_REFERRER"; private static final int WAIT_TIMEOUT_MSEC = 10000; public static final String LOGTAG = "GeckoTestDistribution"; public static class TestableDistribution extends Distribution { @@ -147,16 +149,17 @@ public class testDistribution extends Co // Pre-clear distribution pref, run basic preferences and en-US localized preferences Tests clearDistributionPref(); setTestLocale("en-US"); initDistribution(mockPackagePath); checkPreferences(); checkLocalizedPreferences("en-US"); checkSearchPlugin(); + checkAddon(); // Pre-clear distribution pref, and run es-MX localized preferences Test clearDistributionPref(); setTestLocale("es-MX"); initDistribution(mockPackagePath); checkLocalizedPreferences("es-MX"); // Test the (stubbed) download interaction. @@ -298,30 +301,17 @@ public class testDistribution extends Co try { final String[] prefNames = { prefID, prefAbout, prefVersion, prefTestBoolean, prefTestString, prefTestInt }; - Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("Preferences:Data"); - mActions.sendPreferencesGetEvent(PREF_REQUEST_ID, prefNames); - - JSONObject data = null; - int requestId = -1; - - // Wait until we get the correct "Preferences:Data" event - while (requestId != PREF_REQUEST_ID) { - data = new JSONObject(eventExpecter.blockForEventData()); - requestId = data.getInt("requestId"); - } - eventExpecter.unregisterListener(); - - JSONArray preferences = data.getJSONArray("preferences"); + final JSONArray preferences = getPrefs(prefNames); for (int i = 0; i < preferences.length(); i++) { JSONObject pref = (JSONObject) preferences.get(i); String name = pref.getString("name"); if (name.equals(prefID)) { mAsserter.is(pref.getString("value"), "test-partner", "check " + prefID); } else if (name.equals(prefAbout)) { mAsserter.is(pref.getString("value"), "Test Partner", "check " + prefAbout); @@ -359,16 +349,44 @@ public class testDistribution extends Co } } mAsserter.ok(foundEngine, "check search plugin", "found test search plugin"); } catch (JSONException e) { mAsserter.ok(false, "exception getting search plugins", e.toString()); } } + private void checkAddon() { + try { + final String[] prefNames = { "distribution.test.addonEnabled" }; + final JSONArray preferences = getPrefs(prefNames); + final JSONObject pref = (JSONObject) preferences.get(0); + mAsserter.is(pref.getBoolean("value"), true, "check distribution add-on is enabled"); + } catch (JSONException e) { + mAsserter.ok(false, "exception getting preferences", e.toString()); + } + } + + private JSONArray getPrefs(String[] prefNames) throws JSONException { + Actions.RepeatedEventExpecter eventExpecter = mActions.expectGeckoEvent("Preferences:Data"); + mActions.sendPreferencesGetEvent(PREF_REQUEST_ID, prefNames); + + JSONObject data = null; + int requestId = -1; + + // Wait until we get the correct "Preferences:Data" event + while (requestId != PREF_REQUEST_ID) { + data = new JSONObject(eventExpecter.blockForEventData()); + requestId = data.getInt("requestId"); + } + eventExpecter.unregisterListener(); + + return data.getJSONArray("preferences"); + } + // Sets the distribution locale preference for the test. private void setTestLocale(String locale) { BrowserLocaleManager.getInstance().setSelectedLocale(mActivity, locale); } // Test localized distribution and preferences values stored in preferences.json private void checkLocalizedPreferences(String aLocale) { String prefAbout = "distribution.about";
new file mode 100644 --- /dev/null +++ b/mobile/android/tests/browser/robocop/testSelectionCarets.html @@ -0,0 +1,35 @@ +<html> + <head> + <title>ActionBar Handler and SelectionCarets tests</title> + <meta name="viewport" + content="initial-scale=1, allowZoom=no, maximum-scale=1, + user-scalable=no, width=device-width"> + </head> + + <body> + <div id="LTRcontenteditable" + style="direction: ltr; width: 10em; height: 2em; word-wrap: break-word; + overflow: auto; -moz-user-select:text" + contenteditable="true">Find my book</div> + <div id="RTLcontenteditable" + style="direction: rtl; width: 10em; height: 2em; word-wrap: break-word; + overflow: auto; -moz-user-select:text" + contenteditable="true">איפה האוטו שלי</div> + + <div id="LTRtextContent" + style="direction: ltr; width: 10em; height: 2em; word-wrap: break-word; + overflow: auto; -moz-user-select:text">Open the door</div> + <div id="RTLtextContent" + style="direction: rtl; width: 10em; height: 2em; word-wrap: break-word; + overflow: auto; -moz-user-select:text">תן לי מים</div> + + <input id="LTRinput" style="direction: ltr;" value="Type something"> + <input id="RTLinput" style="direction: rtl;" value="לרוץ במעלה הגבעה"> + <br> + + <textarea id="LTRtextarea" style="direction: ltr;" + rows="3" cols="8">Words in a box</textarea> + <textarea id="RTLtextarea" style="direction: rtl;" + rows="3" cols="8">הספר הוא טוב</textarea> + </body> +</html>
new file mode 100644 --- /dev/null +++ b/mobile/android/tests/browser/robocop/testSelectionCarets.java @@ -0,0 +1,106 @@ +/** + * 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.tests; + +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.EventDispatcher; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.GeckoEvent; +import org.mozilla.gecko.Tab; +import org.mozilla.gecko.Tabs; +import org.mozilla.gecko.util.GeckoEventListener; + +import android.os.SystemClock; +import android.util.Log; +import android.view.MotionEvent; + +import org.json.JSONException; +import org.json.JSONObject; + +public class testSelectionCarets extends JavascriptTest implements GeckoEventListener { + private static final String LOGTAG = "testSelectionCarets"; + + private static final String LONGPRESS_EVENT = "testSelectionCarets:Longpress"; + private static final String TAB_CHANGE_EVENT = "testSelectionCarets:TabChange"; + + private final TabsListener tabsListener; + + public testSelectionCarets() { + super("testSelectionCarets.js"); + + tabsListener = new TabsListener(); + } + + @Override + public void setUp() throws Exception { + super.setUp(); + + Tabs.registerOnTabsChangedListener(tabsListener); + EventDispatcher.getInstance().registerGeckoThreadListener(this, LONGPRESS_EVENT); + } + + @Override + public void testJavascript() throws Exception { + // This feature is currently only available in Nightly. + if (!AppConstants.NIGHTLY_BUILD) { + mAsserter.dumpLog(LOGTAG + " is disabled on non-Nightly builds: returning"); + return; + } + super.testJavascript(); + } + + @Override + public void tearDown() throws Exception { + Tabs.unregisterOnTabsChangedListener(tabsListener); + EventDispatcher.getInstance().unregisterGeckoThreadListener(this, LONGPRESS_EVENT); + + super.tearDown(); + } + + /** + * The test script will request us to trigger Longpress AndroidGeckoEvents. + */ + @Override + public void handleMessage(String event, final JSONObject message) { + switch(event) { + case LONGPRESS_EVENT: { + final long meTime = SystemClock.uptimeMillis(); + final int meX = Math.round(message.optInt("x", 0)); + final int meY = Math.round(message.optInt("y", 0)); + final MotionEvent motionEvent = + MotionEvent.obtain(meTime, meTime, MotionEvent.ACTION_DOWN, meX, meY, 0); + + final GeckoEvent geckoEvent = GeckoEvent.createLongPressEvent(motionEvent); + GeckoAppShell.sendEventToGecko(geckoEvent); + break; + } + } + } + + /** + * Observes tab change events to broadcast to the test script. + */ + private class TabsListener implements Tabs.OnTabsChangedListener { + @Override + public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) { + switch (msg) { + case STOP: + final JSONObject args = new JSONObject(); + try { + args.put("tabId", tab.getId()); + args.put("event", msg.toString()); + } catch (JSONException e) { + Log.e(LOGTAG, "Error building JSON arguments for " + TAB_CHANGE_EVENT, e); + return; + } + final GeckoEvent event = + GeckoEvent.createBroadcastEvent(TAB_CHANGE_EVENT, args.toString()); + GeckoAppShell.sendEventToGecko(event); + break; + } + } + } +}
new file mode 100644 --- /dev/null +++ b/mobile/android/tests/browser/robocop/testSelectionCarets.js @@ -0,0 +1,244 @@ +// -*- indent-tabs-mode: nil; js-indent-level: 2 -*- +/* 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"; + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Messaging.jsm"); +Cu.import('resource://gre/modules/Geometry.jsm'); + +const SELECTION_CARETS_PREF = "selectioncaret.enabled"; +const TOUCH_CARET_PREF = "touchcaret.enabled"; +const TEST_URL = "http://mochi.test:8888/tests/robocop/testSelectionCarets.html"; + +// After longpress, Gecko notifys ActionBarHandler to init then update state. +// When it does, we'll peek over its shoulder and test status. +const LONGPRESS_EVENT = "testSelectionCarets:Longpress"; +const STATUS_UPDATE_EVENT = "ActionBar:UpdateState"; + +// Ensures Tabs are completely loaded, viewport and zoom constraints updated, etc. +const TAB_CHANGE_EVENT = "testSelectionCarets:TabChange"; +const TAB_STOP_EVENT = "STOP"; + +const gChromeWin = Services.wm.getMostRecentWindow("navigator:browser"); + +/** + * Robocop test helpers. + */ +function ok(passed, text) { + do_report_result(passed, text, Components.stack.caller, false); +} + +function is(lhs, rhs, text) { + do_report_result(lhs === rhs, "[ " + lhs + " === " + rhs + " ] " + text, + Components.stack.caller, false); +} + +/** + * Wait for and return, when an expected tab change event occurs. + * + * @param tabId, The id of the target tab we're observing. + * @param eventType, The event type we expect. + * @return {Promise} + * @resolves The tab change object, including the matched tab id and event. + */ +function do_promiseTabChangeEvent(tabId, eventType) { + return new Promise(resolve => { + let observer = (subject, topic, data) => { + let message = JSON.parse(data); + + if (message.event === eventType && message.tabId === tabId) { + Services.obs.removeObserver(observer, TAB_CHANGE_EVENT); + resolve(data); + } + } + + Services.obs.addObserver(observer, TAB_CHANGE_EVENT, false); + }); +} + +/** + * Selection methods vary if we have an input / textarea element, + * or if we have basic content. + */ +function isInputOrTextarea(element) { + return ((element instanceof Ci.nsIDOMHTMLInputElement) || + (element instanceof Ci.nsIDOMHTMLTextAreaElement)); +} + +/** + * Return the selection controller based on element. + */ +function elementSelection(element) { + return (isInputOrTextarea(element)) ? + element.editor.selection : + element.ownerDocument.defaultView.getSelection(); +} + +/** + * Select the first character of a target element, w/o affecting focus. + */ +function selectElementFirstChar(doc, element) { + if (isInputOrTextarea(element)) { + element.setSelectionRange(0, 1); + return; + } + + // Simple test cases designed firstChild == #text node. + let range = doc.createRange(); + range.setStart(element.firstChild, 0); + range.setEnd(element.firstChild, 1); + + let selection = elementSelection(element); + selection.removeAllRanges(); + selection.addRange(range); +} + +/** + * Get longpress point. Determine the midpoint in the first character of + * the content in the element. X will be midpoint from left to right. + * Y will be 1/3 of the height up from the bottom to account for both + * LTR and smaller RTL characters. ie: |X| vs. |א| + */ +function getFirstCharPressPoint(doc, element, expected) { + // Select the first char in the element. + selectElementFirstChar(doc, element); + + // Reality check selected char to expected. + let selection = elementSelection(element); + is(selection.toString(), expected, "Selected char should match expected char."); + + // Return a point where long press should select entire word. + let rect = selection.getRangeAt(0).getBoundingClientRect(); + let r = new Point(rect.left + (rect.width / 2), rect.bottom - (rect.height / 3)); + + return r; +} + +/** + * Long press an element (RTL/LTR) at its calculated first character + * position, and return the result. + * + * @param midPoint, The screen coord for the longpress. + * @return {Promise} + * @resolves The ActionBar status, including its target focused element, and + * the selected text that it sees. + */ +function do_promiseLongPressResult(midPoint) { + return new Promise(resolve => { + let observer = (subject, topic, data) => { + let ActionBarHandler = gChromeWin.ActionBarHandler; + if (topic === STATUS_UPDATE_EVENT) { + let text = ActionBarHandler._getSelectedText(); + if (text !== "") { + // Remove notification observer, and resolve. + Services.obs.removeObserver(observer, STATUS_UPDATE_EVENT); + resolve({ + focusedElement: ActionBarHandler._targetElement, + text: text, + }); + } + } + }; + + // Add notification observer, trigger the longpress and wait. + Services.obs.addObserver(observer, STATUS_UPDATE_EVENT, false); + Messaging.sendRequestForResult({ + type: LONGPRESS_EVENT, + x: midPoint.x, + y: midPoint.y, + }); + }); +} + +/** + * Main test method. + */ +add_task(function* testSelectionCarets() { + // Wait to start loading our test page until after the initial browser tab is + // completely loaded. This allows each tab to complete its layer initialization, + // importantly, its viewport and zoomContraints info. + let BrowserApp = gChromeWin.BrowserApp; + yield do_promiseTabChangeEvent(BrowserApp.selectedTab.id, TAB_STOP_EVENT); + + // Ensure Gecko Selection and Touch carets are enabled. + Services.prefs.setBoolPref(SELECTION_CARETS_PREF, true); + Services.prefs.setBoolPref(TOUCH_CARET_PREF, true); + + // Load test page, wait for load completion, register cleanup. + let browser = BrowserApp.addTab(TEST_URL).browser; + let tab = BrowserApp.getTabForBrowser(browser); + yield do_promiseTabChangeEvent(tab.id, TAB_STOP_EVENT); + + do_register_cleanup(function cleanup() { + Services.prefs.clearUserPref(SELECTION_CARETS_PREF); + Services.prefs.clearUserPref(TOUCH_CARET_PREF); + BrowserApp.closeTab(tab); + }); + + // References to test document elements. + let doc = browser.contentDocument; + let ce_LTR_elem = doc.getElementById("LTRcontenteditable"); + let tc_LTR_elem = doc.getElementById("LTRtextContent"); + let i_LTR_elem = doc.getElementById("LTRinput"); + let ta_LTR_elem = doc.getElementById("LTRtextarea"); + + let ce_RTL_elem = doc.getElementById("RTLcontenteditable"); + let tc_RTL_elem = doc.getElementById("RTLtextContent"); + let i_RTL_elem = doc.getElementById("RTLinput"); + let ta_RTL_elem = doc.getElementById("RTLtextarea"); + + // Locate longpress midpoints for test elements, ensure expactations. + let ce_LTR_midPoint = getFirstCharPressPoint(doc, ce_LTR_elem, "F"); + let tc_LTR_midPoint = getFirstCharPressPoint(doc, tc_LTR_elem, "O"); + let i_LTR_midPoint = getFirstCharPressPoint(doc, i_LTR_elem, "T"); + let ta_LTR_midPoint = getFirstCharPressPoint(doc, ta_LTR_elem, "W"); + + let ce_RTL_midPoint = getFirstCharPressPoint(doc, ce_RTL_elem, "א"); + let tc_RTL_midPoint = getFirstCharPressPoint(doc, tc_RTL_elem, "ת"); + let i_RTL_midPoint = getFirstCharPressPoint(doc, i_RTL_elem, "ל"); + let ta_RTL_midPoint = getFirstCharPressPoint(doc, ta_RTL_elem, "ה"); + + + // Longpress various LTR content elements. Test focused element against + // expected, and selected text against expected. + let result = yield do_promiseLongPressResult(ce_LTR_midPoint); + is(result.focusedElement, ce_LTR_elem, "Focused element should match expected."); + is(result.text, "Find", "Selected text should match expected text."); + + result = yield do_promiseLongPressResult(tc_LTR_midPoint); + is(result.focusedElement, null, "No focused element is expected."); + is(result.text, "Open", "Selected text should match expected text."); + + result = yield do_promiseLongPressResult(i_LTR_midPoint); + is(result.focusedElement, i_LTR_elem, "Focused element should match expected."); + is(result.text, "Type", "Selected text should match expected text."); + + result = yield do_promiseLongPressResult(ta_LTR_midPoint); + is(result.focusedElement, ta_LTR_elem, "Focused element should match expected."); + is(result.text, "Words", "Selected text should match expected text."); + + // Longpress various RTL content elements. Test focused element against + // expected, and selected text against expected. + result = yield do_promiseLongPressResult(ce_RTL_midPoint); + is(result.focusedElement, ce_RTL_elem, "Focused element should match expected."); + is(result.text, "איפה", "Selected text should match expected text."); + + result = yield do_promiseLongPressResult(tc_RTL_midPoint); + is(result.focusedElement, null, "No focused element is expected."); + is(result.text, "תן", "Selected text should match expected text."); + + result = yield do_promiseLongPressResult(i_RTL_midPoint); + is(result.focusedElement, i_RTL_elem, "Focused element should match expected."); + is(result.text, "לרוץ", "Selected text should match expected text."); + + result = yield do_promiseLongPressResult(ta_RTL_midPoint); + is(result.focusedElement, ta_RTL_elem, "Focused element should match expected."); + is(result.text, "הספר", "Selected text should match expected text."); + + ok(true, "Finished all tests."); +}); + +run_next_test();
--- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -4582,16 +4582,32 @@ pref("selectioncaret.enabled", false); // This will inflate size of selection caret frame when we checking if // user click on selection caret or not. In app units. pref("selectioncaret.inflatesize.threshold", 40); // Selection carets will fall-back to internal LongTap detector. pref("selectioncaret.detects.longtap", true); +// Selection carets do not affect caret visibility. +pref("selectioncaret.visibility.affectscaret", false); + +// Selection caret visibility does not observe composition +// selections generated by soft keyboard managers. +pref("selectioncaret.observes.compositions", false); + +// The Touch caret by default observes the b2g visibility rules, and +// not the extended Android visibility rules that allow for touchcaret +// display in empty editable fields, for example. +pref("touchcaret.extendedvisibility", false); + +// Desktop and b2g don't need to open or close the Android +// TextSelection (Actionbar) utility. +pref("caret.manages-android-actionbar", false); + // New implementation to unify touch-caret and selection-carets. pref("layout.accessiblecaret.enabled", false); // Timeout in milliseconds to hide the accessiblecaret under cursor mode while // no one touches it. Set the value to 0 to disable this feature. pref("layout.accessiblecaret.timeout_ms", 3000); // Wakelock is disabled by default.
--- a/toolkit/devtools/server/actors/script.js +++ b/toolkit/devtools/server/actors/script.js @@ -13,16 +13,18 @@ const { DebuggerServer } = require("devt const DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); const { dbg_assert, dumpn, update, fetch } = DevToolsUtils; const { dirname, joinURI } = require("devtools/toolkit/path"); const promise = require("promise"); const PromiseDebugging = require("PromiseDebugging"); const xpcInspector = require("xpcInspector"); const ScriptStore = require("./utils/ScriptStore"); +const { defer, resolve, reject, all } = require("devtools/toolkit/deprecated-sync-thenables"); + loader.lazyGetter(this, "Debugger", () => { let Debugger = require("Debugger"); hackDebugger(Debugger); return Debugger; }); loader.lazyRequireGetter(this, "SourceMapConsumer", "source-map", true); loader.lazyRequireGetter(this, "SourceMapGenerator", "source-map", true); loader.lazyRequireGetter(this, "CssLogic", "devtools/styleinspector/css-logic", true); @@ -759,17 +761,17 @@ ThreadActor.prototype = { return undefined; } packet.frame.where = { source: originalLocation.originalSourceActor.form(), line: originalLocation.originalLine, column: originalLocation.originalColumn }; - Promise.resolve(onPacket(packet)) + resolve(onPacket(packet)) .then(null, error => { reportError(error); return { error: "unknownError", message: error.message + "\n" + error.stack }; }) .then(packet => { @@ -934,18 +936,18 @@ ThreadActor.prototype = { * @param Object aRequest * The request packet received over the RDP. * @returns A promise that resolves to true once the hooks are attached, or is * rejected with an error packet. */ _handleResumeLimit: function (aRequest) { let steppingType = aRequest.resumeLimit.type; if (["break", "step", "next", "finish"].indexOf(steppingType) == -1) { - return Promise.reject({ error: "badParameterType", - message: "Unknown resumeLimit type" }); + return reject({ error: "badParameterType", + message: "Unknown resumeLimit type" }); } const generatedLocation = this.sources.getFrameLocation(this.youngestFrame); return this.sources.getOriginalLocation(generatedLocation) .then(originalLocation => { const { onEnterFrame, onPop, onStep } = this._makeSteppingHooks(originalLocation, steppingType); @@ -1048,17 +1050,17 @@ ThreadActor.prototype = { return this._forceCompletion(aRequest); } let resumeLimitHandled; if (aRequest && aRequest.resumeLimit) { resumeLimitHandled = this._handleResumeLimit(aRequest) } else { this._clearSteppingHooks(this.youngestFrame); - resumeLimitHandled = Promise.resolve(true); + resumeLimitHandled = resolve(true); } return resumeLimitHandled.then(() => { if (aRequest) { this._options.pauseOnExceptions = aRequest.pauseOnExceptions; this._options.ignoreCaughtExceptions = aRequest.ignoreCaughtExceptions; this.maybePauseOnExceptions(); this._maybeListenToEvents(aRequest); @@ -1310,17 +1312,17 @@ ThreadActor.prototype = { line: originalLocation.originalLine, column: originalLocation.originalColumn }; form.source = sourceForm; }); promises.push(promise); } - return Promise.all(promises).then(function () { + return all(promises).then(function () { return { frames: frames }; }); }, onReleaseMany: function (aRequest) { if (!aRequest.actors) { return { error: "missingParameter", message: "no actors were specified" }; @@ -1350,18 +1352,18 @@ ThreadActor.prototype = { const scripts = this.scripts.getAllScripts(); for (let i = 0, len = scripts.length; i < len; i++) { let s = scripts[i]; if (s.source) { sourcesToScripts.set(s.source, s); } } - return Promise.all([this.sources.createSourceActors(script.source) - for (script of sourcesToScripts.values())]); + return all([this.sources.createSourceActors(script.source) + for (script of sourcesToScripts.values())]); }, onSources: function (aRequest) { return this._discoverSources().then(() => { return { sources: [s.form() for (s of this.sources.iter())] }; }); @@ -2503,17 +2505,17 @@ SourceActor.prototype = { return offsets; }, /** * Handler for the "source" packet. */ onSource: function () { - return Promise.resolve(this._init) + return resolve(this._init) .then(this._getSourceText) .then(({ content, contentType }) => { return { from: this.actorID, source: this.threadActor.createValueGrip( content, this.threadActor.threadLifetimePool), contentType: contentType };
--- a/toolkit/mozapps/extensions/LightweightThemeManager.jsm +++ b/toolkit/mozapps/extensions/LightweightThemeManager.jsm @@ -297,17 +297,17 @@ this.LightweightThemeManager = { _notifyWindows(this.currentThemeForDisplay); }.bind(this)); } } if (aData) _prefs.setCharPref("selectedThemeID", aData.id); else - _prefs.deleteBranch("selectedThemeID"); + _prefs.setCharPref("selectedThemeID", ""); _notifyWindows(aData); Services.obs.notifyObservers(null, "lightweight-theme-changed", null); }, /** * Starts the Addons provider and enables the new lightweight theme if * necessary.
--- a/toolkit/mozapps/extensions/test/xpcshell/test_signed_inject.js +++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_inject.js @@ -44,16 +44,23 @@ function resetPrefs() { Services.prefs.setIntPref("bootstraptest.install_reason", -1); Services.prefs.setIntPref("bootstraptest.uninstall_reason", -1); Services.prefs.setIntPref("bootstraptest.startup_oldversion", -1); Services.prefs.setIntPref("bootstraptest.shutdown_newversion", -1); Services.prefs.setIntPref("bootstraptest.install_oldversion", -1); Services.prefs.setIntPref("bootstraptest.uninstall_newversion", -1); } +function clearCache(file) { + if (TEST_UNPACKED) + return; + + Services.obs.notifyObservers(file, "flush-cache-entry", null); +} + function getActiveVersion() { return Services.prefs.getIntPref("bootstraptest.active_version"); } function run_test() { createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4"); // Start and stop the manager to initialise everything in the profile before @@ -62,97 +69,101 @@ function run_test() { shutdownManager(); resetPrefs(); run_next_test(); } // Injecting into profile (bootstrap) add_task(function*() { - manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.unsigned), profileDir, ID); + let file = manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.unsigned), profileDir, ID); startupManager(); // Currently we leave the sideloaded add-on there but just don't run it let addon = yield promiseAddonByID(ID); do_check_neq(addon, null); do_check_true(addon.appDisabled); do_check_false(addon.isActive); do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_MISSING); do_check_eq(getActiveVersion(), -1); addon.uninstall(); yield promiseShutdownManager(); resetPrefs(); - do_check_false(getFileForAddon(profileDir, ID).exists()); + do_check_false(file.exists()); + clearCache(file); }); add_task(function*() { - manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.signed), profileDir, ID); - breakAddon(getFileForAddon(profileDir, ID)); + let file = manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.signed), profileDir, ID); + breakAddon(file); startupManager(); // Currently we leave the sideloaded add-on there but just don't run it let addon = yield promiseAddonByID(ID); do_check_neq(addon, null); do_check_true(addon.appDisabled); do_check_false(addon.isActive); do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN); do_check_eq(getActiveVersion(), -1); addon.uninstall(); yield promiseShutdownManager(); resetPrefs(); - do_check_false(getFileForAddon(profileDir, ID).exists()); + do_check_false(file.exists()); + clearCache(file); }); add_task(function*() { - manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.badid), profileDir, ID); + let file = manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.badid), profileDir, ID); startupManager(); // Currently we leave the sideloaded add-on there but just don't run it let addon = yield promiseAddonByID(ID); do_check_neq(addon, null); do_check_true(addon.appDisabled); do_check_false(addon.isActive); do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN); do_check_eq(getActiveVersion(), -1); addon.uninstall(); yield promiseShutdownManager(); resetPrefs(); - do_check_false(getFileForAddon(profileDir, ID).exists()); + do_check_false(file.exists()); + clearCache(file); }); // Installs a signed add-on then modifies it in place breaking its signing add_task(function*() { - manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.signed), profileDir, ID); + let file = manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.signed), profileDir, ID); // Make it appear to come from the past so when we modify it later it is // detected during startup. Obviously malware can bypass this method of // detection but the periodic scan will catch that - yield promiseSetExtensionModifiedTime(getFileForAddon(profileDir, ID).path, Date.now() - 600000); + yield promiseSetExtensionModifiedTime(file.path, Date.now() - 600000); startupManager(); let addon = yield promiseAddonByID(ID); do_check_neq(addon, null); do_check_false(addon.appDisabled); do_check_true(addon.isActive); do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_SIGNED); do_check_eq(getActiveVersion(), 2); yield promiseShutdownManager(); do_check_eq(getActiveVersion(), 0); - breakAddon(getFileForAddon(profileDir, ID)); + clearCache(file); + breakAddon(file); resetPrefs(); startupManager(); addon = yield promiseAddonByID(ID); do_check_neq(addon, null); do_check_true(addon.appDisabled); do_check_false(addon.isActive); @@ -162,101 +173,106 @@ add_task(function*() { let ids = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED); do_check_eq(ids.length, 1); do_check_eq(ids[0], ID); addon.uninstall(); yield promiseShutdownManager(); resetPrefs(); - do_check_false(getFileForAddon(profileDir, ID).exists()); + do_check_false(file.exists()); + clearCache(file); }); // Injecting into profile (non-bootstrap) add_task(function*() { - manuallyInstall(do_get_file(DATA + ADDONS.nonbootstrap.unsigned), profileDir, ID); + let file = manuallyInstall(do_get_file(DATA + ADDONS.nonbootstrap.unsigned), profileDir, ID); startupManager(); // Currently we leave the sideloaded add-on there but just don't run it let addon = yield promiseAddonByID(ID); do_check_neq(addon, null); do_check_true(addon.appDisabled); do_check_false(addon.isActive); do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_MISSING); do_check_false(isExtensionInAddonsList(profileDir, ID)); addon.uninstall(); yield promiseRestartManager(); yield promiseShutdownManager(); - do_check_false(getFileForAddon(profileDir, ID).exists()); + do_check_false(file.exists()); + clearCache(file); }); add_task(function*() { - manuallyInstall(do_get_file(DATA + ADDONS.nonbootstrap.signed), profileDir, ID); - breakAddon(getFileForAddon(profileDir, ID)); + let file = manuallyInstall(do_get_file(DATA + ADDONS.nonbootstrap.signed), profileDir, ID); + breakAddon(file); startupManager(); // Currently we leave the sideloaded add-on there but just don't run it let addon = yield promiseAddonByID(ID); do_check_neq(addon, null); do_check_true(addon.appDisabled); do_check_false(addon.isActive); do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN); do_check_false(isExtensionInAddonsList(profileDir, ID)); addon.uninstall(); yield promiseRestartManager(); yield promiseShutdownManager(); - do_check_false(getFileForAddon(profileDir, ID).exists()); + do_check_false(file.exists()); + clearCache(file); }); add_task(function*() { - manuallyInstall(do_get_file(DATA + ADDONS.nonbootstrap.badid), profileDir, ID); + let file = manuallyInstall(do_get_file(DATA + ADDONS.nonbootstrap.badid), profileDir, ID); startupManager(); // Currently we leave the sideloaded add-on there but just don't run it let addon = yield promiseAddonByID(ID); do_check_neq(addon, null); do_check_true(addon.appDisabled); do_check_false(addon.isActive); do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN); do_check_false(isExtensionInAddonsList(profileDir, ID)); addon.uninstall(); yield promiseRestartManager(); yield promiseShutdownManager(); - do_check_false(getFileForAddon(profileDir, ID).exists()); + do_check_false(file.exists()); + clearCache(file); }); // Installs a signed add-on then modifies it in place breaking its signing add_task(function*() { - manuallyInstall(do_get_file(DATA + ADDONS.nonbootstrap.signed), profileDir, ID); + let file = manuallyInstall(do_get_file(DATA + ADDONS.nonbootstrap.signed), profileDir, ID); // Make it appear to come from the past so when we modify it later it is // detected during startup. Obviously malware can bypass this method of // detection but the periodic scan will catch that - yield promiseSetExtensionModifiedTime(getFileForAddon(profileDir, ID).path, Date.now() - 60000); + yield promiseSetExtensionModifiedTime(file.path, Date.now() - 60000); startupManager(); let addon = yield promiseAddonByID(ID); do_check_neq(addon, null); do_check_false(addon.appDisabled); do_check_true(addon.isActive); do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_SIGNED); do_check_true(isExtensionInAddonsList(profileDir, ID)); yield promiseShutdownManager(); - breakAddon(getFileForAddon(profileDir, ID)); + clearCache(file); + breakAddon(file); startupManager(); addon = yield promiseAddonByID(ID); do_check_neq(addon, null); do_check_true(addon.appDisabled); do_check_false(addon.isActive); do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN); @@ -265,17 +281,18 @@ add_task(function*() { let ids = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED); do_check_eq(ids.length, 1); do_check_eq(ids[0], ID); addon.uninstall(); yield promiseRestartManager(); yield promiseShutdownManager(); - do_check_false(getFileForAddon(profileDir, ID).exists()); + do_check_false(file.exists()); + clearCache(file); }); // Stage install then modify before startup (non-bootstrap) add_task(function*() { startupManager(); yield promiseInstallAllFiles([do_get_file(DATA + ADDONS.nonbootstrap.signed)]); yield promiseShutdownManager(); @@ -286,18 +303,17 @@ add_task(function*() { breakAddon(staged); startupManager(); // Should have refused to install the broken staged version let addon = yield promiseAddonByID(ID); do_check_eq(addon, null); - let install = getFileForAddon(profileDir, ID); - do_check_false(install.exists()); + clearCache(staged); yield promiseShutdownManager(); }); // Manufacture staged install (bootstrap) add_task(function*() { let stage = profileDir.clone(); stage.append("staged"); @@ -307,14 +323,14 @@ add_task(function*() { startupManager(); // Should have refused to install the broken staged version let addon = yield promiseAddonByID(ID); do_check_eq(addon, null); do_check_eq(getActiveVersion(), -1); - let install = getFileForAddon(profileDir, ID); - do_check_false(install.exists()); + do_check_false(file.exists()); + clearCache(file); yield promiseShutdownManager(); resetPrefs(); });
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini +++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini @@ -233,17 +233,16 @@ skip-if = os == "android" [test_pluginBlocklistCtp.js] # Bug 676992: test consistently fails on Android fail-if = buildapp == "mulet" || os == "android" [test_pref_properties.js] [test_registry.js] [test_safemode.js] [test_signed_verify.js] [test_signed_inject.js] -skip-if = true [test_signed_install.js] run-sequentially = Uses hardcoded ports in xpi files. [test_signed_migrate.js] [test_startup.js] # Bug 676992: test consistently fails on Android fail-if = os == "android" [test_syncGUID.js] [test_strictcompatibility.js]