Bug 1267124 - Implement chrome.pageAction.show on Android. r=kmag, r=margaret
authorMatthew Wein <mwein@mozilla.com>
Wed, 27 Apr 2016 19:06:24 -0400
changeset 296962 7fc6b24beea495728bc5070a5b621b31e403b82d
parent 296961 43ee68814e2395de77b888f43945e638611dd0f6
child 296963 95e7b6e69427410b242d17a2642d2123114c72b7
push id76540
push usercbook@mozilla.com
push dateWed, 11 May 2016 13:22:31 +0000
treeherdermozilla-inbound@d2b9e9a5fe35 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag, margaret
bugs1267124
milestone49.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
Bug 1267124 - Implement chrome.pageAction.show on Android. r=kmag, r=margaret MozReview-Commit-ID: AOwfuuCfhRx
mobile/android/chrome/content/browser.js
mobile/android/components/extensions/.eslintrc
mobile/android/components/extensions/ext-pageAction.js
mobile/android/components/extensions/extension.svg
mobile/android/components/extensions/jar.mn
mobile/android/components/extensions/moz.build
mobile/android/components/extensions/schemas/jar.mn
mobile/android/components/extensions/schemas/moz.build
mobile/android/components/extensions/schemas/page_action.json
mobile/android/components/extensions/test/mochitest/.eslintrc
mobile/android/components/extensions/test/mochitest/chrome.ini
mobile/android/components/extensions/test/mochitest/head.js
mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html
mobile/android/components/moz.build
mobile/android/modules/PageActions.jsm
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -89,16 +89,19 @@ XPCOMUtils.defineLazyServiceGetter(this,
 
 XPCOMUtils.defineLazyServiceGetter(this, "Profiler",
                                    "@mozilla.org/tools/profiler;1",
                                    "nsIProfiler");
 
 XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery",
                                   "resource://gre/modules/SimpleServiceDiscovery.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionManagement",
+                                  "resource://gre/modules/ExtensionManagement.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
                                   "resource://gre/modules/CharsetMenu.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NetErrorHelper",
                                   "resource://gre/modules/NetErrorHelper.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils",
                                   "resource://gre/modules/PermissionsUtils.jsm");
