Bug 1272222: Use larger icons for browser actions in the menu panel. r?Gijs draft
authorKris Maglione <maglione.k@gmail.com>
Wed, 13 Jul 2016 15:16:00 -0700
changeset 387850 669b54dfd3cfb4ab49f7b271410f3206e25b54de
parent 386866 56d636b5d961f3accab65064e7f81be1f78ad81e
child 525464 be618d52e065c7bde39f5eff7286fc6266da3093
push id23085
push usermaglione.k@gmail.com
push dateThu, 14 Jul 2016 22:29:21 +0000
reviewersGijs
bugs1272222
milestone50.0a1
Bug 1272222: Use larger icons for browser actions in the menu panel. r?Gijs MozReview-Commit-ID: 26lmlcrngPk
browser/base/content/browser.css
browser/components/extensions/ext-browserAction.js
browser/components/extensions/ext-contextMenus.js
browser/components/extensions/ext-pageAction.js
browser/components/extensions/test/browser/browser_ext_browserAction_context.js
browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
browser/components/extensions/test/browser/browser_ext_pageAction_context.js
browser/components/extensions/test/browser/head.js
mobile/android/components/extensions/ext-pageAction.js
toolkit/components/extensions/ExtensionUtils.jsm
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -322,16 +322,46 @@ toolbarpaletteitem > toolbaritem[sdkstyl
 toolbarpaletteitem:-moz-any([place="palette"], [place="panel"]) > toolbaritem[sdkstylewidget="true"] > toolbarbutton {
   display: -moz-box;
 }
 
 toolbarpaletteitem > toolbaritem[sdkstylewidget="true"][cui-areatype="toolbar"] > .toolbarbutton-text {
   display: -moz-box;
 }
 
+@media not all and (min-resolution: 1.1dppx) {
+  .webextension-browser-action {
+    list-style-image: var(--webextension-toolbar-image, none);
+  }
+
+  .webextension-browser-action[cui-areatype="menu-panel"],
+  toolbarpaletteitem[place="palette"] > .webextension-browser-action {
+    list-style-image: var(--webextension-menupanel-image, none);
+  }
+
+  .webextension-page-action {
+    list-style-image: var(--webextension-urlbar-image, none);
+  }
+}
+
+@media (min-resolution: 1.1dppx) {
+  .webextension-browser-action {
+    list-style-image: var(--webextension-toolbar-image-2x, none);
+  }
+
+  .webextension-browser-action[cui-areatype="menu-panel"],
+  toolbarpaletteitem[place="palette"] > .webextension-browser-action {
+    list-style-image: var(--webextension-menupanel-image-2x, none);
+  }
+
+  .webextension-page-action {
+    list-style-image: var(--webextension-urlbar-image-2x, none);
+  }
+}
+
 toolbarpaletteitem[removable="false"] {
   opacity: 0.5;
   cursor: default;
 }
 
 %ifndef XP_MACOSX
 toolbarpaletteitem[place="palette"],
 toolbarpaletteitem[place="panel"],
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -74,16 +74,17 @@ BrowserAction.prototype = {
         let view = document.getElementById(this.viewId);
         if (view) {
           view.remove();
         }
       },
 
       onCreated: node => {
         node.classList.add("badged-button");
+        node.classList.add("webextension-browser-action");
         node.setAttribute("constrain-size", "true");
 
         this.updateButton(node, this.defaults);
       },
 
       onViewShowing: event => {
         let document = event.target.ownerDocument;
         let tabbrowser = document.defaultView.gBrowser;
@@ -145,32 +146,44 @@ BrowserAction.prototype = {
         color = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
       }
       badgeNode.style.backgroundColor = color || "";
     }
 
     const LEGACY_CLASS = "toolbarbutton-legacy-addon";
     node.classList.remove(LEGACY_CLASS);
 
-
-    let win = node.ownerDocument.defaultView;
-    let {icon, size} = IconDetails.getURL(tabData.icon, win, this.extension);
+    let baseSize = 16;
+    let {icon, size} = IconDetails.getPreferredIcon(tabData.icon, this.extension, baseSize);
 
     // If the best available icon size is not divisible by 16, check if we have
     // an 18px icon to fall back to, and trim off the padding instead.
     if (size % 16 && !icon.endsWith(".svg")) {
-      let result = IconDetails.getURL(tabData.icon, win, this.extension, 18);
+      let result = IconDetails.getPreferredIcon(tabData.icon, this.extension, 18);
 
       if (result.size % 18 == 0) {
+        baseSize = 18;
         icon = result.icon;
         node.classList.add(LEGACY_CLASS);
       }
     }
 
-    node.setAttribute("image", icon);
+    // These URLs should already be properly escaped, but make doubly sure CSS
+    // string escape characters are escaped here, since they could lead to a
+    // sandbox break.
+    let escape = str => str.replace(/[\\\s"]/g, encodeURIComponent);
+
+    let getIcon = size => escape(IconDetails.getPreferredIcon(tabData.icon, this.extension, size).icon);
+
+    node.setAttribute("style", `
+      --webextension-menupanel-image: url("${getIcon(32)}");
+      --webextension-menupanel-image-2x: url("${getIcon(64)}");
+      --webextension-toolbar-image: url("${escape(icon)}");
+      --webextension-toolbar-image-2x: url("${getIcon(baseSize * 2)}");
+    `);
   },
 
   // Update the toolbar button for a given window.
   updateWindow(window) {
     let widget = this.widget.forWindow(window);
     if (widget) {
       let tab = window.gBrowser.selectedTab;
       this.updateButton(widget.node, this.tabContext.get(tab));
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -49,17 +49,18 @@ var gMenuBuilder = {
       rootElement.setAttribute("ext-type", "top-level-menu");
       rootElement = this.removeTopLevelMenuIfNeeded(rootElement);
 
       // Display the extension icon on the root element.
       if (root.extension.manifest.icons) {
         let parentWindow = contextData.menu.ownerDocument.defaultView;
         let extension = root.extension;
 
-        let {icon} = IconDetails.getURL(extension.manifest.icons, parentWindow, extension, 16 /* size */);
+        let {icon} = IconDetails.getPreferredIcon(extension.manifest.icons, extension,
+                                                  16 * parentWindow.devicePixelRatio);
         let resolvedURL = root.extension.baseURI.resolve(icon);
 
         if (rootElement.localName == "menu") {
           rootElement.setAttribute("class", "menu-iconic");
         } else if (rootElement.localName == "menuitem") {
           rootElement.setAttribute("class", "menuitem-iconic");
         }
         rootElement.setAttribute("image", resolvedURL);
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -87,31 +87,40 @@ PageAction.prototype = {
 
     if (tabData.show) {
       // Update the title and icon only if the button is visible.
 
       let title = tabData.title || this.extension.name;
       button.setAttribute("tooltiptext", title);
       button.setAttribute("aria-label", title);
 
-      let {icon} = IconDetails.getURL(tabData.icon, window, this.extension);
-      button.setAttribute("src", icon);
+      // These URLs should already be properly escaped, but make doubly sure CSS
+      // string escape characters are escaped here, since they could lead to a
+      // sandbox break.
+      let escape = str => str.replace(/[\\\s"]/g, encodeURIComponent);
+
+      let getIcon = size => escape(IconDetails.getPreferredIcon(tabData.icon, this.extension, size).icon);
+
+      button.setAttribute("style", `
+        --webextension-urlbar-image: url("${getIcon(16)}");
+        --webextension-urlbar-image-2x: url("${getIcon(32)}");
+      `);
     }
 
     button.hidden = !tabData.show;
   },
 
   // Create an |image| node and add it to the |urlbar-icons|
   // container in the given window.
   addButton(window) {
     let document = window.document;
 
     let button = document.createElement("image");
     button.id = this.id;
-    button.setAttribute("class", "urlbar-icon");
+    button.setAttribute("class", "urlbar-icon webextension-page-action");
 
     button.addEventListener("click", event => { // eslint-disable-line mozilla/balanced-listeners
       if (event.button == 0) {
         this.handleClick(window);
       }
     });
 
     document.getElementById("urlbar-icons").appendChild(button);
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -92,17 +92,17 @@ function* runTests(options) {
 
   function checkDetails(details) {
     let button = document.getElementById(browserActionId);
 
     ok(button, "button exists");
 
     let title = details.title || options.manifest.name;
 
-    is(button.getAttribute("image"), details.icon, "icon URL is correct");
+    is(getListStyleImage(button), details.icon, "icon URL is correct");
     is(button.getAttribute("tooltiptext"), title, "image title is correct");
     is(button.getAttribute("label"), title, "image label is correct");
     is(button.getAttribute("badge"), details.badge, "badge text is correct");
     is(button.getAttribute("disabled") == "true", Boolean(details.disabled), "disabled state is correct");
 
     if (details.badge && details.badgeBackgroundColor) {
       let badge = button.ownerDocument.getAnonymousElementByAttribute(
         button, "class", "toolbarbutton-badge");
@@ -158,16 +158,21 @@ add_task(function* testTabSwitchContext(
           "description": "Popup",
         },
 
         "title": {
           "message": "Title",
           "description": "Title",
         },
       },
+
+      "default.png": imageBuffer,
+      "default-2.png": imageBuffer,
+      "1.png": imageBuffer,
+      "2.png": imageBuffer,
     },
 
     getTests(tabs, expectDefaults) {
       let details = [
         {"icon": browser.runtime.getURL("default.png"),
          "popup": browser.runtime.getURL("default.html"),
          "title": "Default Title",
          "badge": "",
@@ -317,16 +322,20 @@ add_task(function* testDefaultTitle() {
 
       "browser_action": {
         "default_icon": "icon.png",
       },
 
       "permissions": ["tabs"],
     },
 
+    files: {
+      "icon.png": imageBuffer,
+    },
+
     getTests(tabs, expectDefaults) {
       let details = [
         {"title": "Foo Extension",
          "popup": "",
          "badge": "",
          "badgeBackgroundColor": null,
          "icon": browser.runtime.getURL("icon.png")},
         {"title": "Foo Title",
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
@@ -47,16 +47,22 @@ add_task(function* testDetailsObjects() 
         resolutions: {
           "1": browser.runtime.getURL("data/a.png"),
           "2": browser.runtime.getURL("data/a.png")}},
       {details: {"path": {"19": "a.png", "38": "a-x2.png"}},
         resolutions: {
           "1": browser.runtime.getURL("data/a.png"),
           "2": browser.runtime.getURL("data/a-x2.png")}},
 
+      // Test that CSS strings are escaped properly.
+      {details: {"path": 'a.png#" \\'},
+        resolutions: {
+          "1": browser.runtime.getURL("data/a.png#%22%20%5C"),
+          "2": browser.runtime.getURL("data/a.png#%22%20%5C")}},
+
       // Only ImageData objects.
       {details: {"imageData": imageData.red.imageData},
         resolutions: {
           "1": imageData.red.url,
           "2": imageData.red.url}},
       {details: {"imageData": {"19": imageData.red.imageData}},
         resolutions: {
           "1": imageData.red.url,
@@ -131,26 +137,33 @@ add_task(function* testDetailsObjects() 
         "6": "6.png",
         "18": "18.png",
         "36": "36.png",
         "48": "48.png",
         "128": "128.png"}},
         legacy: true,
         resolutions: {
           "1": browser.runtime.getURL("data/18.png"),
-          "2": browser.runtime.getURL("data/36.png")}},
+          "2": browser.runtime.getURL("data/36.png")},
+        menuResolutions: {
+          "1": browser.runtime.getURL("data/36.png"),
+          "2": browser.runtime.getURL("data/128.png")}},
       {details: {"path": {
         "16": "16.png",
         "18": "18.png",
         "32": "32.png",
         "48": "48.png",
+        "64": "64.png",
         "128": "128.png"}},
         resolutions: {
           "1": browser.runtime.getURL("data/16.png"),
-          "2": browser.runtime.getURL("data/32.png")}},
+          "2": browser.runtime.getURL("data/32.png")},
+        menuResolutions: {
+          "1": browser.runtime.getURL("data/32.png"),
+          "2": browser.runtime.getURL("data/64.png")}},
       {details: {"path": {
         "18": "18.png",
         "32": "32.png",
         "48": "48.png",
         "128": "128.png"}},
         resolutions: {
           "1": browser.runtime.getURL("data/32.png"),
           "2": browser.runtime.getURL("data/32.png")}},
@@ -162,42 +175,44 @@ add_task(function* testDetailsObjects() 
     let tabId;
 
     browser.test.onMessage.addListener((msg, test) => {
       if (msg != "setIcon") {
         browser.test.fail("expecting 'setIcon' message");
       }
 
       let details = iconDetails[test.index];
-      let expectedURL = details.resolutions[test.resolution];
 
       let detailString = JSON.stringify(details);
-      browser.test.log(`Setting browerAction/pageAction to ${detailString} expecting URL ${expectedURL}`);
+      browser.test.log(`Setting browerAction/pageAction to ${detailString} expecting URLs ${JSON.stringify(details.resolutions)}`);
 
       browser.browserAction.setIcon(Object.assign({tabId}, details.details));
       browser.pageAction.setIcon(Object.assign({tabId}, details.details));
 
-      browser.test.sendMessage("imageURL", [expectedURL, !!details.legacy]);
+      browser.test.sendMessage("iconSet");
     });
 
     // Generate a list of tests and resolutions to send back to the test
     // context.
     //
     // This process is a bit convoluted, because the outer test context needs
     // to handle checking the button nodes and changing the screen resolution,
     // but it can't pass us icon definitions with ImageData objects. This
     // shouldn't be a problem, since structured clones should handle ImageData
     // objects without issue. Unfortunately, |cloneInto| implements a slightly
     // different algorithm than we use in web APIs, and does not handle them
     // correctly.
     let tests = [];
     for (let [idx, icon] of iconDetails.entries()) {
-      for (let res of Object.keys(icon.resolutions)) {
-        tests.push({index: idx, resolution: Number(res)});
-      }
+      tests.push({
+        index: idx,
+        legacy: !!icon.legacy,
+        menuResolutions: icon.menuResolutions,
+        resolutions: icon.resolutions,
+      });
     }
 
     // Sort by resolution, so we don't needlessly switch back and forth
     // between each test.
     tests.sort(test => test.resolution);
 
     browser.tabs.query({active: true, currentWindow: true}, tabs => {
       tabId = tabs[0].id;
@@ -214,45 +229,96 @@ add_task(function* testDetailsObjects() 
       "background": {
         "page": "data/background.html",
       }
     },
 
     files: {
       "data/background.html": `<script src="background.js"></script>`,
       "data/background.js": background,
+
+      "data/16.svg": imageBuffer,
+      "data/18.svg": imageBuffer,
+
+      "data/16.png": imageBuffer,
+      "data/18.png": imageBuffer,
+      "data/32.png": imageBuffer,
+      "data/36.png": imageBuffer,
+      "data/48.png": imageBuffer,
+      "data/64.png": imageBuffer,
+      "data/128.png": imageBuffer,
+
+      "a.png": imageBuffer,
+      "data/2.png": imageBuffer,
+      "data/100.png": imageBuffer,
+      "data/a.png": imageBuffer,
+      "data/a-x2.png": imageBuffer,
     },
   });
 
   const RESOLUTION_PREF = "layout.css.devPixelsPerPx";
-  registerCleanupFunction(() => {
-    SpecialPowers.clearUserPref(RESOLUTION_PREF);
-  });
+
+  let pageActionId = makeWidgetId(extension.id) + "-page-action";
+  let browserActionWidget = getBrowserActionWidget(extension);
 
-  let browserActionId = makeWidgetId(extension.id) + "-browser-action";
-  let pageActionId = makeWidgetId(extension.id) + "-page-action";
 
-  let [, tests] = yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+  yield extension.startup();
+  let tests = yield extension.awaitMessage("ready");
 
   for (let test of tests) {
-    SpecialPowers.setCharPref(RESOLUTION_PREF, String(test.resolution));
-    is(window.devicePixelRatio, test.resolution, "window has the required resolution");
+    extension.sendMessage("setIcon", test);
+    yield extension.awaitMessage("iconSet");
+
+    let browserActionButton = browserActionWidget.forWindow(window).node;
+    let pageActionImage = document.getElementById(pageActionId);
+
+
+    // Test icon sizes in the toolbar/urlbar.
+    for (let resolution of Object.keys(test.resolutions)) {
+      yield SpecialPowers.pushPrefEnv({set: [[RESOLUTION_PREF, resolution]]});
 
-    extension.sendMessage("setIcon", test);
+      is(window.devicePixelRatio, +resolution, "window has the required resolution");
+
+      let imageURL = test.resolutions[resolution];
+      is(getListStyleImage(browserActionButton), imageURL, `browser action has the correct image at ${resolution}x resolution`);
+      is(getListStyleImage(pageActionImage), imageURL, `page action has the correct image at ${resolution}x resolution`);
 
-    let [imageURL, legacy] = yield extension.awaitMessage("imageURL");
+      let isLegacy = browserActionButton.classList.contains("toolbarbutton-legacy-addon");
+      is(isLegacy, test.legacy, "Legacy class should be present?");
+
+      yield SpecialPowers.popPrefEnv();
+    }
 
-    let browserActionButton = document.getElementById(browserActionId);
-    is(browserActionButton.getAttribute("image"), imageURL, "browser action has the correct image");
+    if (!test.menuResolutions) {
+      continue;
+    }
+
+
+    // Test icon sizes in the menu panel.
+    CustomizableUI.addWidgetToArea(browserActionWidget.id,
+                                   CustomizableUI.AREA_PANEL);
+
+    yield showBrowserAction(extension);
+    browserActionButton = browserActionWidget.forWindow(window).node;
 
-    let isLegacy = browserActionButton.classList.contains("toolbarbutton-legacy-addon");
-    is(isLegacy, legacy, "Legacy class should be present?");
+    for (let resolution of Object.keys(test.menuResolutions)) {
+      yield SpecialPowers.pushPrefEnv({set: [[RESOLUTION_PREF, resolution]]});
+
+      is(window.devicePixelRatio, +resolution, "window has the required resolution");
+
+      let imageURL = test.menuResolutions[resolution];
+      is(getListStyleImage(browserActionButton), imageURL, `browser action has the correct menu image at ${resolution}x resolution`);
 
-    let pageActionImage = document.getElementById(pageActionId);
-    is(pageActionImage.src, imageURL, "page action has the correct image");
+      yield SpecialPowers.popPrefEnv();
+    }
+
+    yield closeBrowserAction(extension);
+
+    CustomizableUI.addWidgetToArea(browserActionWidget.id,
+                                   CustomizableUI.AREA_NAVBAR);
   }
 
   yield extension.unload();
 });
 
 // Test that an error is thrown when providing invalid icon sizes
 add_task(function* testInvalidIconSizes() {
   let extension = ExtensionTestUtils.loadExtension({
@@ -336,31 +402,36 @@ add_task(function* testDefaultDetails() 
       background: function() {
         browser.tabs.query({active: true, currentWindow: true}, tabs => {
           let tabId = tabs[0].id;
 
           browser.pageAction.show(tabId).then(() => {
             browser.test.sendMessage("ready");
           });
         });
-      }
+      },
+
+      files: {
+        "foo/bar.png": imageBuffer,
+        "baz/quux.png": imageBuffer,
+      },
     });
 
     yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
 
     let browserActionId = makeWidgetId(extension.id) + "-browser-action";
     let pageActionId = makeWidgetId(extension.id) + "-page-action";
 
     let browserActionButton = document.getElementById(browserActionId);
-    let image = browserActionButton.getAttribute("image");
+    let image = getListStyleImage(browserActionButton);
 
     ok(expectedURL.test(image), `browser action image ${image} matches ${expectedURL}`);
 
     let pageActionImage = document.getElementById(pageActionId);
-    image = pageActionImage.src;
+    image = getListStyleImage(pageActionImage);
 
     ok(expectedURL.test(image), `page action image ${image} matches ${expectedURL}`);
 
     yield extension.unload();
 
     let node = document.getElementById(pageActionId);
     is(node, null, "pageAction image removed from document");
   }
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
@@ -93,17 +93,17 @@ function* runTests(options) {
 
   function checkDetails(details) {
     let image = currentWindow.document.getElementById(pageActionId);
     if (details == null) {
       ok(image == null || image.hidden, "image is hidden");
     } else {
       ok(image, "image exists");
 
-      is(image.src, details.icon, "icon URL is correct");
+      is(getListStyleImage(image), details.icon, "icon URL is correct");
 
       let title = details.title || options.manifest.name;
       is(image.getAttribute("tooltiptext"), title, "image title is correct");
       is(image.getAttribute("aria-label"), title, "image aria-label is correct");
       // TODO: Popup URL.
     }
   }
 
@@ -172,16 +172,20 @@ add_task(function* testTabSwitchContext(
           "description": "Popup",
         },
 
         "title": {
           "message": "Title",
           "description": "Title",
         },
       },
+
+      "default.png": imageBuffer,
+      "1.png": imageBuffer,
+      "2.png": imageBuffer,
     },
 
     getTests(tabs) {
       let details = [
         {"icon": browser.runtime.getURL("default.png"),
          "popup": browser.runtime.getURL("default.html"),
          "title": "Default Title \u263a"},
         {"icon": browser.runtime.getURL("1.png"),
@@ -304,16 +308,20 @@ add_task(function* testDefaultTitle() {
 
       "page_action": {
         "default_icon": "icon.png",
       },
 
       "permissions": ["tabs"],
     },
 
+    files: {
+      "icon.png": imageBuffer,
+    },
+
     getTests(tabs) {
       let details = [
         {"title": "Foo Extension",
          "popup": "",
          "icon": browser.runtime.getURL("icon.png")},
         {"title": "Foo Title",
          "popup": "",
          "icon": browser.runtime.getURL("icon.png")},
--- a/browser/components/extensions/test/browser/head.js
+++ b/browser/components/extensions/test/browser/head.js
@@ -5,16 +5,17 @@
 /* exported CustomizableUI makeWidgetId focusWindow forceGC
  *          getBrowserActionWidget
  *          clickBrowserAction clickPageAction
  *          getBrowserActionPopup getPageActionPopup
  *          closeBrowserAction closePageAction
  *          promisePopupShown promisePopupHidden
  *          openContextMenu closeContextMenu
  *          openExtensionContextMenu closeExtensionContextMenu
+ *          imageBuffer getListStyleImage
  */
 
 var {AppConstants} = Cu.import("resource://gre/modules/AppConstants.jsm");
 var {CustomizableUI} = Cu.import("resource:///modules/CustomizableUI.jsm");
 
 // Bug 1239884: Our tests occasionally hit a long GC pause at unpredictable
 // times in debug builds, which results in intermittent timeouts. Until we have
 // a better solution, we force a GC after certain strategic tests, which tend to
@@ -42,16 +43,27 @@ var focusWindow = Task.async(function* f
       resolve();
     }, true);
   });
 
   win.focus();
   yield promise;
 });
 
+let img = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg==";
+var imageBuffer = Uint8Array.from(atob(img), byte => byte.charCodeAt(0)).buffer;
+
+function getListStyleImage(button) {
+  let style = button.ownerDocument.defaultView.getComputedStyle(button);
+
+  let match = /^url\("(.*)"\)$/.exec(style.listStyleImage);
+
+  return match && match[1];
+}
+
 function promisePopupShown(popup) {
   return new Promise(resolve => {
     if (popup.state == "open") {
       resolve();
     } else {
       let onPopupShown = event => {
         popup.removeEventListener("popupshown", onPopupShown);
         resolve();
@@ -79,25 +91,31 @@ function getBrowserActionPopup(extension
   let group = getBrowserActionWidget(extension);
 
   if (group.areaType == CustomizableUI.TYPE_TOOLBAR) {
     return win.document.getElementById("customizationui-widget-panel");
   }
   return win.PanelUI.panel;
 }
 
-var clickBrowserAction = Task.async(function* (extension, win = window) {
+var showBrowserAction = Task.async(function* (extension, win = window) {
   let group = getBrowserActionWidget(extension);
   let widget = group.forWindow(win);
 
   if (group.areaType == CustomizableUI.TYPE_TOOLBAR) {
     ok(!widget.overflowed, "Expect widget not to be overflowed");
   } else if (group.areaType == CustomizableUI.TYPE_MENU_PANEL) {
     yield win.PanelUI.show();
   }
+});
+
+var clickBrowserAction = Task.async(function* (extension, win = window) {
+  yield showBrowserAction(extension, win);
+
+  let widget = getBrowserActionWidget(extension).forWindow(win);
 
   EventUtils.synthesizeMouseAtCenter(widget.node, {}, win);
 });
 
 function closeBrowserAction(extension, win = window) {
   let group = getBrowserActionWidget(extension);
 
   let node = win.document.getElementById(group.viewId);
--- a/mobile/android/components/extensions/ext-pageAction.js
+++ b/mobile/android/components/extensions/ext-pageAction.js
@@ -59,17 +59,18 @@ PageAction.prototype = {
 
     if (this.options.icon) {
       this.id = PageActions.add(this.options);
       return Promise.resolve();
     }
 
     this.shouldShow = true;
 
-    let {icon} = IconDetails.getURL(this.icons, context.contentWindow, this.extension, 18);
+    let {icon} = IconDetails.getPreferredIcon(this.icons, this.extension,
+                                              18 * context.contentWindow.devicePixelRatio);
 
     let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
     return IconDetails.convertImageURLToDataURL(icon, context, browserWindow).then(dataURI => {
       if (this.shouldShow) {
         this.options.icon = dataURI;
         this.id = PageActions.add(this.options);
       }
     }).catch(() => {
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -469,21 +469,19 @@ let IconDetails = {
       extension.manifestError(`Invalid icon data: ${e}`);
     }
 
     return result;
   },
 
   // Returns the appropriate icon URL for the given icons object and the
   // screen resolution of the given window.
-  getURL(icons, window, extension, size = 16) {
+  getPreferredIcon(icons, extension = null, size = 16) {
     const DEFAULT = "chrome://browser/content/extension.svg";
 
-    size *= window.devicePixelRatio;
-
     let bestSize = null;
     if (icons[size]) {
       bestSize = size;
     } else if (icons[2 * size]) {
       bestSize = 2 * size;
     } else {
       let sizes = Object.keys(icons)
                         .map(key => parseInt(key, 10))