Bug 1270742 - Add support for default_icon in chrome.pageAction r?kmag try: -b o -p android-x86,android-api-15 -u mochitest-1,mochitest-e10s draft
authorMatthew Wein <mwein@mozilla.com>
Mon, 23 May 2016 15:59:33 -0700
changeset 382602 fcfb750c626f68df76bb8dac20eda5e13ede1236
parent 382601 94763b7450fd1dae8cae6135c1ffbe4cebe07a1e
child 524246 998409e1c8b8d8a863c382131cc3b86a62faa33f
push id21778
push usermwein@mozilla.com
push dateWed, 29 Jun 2016 23:53:34 +0000
reviewerskmag
bugs1270742
milestone50.0a1
Bug 1270742 - Add support for default_icon in chrome.pageAction r?kmag try: -b o -p android-x86,android-api-15 -u mochitest-1,mochitest-e10s MozReview-Commit-ID: D9uR0JUXJwx
browser/components/extensions/.eslintrc
browser/components/extensions/ext-browserAction.js
browser/components/extensions/ext-pageAction.js
browser/components/extensions/ext-utils.js
browser/components/extensions/schemas/page_action.json
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/browser_ext_pageAction_popup.js
browser/components/extensions/test/browser/browser_ext_pageAction_popup_resize.js
browser/components/extensions/test/browser/browser_ext_pageAction_simple.js
browser/components/extensions/test/browser/browser_ext_popup_api_injection.js
browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js
browser/components/extensions/test/browser/browser_ext_tabs_executeScript_good.js
mobile/android/components/extensions/ext-pageAction.js
mobile/android/components/extensions/schemas/page_action.json
mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html
mobile/android/components/extensions/test/mochitest/test_ext_pageAction_popup.html
toolkit/components/extensions/ExtensionUtils.jsm
toolkit/components/extensions/Schemas.jsm
--- a/browser/components/extensions/.eslintrc
+++ b/browser/components/extensions/.eslintrc
@@ -1,18 +1,18 @@
 {
   "extends": "../../../toolkit/components/extensions/.eslintrc",
 
   "globals": {
     "AllWindowEvents": true,
     "currentWindow": true,
     "EventEmitter": true,
-    "IconDetails": true,
     "makeWidgetId": true,
     "pageActionFor": true,
+    "IconDetails": true,
     "PanelPopup": true,
     "TabContext": true,
     "ViewPopup": true,
     "WindowEventManager": true,
     "WindowListManager": true,
     "WindowManager": true,
   },
 }
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -5,16 +5,17 @@
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
                                   "resource:///modules/CustomizableUI.jsm");
 
 Cu.import("resource://devtools/shared/event-emitter.js");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   EventManager,
+  IconDetails,
 } = ExtensionUtils;
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 // WeakMap[Extension -> BrowserAction]
 var browserActionMap = new WeakMap();
 
 // Responsible for the browser_action section of the manifest as well
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -1,16 +1,17 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   EventManager,
+  IconDetails,
 } = ExtensionUtils;
 
 // WeakMap[Extension -> PageAction]
 var pageActionMap = new WeakMap();
 
 // Handles URL bar icons, including the |page_action| manifest entry
 // and associated API.
 function PageAction(options, extension) {
@@ -218,21 +219,23 @@ extensions.registerSchemaAPI("pageAction
         return () => {
           pageAction.off("click", listener);
         };
       }).api(),
 
       show(tabId) {
         let tab = TabManager.getTab(tabId);
         PageAction.for(extension).setProperty(tab, "show", true);
+        return Promise.resolve();
       },
 
       hide(tabId) {
         let tab = TabManager.getTab(tabId);
         PageAction.for(extension).setProperty(tab, "show", false);
+        return Promise.resolve();
       },
 
       setTitle(details) {
         let tab = TabManager.getTab(details.tabId);
 
         // Clear the tab-specific title when given a null string.
         PageAction.for(extension).setProperty(tab, "title", details.title || null);
       },
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -3,148 +3,31 @@
 "use strict";
 
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
                                   "resource:///modules/CustomizableUI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/AppConstants.jsm");
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
-const INTEGER = /^[1-9]\d*$/;
+
 // Minimum time between two resizes.
 const RESIZE_TIMEOUT = 100;
 
 var {
   EventManager,
-  instanceOf,
 } = ExtensionUtils;
 
 // This file provides some useful code for the |tabs| and |windows|
 // modules. All of the code is installed on |global|, which is a scope
 // shared among the different ext-*.js scripts.
 