@@ -381,16 +384,22 @@ var BrowserApp = {
     Services.obs.addObserver(this, "android-get-pref", false);
     Services.obs.addObserver(this, "android-set-pref", false);
     Services.obs.addObserver(this, "gather-telemetry", false);
     Services.obs.addObserver(this, "keyword-search", false);
     Services.obs.addObserver(this, "sessionstore-state-purge-complete", false);
     Services.obs.addObserver(this, "Fonts:Reload", false);
     Services.obs.addObserver(this, "Vibration:Request", false);
 
+    // Register extension source files.
+    ExtensionManagement.registerScript("chrome://browser/content/ext-pageAction.js");
+
+    // Register extension schemas.
+    ExtensionManagement.registerSchema("chrome://browser/content/schemas/page_action.json");
+
     Messaging.addListener(this.getHistory.bind(this), "Session:GetHistory");
 
     function showFullScreenWarning() {
       Snackbars.show(Strings.browser.GetStringFromName("alertFullScreenToast"), Snackbars.LENGTH_LONG);
     }
 
     window.addEventListener("fullscreen", function() {
       Messaging.sendRequest({
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/.eslintrc
@@ -0,0 +1,3 @@
+{
+  "extends": "../../../../toolkit/components/extensions/.eslintrc",
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/ext-pageAction.js
@@ -0,0 +1,62 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+// Import the android PageActions module.
+XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
+                                  "resource://gre/modules/PageActions.jsm");
+
+// 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.options = {
+    title: options.default_title || extension.name,
+    icon: DEFAULT_ICON,
+    id: extension.id,
+  };
+}
+
+PageAction.prototype = {
+  show(tabId) {
+    // TODO: Only show the PageAction for the tab with the provided tabId.
+    if (!this.id) {
+      this.id = PageActions.add(this.options);
+    }
+  },
+
+  shutdown() {
+    if (this.id) {
+      PageActions.remove(this.id);
+      this.id = null;
+    }
+  },
+};
+
+/* eslint-disable mozilla/balanced-listeners */
+extensions.on("manifest_page_action", (type, directive, extension, manifest) => {
+  let pageAction = new PageAction(manifest.page_action, extension);
+  pageActionMap.set(extension, pageAction);
+});
+
+extensions.on("shutdown", (type, extension) => {
+  if (pageActionMap.has(extension)) {
+    pageActionMap.get(extension).shutdown();
+    pageActionMap.delete(extension);
+  }
+});
+/* eslint-enable mozilla/balanced-listeners */
+
+extensions.registerSchemaAPI("pageAction", null, (extension, context) => {
+  return {
+    pageAction: {
+      show(tabId) {
+        pageActionMap.get(extension).show(tabId);
+      },
+    },
+  };
+});
copy from browser/components/extensions/extension.svg
copy to mobile/android/components/extensions/extension.svg
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/jar.mn
@@ -0,0 +1,7 @@
+# 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/.
+
+chrome.jar:
+    content/extension.svg
+    content/ext-pageAction.js
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ['jar.mn']
+
+DIRS += ['schemas']
+
+MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/schemas/jar.mn
@@ -0,0 +1,6 @@
+# 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/.
+
+chrome.jar:
+    content/schemas/page_action.json
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/schemas/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+JAR_MANIFESTS += ['jar.mn']
\ No newline at end of file
copy from browser/components/extensions/schemas/page_action.json
copy to mobile/android/components/extensions/schemas/page_action.json
--- a/browser/components/extensions/schemas/page_action.json
+++ b/mobile/android/components/extensions/schemas/page_action.json
@@ -14,20 +14,22 @@
             "additionalProperties": { "$ref": "UnrecognizedProperty" },
             "properties": {
               "default_title": {
                 "type": "string",
                 "optional": true,
                 "preprocess": "localize"
               },
               "default_icon": {
+                "unsupported": true,
                 "$ref": "IconPath",
                 "optional": true
               },
               "default_popup": {
+                "unsupported": true,
                 "type": "string",
                 "format": "relativeUrl",
                 "optional": true,
                 "preprocess": "localize"
               },
               "browser_style": {
                 "type": "boolean",
                 "optional": true
@@ -57,39 +59,42 @@
         "type": "function",
         "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",
+        "unsupported": true,
         "type": "function",
         "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",
+        "unsupported": true,
         "type": "function",
         "description": "Sets the title of the page action. This is displayed in a tooltip over the page action.",
         "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."},
               "title": {"type": "string", "description": "The tooltip string."}
             }
           }
         ]
       },
       {
         "name": "getTitle",
+        "unsupported": true,
         "type": "function",
         "description": "Gets the title of the page action.",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
@@ -108,16 +113,17 @@
                 "type": "string"
               }
             ]
           }
         ]
       },
       {
         "name": "setIcon",
+        "unsupported": true,
         "type": "function",
         "description": "Sets the icon for the page action. The icon can be specified either as the path to an image file or as the pixel data from a canvas element, or as dictionary of either one of those. Either the <b>path</b> or the <b>imageData</b> property must be specified.",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
@@ -151,16 +157,17 @@
             "name": "callback",
             "optional": true,
             "parameters": []
           }
         ]
       },
       {
         "name": "setPopup",
+        "unsupported": true,
         "type": "function",
         "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."},
