--- 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",