-
-// Manages icon details for toolbar buttons in the |pageAction| and
-// |browserAction| APIs.
-global.IconDetails = {
-  // Normalizes the various acceptable input formats into an object
-  // with icon size as key and icon URL as value.
-  //
-  // If a context is specified (function is called from an extension):
-  // Throws an error if an invalid icon size was provided or the
-  // extension is not allowed to load the specified resources.
-  //
-  // If no context is specified, instead of throwing an error, this
-  // function simply logs a warning message.
-  normalize(details, extension, context = null) {
-    let result = {};
-
-    try {
-      if (details.imageData) {
-        let imageData = details.imageData;
-
-        // The global might actually be from Schema.jsm, which
-        // normalizes most of our arguments. In that case it won't have
-        // an ImageData property. But Schema.jsm doesn't normalize
-        // actual ImageData objects, so they will come from a global
-        // with the right property.
-        if (instanceOf(imageData, "ImageData")) {
-          imageData = {"19": imageData};
-        }
-
-        for (let size of Object.keys(imageData)) {
-          if (!INTEGER.test(size)) {
-            throw new Error(`Invalid icon size ${size}, must be an integer`);
-          }
-          result[size] = this.convertImageDataToPNG(imageData[size], context);
-        }
-      }
-
-      if (details.path) {
-        let path = details.path;
-        if (typeof path != "object") {
-          path = {"19": path};
-        }
-
-        let baseURI = context ? context.uri : extension.baseURI;
-
-        for (let size of Object.keys(path)) {
-          if (!INTEGER.test(size)) {
-            throw new Error(`Invalid icon size ${size}, must be an integer`);
-          }
-
-          let url = baseURI.resolve(path[size]);
-
-          // The Chrome documentation specifies these parameters as
-          // relative paths. We currently accept absolute URLs as well,
-          // which means we need to check that the extension is allowed
-          // to load them. This will throw an error if it's not allowed.
-          Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
-            extension.principal, url,
-            Services.scriptSecurityManager.DISALLOW_SCRIPT);
-
-          result[size] = url;
-        }
-      }
-    } catch (e) {
-      // Function is called from extension code, delegate error.
-      if (context) {
-        throw e;
-      }
-      // If there's no context, it's because we're handling this
-      // as a manifest directive. Log a warning rather than
-      // raising an error.
-      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) {
-    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))
-                        .sort((a, b) => a - b);
-
-      bestSize = sizes.find(candidate => candidate > size) || sizes.pop();
-    }
-
-    if (bestSize) {
-      return {size: bestSize, icon: icons[bestSize]};
-    }
-
-    return {size, icon: DEFAULT};
-  },
-
-  convertImageDataToPNG(imageData, context) {
-    let document = context.contentWindow.document;
-    let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
-    canvas.width = imageData.width;
-    canvas.height = imageData.height;
-    canvas.getContext("2d").putImageData(imageData, 0, 0);
-
-    return canvas.toDataURL("image/png");
-  },
-};
-
 global.makeWidgetId = id => {
   id = id.toLowerCase();
   // FIXME: This allows for collisions.
   return id.replace(/[^a-z0-9_-]/g, "_");
 };
 
 function promisePopupShown(popup) {
   return new Promise(resolve => {
--- a/browser/components/extensions/schemas/page_action.json
+++ b/browser/components/extensions/schemas/page_action.json
@@ -50,24 +50,26 @@
         "additionalProperties": { "type": "any" },
         "description": "Pixel data for an image. Must be an ImageData object (for example, from a <code>canvas</code> element)."
       }
     ],
     "functions": [
       {
         "name": "show",
         "type": "function",
+        "async": true,
         "description": "Shows the page action. The page action is shown whenever the tab is selected.",
         "parameters": [
           {"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."}
         ]
       },
       {
         "name": "hide",
         "type": "function",
+        "async": true,
         "description": "Hides the page action.",
         "parameters": [
           {"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."}
         ]
       },
       {
         "name": "setTitle",
         "type": "function",
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
@@ -196,19 +196,19 @@ add_task(function* testDetailsObjects() 
     }
 
     // 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;
-      browser.pageAction.show(tabId);
-
-      browser.test.sendMessage("ready", tests);
+      browser.pageAction.show(tabId).then(() => {
+        browser.test.sendMessage("ready", tests);
+      });
     });
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "browser_action": {},
       "page_action": {},
       "background": {
@@ -332,18 +332,19 @@ add_task(function* testDefaultDetails() 
         "browser_action": {"default_icon": icon},
         "page_action": {"default_icon": icon},
       },
 
       background: function() {
         browser.tabs.query({active: true, currentWindow: true}, tabs => {
           let tabId = tabs[0].id;
 
-          browser.pageAction.show(tabId);
-          browser.test.sendMessage("ready");
+          browser.pageAction.show(tabId).then(() => {
+            browser.test.sendMessage("ready");
+          });
         });
       }
     });
 
     yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
 
     let browserActionId = makeWidgetId(extension.id) + "-browser-action";
     let pageActionId = makeWidgetId(extension.id) + "-page-action";
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
@@ -207,18 +207,19 @@ add_task(function* testTabSwitchContext(
       };
       return [
         expect => {
           browser.test.log("Initial state. No icon visible.");
           expect(null);
         },
         expect => {
           browser.test.log("Show the icon on the first tab, expect default properties.");
-          browser.pageAction.show(tabs[0]);
-          expect(details[0]);
+          browser.pageAction.show(tabs[0]).then(() => {
+            expect(details[0]);
+          });
         },
         expect => {
           browser.test.log("Change the icon. Expect default properties excluding the icon.");
           browser.pageAction.setIcon({tabId: tabs[0], path: "1.png"});
           expect(details[1]);
         },
         expect => {
           browser.test.log("Create a new tab. No icon visible.");
@@ -229,22 +230,23 @@ add_task(function* testTabSwitchContext(
         },
         expect => {
           browser.test.log("Await tab load. No icon visible.");
           expect(null);
         },
         expect => {
           browser.test.log("Change properties. Expect new properties.");
           let tabId = tabs[1];
-          browser.pageAction.show(tabId);
-          browser.pageAction.setIcon({tabId, path: "2.png"});
-          browser.pageAction.setPopup({tabId, popup: "2.html"});
-          browser.pageAction.setTitle({tabId, title: "Title 2"});
+          browser.pageAction.show(tabId).then(() => {
+            browser.pageAction.setIcon({tabId, path: "2.png"});
+            browser.pageAction.setPopup({tabId, popup: "2.html"});
+            browser.pageAction.setTitle({tabId, title: "Title 2"});
 
-          expect(details[2]);
+            expect(details[2]);
+          });
         },
         expect => {
           browser.test.log("Clear the title. Expect default title.");
           browser.pageAction.setTitle({tabId: tabs[1], title: ""});
 
           expect(details[3]);
         },
         expect => {
@@ -255,42 +257,45 @@ add_task(function* testTabSwitchContext(
           promiseTabLoad({id: tabs[1], url: "about:blank?1"}).then(() => {
             expect(null);
           });
 
           browser.tabs.update(tabs[1], {url: "about:blank?1"});
         },
         expect => {
           browser.test.log("Show the icon. Expect default properties again.");
-          browser.pageAction.show(tabs[1]);
-          expect(details[0]);
+          browser.pageAction.show(tabs[1]).then(() => {
+            expect(details[0]);
+          });
         },
         expect => {
           browser.test.log("Switch back to the first tab. Expect previously set properties.");
           browser.tabs.update(tabs[0], {active: true}, () => {
             expect(details[1]);
           });
         },
         expect => {
           browser.test.log("Hide the icon on tab 2. Switch back, expect hidden.");
-          browser.pageAction.hide(tabs[1]);
-          browser.tabs.update(tabs[1], {active: true}, () => {
-            expect(null);
+          browser.pageAction.hide(tabs[1]).then(() => {
+            browser.tabs.update(tabs[1], {active: true}, () => {
+              expect(null);
+            });
           });
         },
         expect => {
           browser.test.log("Switch back to tab 1. Expect previous results again.");
           browser.tabs.remove(tabs[1], () => {
             expect(details[1]);
           });
         },
         expect => {
           browser.test.log("Hide the icon. Expect hidden.");
-          browser.pageAction.hide(tabs[0]);
-          expect(null);
+          browser.pageAction.hide(tabs[0]).then(() => {
+            expect(null);
+          });
         },
       ];
     },
   });
 });
 
 add_task(function* testDefaultTitle() {
   yield runTests({
@@ -316,18 +321,19 @@ add_task(function* testDefaultTitle() {
 
       return [
         expect => {
           browser.test.log("Initial state. No icon visible.");
           expect(null);
         },
         expect => {
           browser.test.log("Show the icon on the first tab, expect extension title as default title.");
-          browser.pageAction.show(tabs[0]);
-          expect(details[0]);
+          browser.pageAction.show(tabs[0]).then(() => {
+            expect(details[0]);
+          });
         },
         expect => {
           browser.test.log("Change the title. Expect new title.");
           browser.pageAction.setTitle({tabId: tabs[0], title: "Foo Title"});
           expect(details[1]);
         },
         expect => {
           browser.test.log("Clear the title. Expect extension title.");
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
@@ -123,18 +123,19 @@ add_task(function* testPageActionPopup()
           } else {
             browser.test.notifyPass("pageaction-tests-done");
           }
         });
 
         browser.tabs.query({active: true, currentWindow: true}, tabs => {
           tabId = tabs[0].id;
 
-          browser.pageAction.show(tabId);
-          browser.test.sendMessage("next-test");
+          browser.pageAction.show(tabId).then(() => {
+            browser.test.sendMessage("next-test");
+          });
         });
       },
     },
   });
 
   let pageActionId = makeWidgetId(extension.id) + "-page-action";
   let panelId = makeWidgetId(extension.id) + "-panel";
 
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_popup_resize.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup_resize.js
@@ -9,18 +9,19 @@ add_task(function* testPageActionPopupRe
         "default_popup": "popup.html",
         "browser_style": true,
       },
     },
     background: function() {
       browser.tabs.query({active: true, currentWindow: true}, tabs => {
         const tabId = tabs[0].id;
 
-        browser.pageAction.show(tabId);
-        browser.test.sendMessage("action-shown");
+        browser.pageAction.show(tabId).then(() => {
+          browser.test.sendMessage("action-shown");
+        });
       });
     },
 
     files: {
       "popup.html": "<html><head><meta charset=\"utf-8\"></head></html>",
     },
   });
 
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_simple.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_simple.js
@@ -27,18 +27,19 @@ add_task(function* () {
     background: function() {
       browser.runtime.onMessage.addListener(msg => {
         browser.test.assertEq(msg, "from-popup", "correct message received");
         browser.test.sendMessage("popup");
       });
       browser.tabs.query({active: true, currentWindow: true}, tabs => {
         let tabId = tabs[0].id;
 
-        browser.pageAction.show(tabId);
-        browser.test.sendMessage("page-action-shown");
+        browser.pageAction.show(tabId).then(() => {
+          browser.test.sendMessage("page-action-shown");
+        });
       });
     },
   });
 
   SimpleTest.waitForExplicitFinish();
   let waitForConsole = new Promise(resolve => {
     SimpleTest.monitorConsole(resolve, [{
       message: /Reading manifest: Error processing page_action.unrecognized_property: An unexpected property was found/,
--- a/browser/components/extensions/test/browser/browser_ext_popup_api_injection.js
+++ b/browser/components/extensions/test/browser/browser_ext_popup_api_injection.js
@@ -24,18 +24,19 @@ add_task(function* testPageActionPopup()
                        <script type="application/javascript" src="popup-b.js"></script></head></html>`,
       "popup-b.js": 'browser.test.sendMessage("from-popup-b");',
     },
 
     background: function() {
       let tabId;
       browser.tabs.query({active: true, currentWindow: true}, tabs => {
         tabId = tabs[0].id;
-        browser.pageAction.show(tabId);
-        browser.test.sendMessage("ready");
+        browser.pageAction.show(tabId).then(() => {
+          browser.test.sendMessage("ready");
+        });
       });
 
       browser.test.onMessage.addListener(() => {
         browser.browserAction.setPopup({popup: "/popup-a.html"});
         browser.pageAction.setPopup({tabId, popup: "popup-b.html"});
 
         browser.test.sendMessage("ok");
       });
--- a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_bad.js
@@ -90,18 +90,19 @@ add_task(function* testBadPermissions() 
   yield testHasNoPermission({
     manifest: {
       "permissions": ["http://example.com/", "activeTab"],
       "page_action": {},
     },
     contentSetup() {
       return new Promise(resolve => {
         browser.tabs.query({active: true, currentWindow: true}, tabs => {
-          browser.pageAction.show(tabs[0].id);
-          resolve();
+          browser.pageAction.show(tabs[0].id).then(() => {
+            resolve();
+          });
         });
       });
     },
   });
 
   yield BrowserTestUtils.removeTab(tab2);
   yield BrowserTestUtils.removeTab(tab1);
 });
--- a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_good.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript_good.js
@@ -94,18 +94,19 @@ add_task(function* testGoodPermissions()
   yield testHasPermission({
     manifest: {
       "permissions": ["activeTab"],
       "page_action": {},
     },
     contentSetup() {
       return new Promise(resolve => {
         browser.tabs.query({active: true, currentWindow: true}, tabs => {
-          browser.pageAction.show(tabs[0].id);
-          resolve();
+          browser.pageAction.show(tabs[0].id).then(() => {
+            resolve();
+          });
         });
       });
     },
     setup: clickPageAction,
     tearDown: closePageAction,
   });
 
   info("Test activeTab permission with a browser action w/popup click");
@@ -122,18 +123,19 @@ add_task(function* testGoodPermissions()
   yield testHasPermission({
     manifest: {
       "permissions": ["activeTab"],
       "page_action": {"default_popup": "_blank.html"},
     },
     contentSetup() {
       return new Promise(resolve => {
         browser.tabs.query({active: true, currentWindow: true}, tabs => {
-          browser.pageAction.show(tabs[0].id);
-          resolve();
+          browser.pageAction.show(tabs[0].id).then(() => {
+            resolve();
+          });
         });
       });
     },
     setup: clickPageAction,
     tearDown: closePageAction,
   });
 
   info("Test activeTab permission with a context menu click");
--- a/mobile/android/components/extensions/ext-pageAction.js
+++ b/mobile/android/components/extensions/ext-pageAction.js
@@ -10,58 +10,82 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 // Import the android PageActions module.
 XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
                                   "resource://gre/modules/PageActions.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
+  IconDetails,
   SingletonEventManager,
 } = ExtensionUtils;
 
 // WeakMap[Extension -> PageAction]
 var pageActionMap = new WeakMap();
 
 function PageAction(options, extension) {
   this.id = null;
 
-  let DEFAULT_ICON = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
+  this.extension = extension;
+  this.icons = IconDetails.normalize({path: options.default_icon}, extension);
 
   this.popupUrl = options.default_popup;
 
   this.options = {
     title: options.default_title || extension.name,
-    icon: DEFAULT_ICON,
     id: extension.id,
     clickCallback: () => {
       if (this.popupUrl) {
         let win = Services.wm.getMostRecentWindow("navigator:browser");
         win.BrowserApp.addTab(this.popupUrl, {
           selected: true,
           parentId: win.BrowserApp.selectedTab.id,
         });
       } else {
         this.emit("click");
       }
     },
   };
 
+  this.shouldShow = false;
+
   EventEmitter.decorate(this);
 }
 
 PageAction.prototype = {
-  show(tabId) {
-    // TODO: Only show the PageAction for the tab with the provided tabId.
-    if (!this.id) {
+  show(tabId, context) {
+    if (this.id) {
+      return Promise.resolve();
+    }
+
+    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 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(() => {
+      return Promise.reject({
+        message: "Failed to load PageAction icon",
+      });
+    });
   },
 
   hide(tabId) {
+    this.shouldShow = false;
     if (this.id) {
       PageActions.remove(this.id);
       this.id = null;
     }
   },
 
   setPopup(tab, url) {
     // TODO: Only set the popup for the specified tab once we have Tabs API support.
@@ -101,21 +125,24 @@ extensions.registerSchemaAPI("pageAction
         };
         pageActionMap.get(extension).on("click", listener);
         return () => {
           pageActionMap.get(extension).off("click", listener);
         };
       }).api(),
 
       show(tabId) {
-        pageActionMap.get(extension).show(tabId);
+        return pageActionMap.get(extension)
+                            .show(tabId, context)
+                            .then(() => {});
       },
 
       hide(tabId) {
         pageActionMap.get(extension).hide(tabId);
+        return Promise.resolve();
       },
 
       setPopup(details) {
         // TODO: Use the Tabs API to get the tab from details.tabId.
         let tab = null;
         let url = details.popup && context.uri.resolve(details.popup);
         pageActionMap.get(extension).setPopup(tab, url);
       },
--- a/mobile/android/components/extensions/schemas/page_action.json
+++ b/mobile/android/components/extensions/schemas/page_action.json
@@ -14,17 +14,16 @@
             "additionalProperties": { "$ref": "UnrecognizedProperty" },
             "properties": {
               "default_title": {
                 "type": "string",
                 "optional": true,
                 "preprocess": "localize"
               },
               "default_icon": {
-                "unsupported": true,
                 "$ref": "IconPath",
                 "optional": true
               },
               "default_popup": {
                 "type": "string",
                 "format": "relativeUrl",
                 "optional": true,
                 "preprocess": "localize"
@@ -52,24 +51,26 @@
         "description": "Pixel data for an image. Must be an ImageData object (for example, from a <code>canvas</code> element)."
       }
     ],
     "functions": [
       {
         "name": "show",
         "type": "function",
         "description": "Shows the page action. The page action is shown whenever the tab is selected.",
+        "async": true,
         "parameters": [
           {"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."}
         ]
       },
       {
         "name": "hide",
         "type": "function",
         "description": "Hides the page action.",
+        "async": true,
         "parameters": [
           {"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."}
         ]
       },
       {
         "name": "setTitle",
         "unsupported": true,
         "type": "function",
@@ -156,16 +157,17 @@
             "optional": true,
             "parameters": []
           }
         ]
       },
       {
         "name": "setPopup",
         "type": "function",
+        "async": true,
         "description": "Sets the html document to be opened as a popup when the user clicks on the page action's icon.",
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
               "tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
               "popup": {
@@ -175,37 +177,27 @@
             }
           }
         ]
       },
       {
         "name": "getPopup",
         "type": "function",
         "description": "Gets the html document set as the popup for this page action.",
-        "async": "callback",
+        "async": true,
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
               "tabId": {
                 "type": "integer",
                 "description": "Specify the tab to get the popup from."
               }
             }
-          },
-          {
-            "type": "function",
-            "name": "callback",
-            "parameters": [
-              {
-                "name": "result",
-                "type": "string"
-              }
-            ]
           }
         ]
       }
     ],
     "events": [
       {
         "name": "onClicked",
         "type": "function",
--- a/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html
@@ -8,29 +8,36 @@
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
+let dataURI = "iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
+
+let image = atob(dataURI);
+const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
+
 function backgroundScript() {
   browser.test.assertTrue("pageAction" in browser, "Namespace 'pageAction' exists in browser");
   browser.test.assertTrue("show" in browser.pageAction, "API method 'show' exists in browser.pageAction");
 
   // TODO: Use the Tabs API to obtain the tab ids for showing pageActions.
   let tabId = 1;
   browser.test.onMessage.addListener(msg => {
     if (msg === "pageAction-show") {
-      browser.pageAction.show(tabId);
-      browser.test.sendMessage("page-action-shown");
+      browser.pageAction.show(tabId).then(() => {
+        browser.test.sendMessage("page-action-shown");
+      });
     } else if (msg === "pageAction-hide") {
-      browser.pageAction.hide(tabId);
-      browser.test.sendMessage("page-action-hidden");
+      browser.pageAction.hide(tabId).then(() => {
+        browser.test.sendMessage("page-action-hidden");
+      });
     }
   });
 
   browser.pageAction.onClicked.addListener(tab => {
     // TODO: Make sure we get the correct tab once basic tabs support is added.
     browser.test.sendMessage("page-action-clicked");
   });
 
@@ -39,18 +46,24 @@ function backgroundScript() {
 
 add_task(function* test_contentscript() {
   let extension = ExtensionTestUtils.loadExtension({
     background: "(" + backgroundScript.toString() + ")()",
     manifest: {
       "name": "PageAction Extension",
       "page_action": {
         "default_title": "Page Action",
+        "default_icon": {
+          "18": "extension.png",
+        },
       },
     },
+    files: {
+      "extension.png": IMAGE_ARRAYBUFFER,
+    },
   });
 
   yield extension.startup();
   yield extension.awaitMessage("ready");
 
   extension.sendMessage("pageAction-show");
   yield extension.awaitMessage("page-action-shown");
   ok(isPageActionShown(extension.id), "The PageAction should be shown");
--- a/mobile/android/components/extensions/test/mochitest/test_ext_pageAction_popup.html
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction_popup.html
@@ -10,30 +10,37 @@
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
 Cu.import("resource://gre/modules/Services.jsm");
 
+let dataURI = "iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
+
+let image = atob(dataURI);
+const IMAGE_ARRAYBUFFER = Uint8Array.from(image, byte => byte.charCodeAt(0)).buffer;
+
 add_task(function* test_contentscript() {
   function backgroundScript() {
     // TODO: Use the Tabs API to obtain the tab ids for showing pageActions.
     let tabId = 1;
     let onClickedListenerEnabled = false;
 
     browser.test.onMessage.addListener((msg, details) => {
       if (msg === "page-action-show") {
         // TODO: switch to using .show(tabId).then(...) once bug 1270742 lands.
-        browser.pageAction.show(tabId);
-        browser.test.sendMessage("page-action-shown");
+        browser.pageAction.show(tabId).then(() => {
+          browser.test.sendMessage("page-action-shown");
+        });
       } else if (msg == "page-action-set-popup") {
-        browser.pageAction.setPopup({popup: details.name, tabId: tabId});
-        browser.test.sendMessage("page-action-popup-set");
+        browser.pageAction.setPopup({popup: details.name, tabId: tabId}).then(() => {
+          browser.test.sendMessage("page-action-popup-set");
+        });
       } else if (msg == "page-action-get-popup") {
         browser.pageAction.getPopup({tabId: tabId}).then(url => {
           browser.test.sendMessage("page-action-got-popup", url);
         });
       } else if (msg == "page-action-enable-onClicked-listener") {
         onClickedListenerEnabled = true;
         browser.test.sendMessage("page-action-onClicked-listener-enabled");
       } else if (msg == "page-action-disable-onClicked-listener") {
@@ -65,20 +72,24 @@ add_task(function* test_contentscript() 
 
   let extension = ExtensionTestUtils.loadExtension({
     background: `(${backgroundScript}())`,
     manifest: {
       "name": "PageAction Extension",
       "page_action": {
         "default_title": "Page Action",
         "default_popup": "default.html",
+        "default_icon": {
+          "18": "extension.png",
+        },
       },
     },
     files: {
       "default.html": `<html><head><meta charset="utf-8"><script src="popup.js"></${"script"}></head></html>`,
+      "extension.png": IMAGE_ARRAYBUFFER,
       "a.html": `<html><head><meta charset="utf-8"><script src="popup.js"></${"script"}></head></html>`,
       "b.html": `<html><head><meta charset="utf-8"><script src="popup.js"></${"script"}></head></html>`,
       "popup.js": `(${popupScript})()`,
     },
   });
 
   let tabClosedPromise = () => {
     return new Promise(resolve => {
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -6,19 +6,23 @@
 
 this.EXPORTED_SYMBOLS = ["ExtensionUtils"];
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 const Cr = Components.results;
 
+const INTEGER = /^[1-9]\d*$/;
+
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+                                  "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LanguageDetector",
                                   "resource:///modules/translation/LanguageDetector.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Locale",
                                   "resource://gre/modules/Locale.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MessageChannel",
                                   "resource://gre/modules/MessageChannel.jsm");
@@ -386,16 +390,162 @@ class BaseContext {
     });
 
     for (let obj of this.onClose) {
       obj.close();
     }
   }
 }
 
+// Manages icon details for toolbar buttons in the |pageAction| and
+// |browserAction| APIs.
+let IconDetails = {
+  // Normalizes the various acceptable input formats into an object
+  // with icon size as key and icon URL as value.
+  //
+  // If a context is specified (function is called from an extension):
+  // Throws an error if an invalid icon size was provided or the
+  // extension is not allowed to load the specified resources.
+  //
+  // If no context is specified, instead of throwing an error, this
+  // function simply logs a warning message.
+  normalize(details, extension, context = null) {
+    let result = {};
+
+    try {
+      if (details.imageData) {
+        let imageData = details.imageData;
+
+        // The global might actually be from Schema.jsm, which
+        // normalizes most of our arguments. In that case it won't have
+        // an ImageData property. But Schema.jsm doesn't normalize
+        // actual ImageData objects, so they will come from a global
+        // with the right property.
+        if (instanceOf(imageData, "ImageData")) {
+          imageData = {"19": imageData};
+        }
+
+        for (let size of Object.keys(imageData)) {
+          if (!INTEGER.test(size)) {
+            throw new Error(`Invalid icon size ${size}, must be an integer`);
+          }
+          result[size] = this.convertImageDataToDataURL(imageData[size], context);
+        }
+      }
+
+      if (details.path) {
+        let path = details.path;
+        if (typeof path != "object") {
+          path = {"19": path};
+        }
+
+        let baseURI = context ? context.uri : extension.baseURI;
+
+        for (let size of Object.keys(path)) {
+          if (!INTEGER.test(size)) {
+            throw new Error(`Invalid icon size ${size}, must be an integer`);
+          }
+
+          let url = baseURI.resolve(path[size]);
+
+          // The Chrome documentation specifies these parameters as
+          // relative paths. We currently accept absolute URLs as well,
+          // which means we need to check that the extension is allowed
+          // to load them. This will throw an error if it's not allowed.
+          Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
+            extension.principal, url,
+            Services.scriptSecurityManager.DISALLOW_SCRIPT);
+
+          result[size] = url;
+        }
+      }
+    } catch (e) {
+      // Function is called from extension code, delegate error.
+      if (context) {
+        throw e;
+      }
+      // If there's no context, it's because we're handling this
+      // as a manifest directive. Log a warning rather than
+      // raising an error.
+      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) {
+    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))
+                        .sort((a, b) => a - b);
+
+      bestSize = sizes.find(candidate => candidate > size) || sizes.pop();
+    }
+
+    if (bestSize) {
+      return {size: bestSize, icon: icons[bestSize]};
+    }
+
+    return {size, icon: DEFAULT};
+  },
+
+  convertImageURLToDataURL(imageURL, context, browserWindow, size = 18) {
+    return new Promise((resolve, reject) => {
+      let image = new context.contentWindow.Image();
+      image.onload = function() {
+        let canvas = context.contentWindow.document.createElement("canvas");
+        let ctx = canvas.getContext("2d");
+        let dSize = size * browserWindow.devicePixelRatio;
+
+        // Scales the image while maintaing width to height ratio.
+        // If the width and height differ, the image is centered using the
+        // smaller of the two dimensions.
+        let dWidth, dHeight, dx, dy;
+        if (this.width > this.height) {
+          dWidth = dSize;
+          dHeight = image.height * (dSize / image.width);
+          dx = 0;
+          dy = (dSize - dHeight) / 2;
+        } else {
+          dWidth = image.width * (dSize / image.height);
+          dHeight = dSize;
+          dx = (dSize - dWidth) / 2;
+          dy = 0;
+        }
+
+        ctx.drawImage(this, 0, 0, this.width, this.height, dx, dy, dWidth, dHeight);
+        resolve(canvas.toDataURL("image/png"));
+      };
+      image.onerror = reject;
+      image.src = imageURL;
+    });
+  },
+
+  convertImageDataToDataURL(imageData, context) {
+    let document = context.contentWindow.document;
+    let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+    canvas.width = imageData.width;
+    canvas.height = imageData.height;
+    canvas.getContext("2d").putImageData(imageData, 0, 0);
+
+    return canvas.toDataURL("image/png");
+  },
+};
+
 function LocaleData(data) {
   this.defaultLocale = data.defaultLocale;
   this.selectedLocale = data.selectedLocale;
   this.locales = data.locales || new Map();
   this.warnedMissingKeys = new Set();
 
   // Map(locale-name -> Map(message-key -> localized-string))
   //
@@ -403,16 +553,17 @@ function LocaleData(data) {
   // Map of message keys to their localized strings.
   this.messages = data.messages || new Map();
 
   if (data.builtinMessages) {
     this.messages.set(this.BUILTIN, data.builtinMessages);
   }
 }
 
+
 LocaleData.prototype = {
   // Representation of the object to send to content processes. This
   // should include anything the content process might need.
   serialize() {
     return {
       defaultLocale: this.defaultLocale,
       selectedLocale: this.selectedLocale,
       messages: this.messages,
@@ -1251,15 +1402,16 @@ this.ExtensionUtils = {
   promiseDocumentReady,
   runSafe,
   runSafeSync,
   runSafeSyncWithoutClone,
   runSafeWithoutClone,
   BaseContext,
   DefaultWeakMap,
   EventManager,
+  IconDetails,
   LocaleData,
   Messenger,
   PlatformInfo,
   SingletonEventManager,
   SpreadArgs,
   ChildAPIManager,
 };
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -941,20 +941,21 @@ class ArrayType extends Type {
   }
 
   checkBaseType(baseType) {
     return baseType == "array";
   }
 }
 
 class FunctionType extends Type {
-  constructor(schema, parameters, isAsync) {
+  constructor(schema, parameters, isAsync, hasAsyncCallback) {
     super(schema);
     this.parameters = parameters;
     this.isAsync = isAsync;
+    this.hasAsyncCallback = hasAsyncCallback;
   }
 
   normalize(value, context) {
     return this.normalizeBase("function", value, context);
   }
 
   checkBaseType(baseType) {
     return baseType == "function";
@@ -1151,33 +1152,37 @@ class CallEntry extends Entry {
 class FunctionEntry extends CallEntry {
   constructor(schema, path, name, type, unsupported, allowAmbiguousOptionalArguments, returns, permissions) {
     super(schema, path, name, type.parameters, allowAmbiguousOptionalArguments);
     this.unsupported = unsupported;
     this.returns = returns;
     this.permissions = permissions;
 
     this.isAsync = type.isAsync;
+    this.hasAsyncCallback = type.hasAsyncCallback;
   }
 
   inject(path, name, dest, context) {
     if (this.unsupported) {
       return;
     }
 
     if (this.permissions && !this.permissions.some(perm => context.hasPermission(perm))) {
       return;
     }
 
     let stub;
     if (this.isAsync) {
       stub = (...args) => {
         this.checkDeprecated(context);
         let actuals = this.checkParameters(args, context);
-        let callback = actuals.pop();
+        let callback = null;
+        if (this.hasAsyncCallback) {
+          callback = actuals.pop();
+        }
         return context.callAsyncFunction(path, name, actuals, callback);
       };
     } else if (!this.returns) {
       stub = (...args) => {
         this.checkDeprecated(context);
         let actuals = this.checkParameters(args, context);
         return context.callFunctionNoReturn(path, name, actuals);
       };
@@ -1398,17 +1403,17 @@ this.Schemas = {
       return new NumberType(type);
     } else if (type.type == "integer") {
       checkTypeProperties("minimum", "maximum");
       return new IntegerType(type, type.minimum || -Infinity, type.maximum || Infinity);
     } else if (type.type == "boolean") {
       checkTypeProperties();
       return new BooleanType(type);
     } else if (type.type == "function") {
-      let isAsync = typeof(type.async) == "string";
+      let isAsync = Boolean(type.async);
 
       let parameters = null;
       if ("parameters" in type) {
         parameters = [];
         for (let param of type.parameters) {
           // Callbacks default to optional for now, because of promise
           // handling.
           let isCallback = isAsync && param.name == type.async;
@@ -1416,27 +1421,28 @@ this.Schemas = {
           parameters.push({
             type: this.parseType(path, param, ["name", "optional"]),
             name: param.name,
             optional: param.optional == null ? isCallback : param.optional,
           });
         }
       }
 
+      let hasAsyncCallback = false;
       if (isAsync) {
-        if (!parameters || !parameters.length || parameters[parameters.length - 1].name != type.async) {
-          throw new Error(`Internal error: "async" property must name the last parameter of the function.`);
+        if (parameters && parameters.length && parameters[parameters.length - 1].name == type.async) {
+          hasAsyncCallback = true;
         }
         if (type.returns || type.allowAmbiguousOptionalArguments) {
           throw new Error(`Internal error: Async functions must not have return values or ambiguous arguments.`);
         }
       }
 
       checkTypeProperties("parameters", "async", "returns");
-      return new FunctionType(type, parameters, isAsync);
+      return new FunctionType(type, parameters, isAsync, hasAsyncCallback);
     } else if (type.type == "any") {
       // Need to see what minimum and maximum are supposed to do here.
       checkTypeProperties("minimum", "maximum");
       return new AnyType(type);
     } else {
       throw new Error(`Unexpected type ${type.type}`);
     }
   },