@@ -169,16 +176,17 @@
                 "description": "The html file to show in a popup.  If set to the empty string (''), no popup is shown."
               }
             }
           }
         ]
       },
       {
         "name": "getPopup",
+        "unsupported": true,
         "type": "function",
         "description": "Gets the html document set as the popup for this page action.",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
@@ -199,16 +207,17 @@
             ]
           }
         ]
       }
     ],
     "events": [
       {
         "name": "onClicked",
+        "unsupported": true,
         "type": "function",
         "description": "Fired when a page action icon is clicked.  This event will not fire if the page action has a popup.",
         "parameters": [
           {
             "name": "tab",
             "$ref": "tabs.Tab"
           }
         ]
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/.eslintrc
@@ -0,0 +1,7 @@
+{
+  "extends": "../../../../../../toolkit/components/extensions/test/mochitest/.eslintrc",
+
+  "globals": {
+    "isPageActionShown": true,
+  },
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/chrome.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+support-files =
+  head.js
+
+[test_ext_pageAction.html]
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/head.js
@@ -0,0 +1,11 @@
+"use strict";
+
+/* exported isPageActionShown */
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/PageActions.jsm");
+
+function isPageActionShown(extensionId) {
+  return PageActions.isShown(extensionId);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>PageAction Test</title>
+  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+  <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <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";
+
+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.pageAction.show(tabId);
+  browser.test.sendMessage("page-action-shown");
+
+  browser.test.notifyPass("page-action");
+}
+
+add_task(function* test_contentscript() {
+  let extension = ExtensionTestUtils.loadExtension({
+    background: "(" + backgroundScript.toString() + ")()",
+    manifest: {
+      "name": "PageAction Extension",
+      "page_action": {
+        "default_title": "Page Action",
+      },
+    },
+  });
+
+  yield extension.startup();
+  yield extension.awaitMessage("page-action-shown");
+
+  is(isPageActionShown(extension.id), true, "The PageAction should be shown");
+
+  yield extension.awaitFinish("page-action");
+  yield extension.unload();
+
+  is(isPageActionShown(extension.id), false, "The PageAction should be removed after unload");
+});
+</script>
+
+</body>
+</html>
--- a/mobile/android/components/moz.build
+++ b/mobile/android/components/moz.build
@@ -35,9 +35,12 @@ EXTRA_COMPONENTS += [
 ]
 
 # Keep it this way if at all possible.  If you need preprocessing,
 # consider adding fields to AppConstants.jsm.
 EXTRA_PP_COMPONENTS += [
     'MobileComponents.manifest',
 ]
 
-DIRS += ['build']
+DIRS += [
+    'extensions',
+    'build',
+]
--- a/mobile/android/modules/PageActions.jsm
+++ b/mobile/android/modules/PageActions.jsm
@@ -49,41 +49,52 @@ var PageActions = {
     if (this._inited && Object.keys(this._items).length == 0) {
       this._inited = false;
       Services.obs.removeObserver(this, "PageActions:Clicked");
       Services.obs.removeObserver(this, "PageActions:LongClicked");
     }
   },
 
   observe: function(aSubject, aTopic, aData) {
+    let item = this._items[aData];
     if (aTopic == "PageActions:Clicked") {
-      if (this._items[aData].clickCallback) {
-        this._items[aData].clickCallback();
+      if (item.clickCallback) {
+        item.clickCallback();
       }
     } else if (aTopic == "PageActions:LongClicked") {
-      if (this._items[aData].longClickCallback) {
-        this._items[aData].longClickCallback();
+      if (item.longClickCallback) {
+        item.longClickCallback();
       }
     }
   },
 
+  isShown: function(id) {
+    return !!this._items[id];
+  },
+
   add: function(aOptions) {
-    let id = uuidgen.generateUUID().toString();
+    let id = aOptions.id || uuidgen.generateUUID().toString()
+
     Messaging.sendRequest({
       type: "PageActions:Add",
       id: id,
       title: aOptions.title,
       icon: resolveGeckoURI(aOptions.icon),
       important: "important" in aOptions ? aOptions.important : false
     });
 
-    this._items[id] = {
-      clickCallback: aOptions.clickCallback,
-      longClickCallback: aOptions.longClickCallback
-    };
+    this._items[id] = {};
+
+    if (aOptions.clickCallback) {
+      this._items[id].clickCallback = aOptions.clickCallback;
+    }
+
+    if (aOptions.longClickCallback) {
+      this._items[id].longClickCallback = aOptions.longClickCallback;
+    }
 
     this._maybeInit();
     return id;
   },
 
   remove: function(id) {
     Messaging.sendRequest({
       type: "PageActions:Remove",