Bug 1267124 - Implement chrome.pageAction.show on android. r?kmag,margaret draft
authorMatthew Wein <mwein@mozilla.com>
Wed, 27 Apr 2016 19:06:24 -0400
changeset 365484 13d1e23a783312c981e91c743e33549b72e0db50
parent 365483 ee562525573f8896fe4f7a5ac053de3d97ae4ccb
child 520570 ad7b1dfed5c7a627ba0c10779d5f97072be3852b
push id17756
push usermwein@mozilla.com
push dateTue, 10 May 2016 22:07:50 +0000
reviewerskmag, margaret
bugs1267124
milestone49.0a1
Bug 1267124 - Implement chrome.pageAction.show on android. r?kmag,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",