Bug 1530402 - Implement {Browser,Page}Action for GeckoView. r=snorp,mixedpuppy,esawin

Design doc: https://docs.google.com/document/d/1XJuKk9Hm_2RNbX8KRcyUOXTaELBWYMyXBUchz15OElY

Differential Revision: https://phabricator.services.mozilla.com/D49041

--HG--
rename : browser/components/extensions/schemas/browser_action.json => toolkit/components/extensions/schemas/browser_action.json
rename : browser/components/extensions/schemas/page_action.json => toolkit/components/extensions/schemas/page_action.json
extra : moz-landing-system : lando
This commit is contained in:
Agi Sferro 2019-11-15 16:33:57 +00:00
Родитель 8e647541c1
Коммит 55147a9528
29 изменённых файлов: 1769 добавлений и 13 удалений

Просмотреть файл

@ -9,7 +9,7 @@
},
"browserAction": {
"url": "chrome://browser/content/parent/ext-browserAction.js",
"schema": "chrome://browser/content/schemas/browser_action.json",
"schema": "chrome://extensions/content/schemas/browser_action.json",
"scopes": ["addon_parent"],
"manifest": ["browser_action"],
"paths": [
@ -140,7 +140,7 @@
},
"pageAction": {
"url": "chrome://browser/content/parent/ext-pageAction.js",
"schema": "chrome://browser/content/schemas/page_action.json",
"schema": "chrome://extensions/content/schemas/page_action.json",
"scopes": ["addon_parent"],
"manifest": ["page_action"],
"paths": [

Просмотреть файл

@ -4,7 +4,6 @@
browser.jar:
content/browser/schemas/bookmarks.json
content/browser/schemas/browser_action.json
content/browser/schemas/browsing_data.json
content/browser/schemas/chrome_settings_overrides.json
content/browser/schemas/commands.json
@ -18,7 +17,6 @@ browser.jar:
content/browser/schemas/menus_child.json
content/browser/schemas/normandyAddonStudy.json
content/browser/schemas/omnibox.json
content/browser/schemas/page_action.json
content/browser/schemas/pkcs11.json
content/browser/schemas/search.json
content/browser/schemas/sessions.json

Просмотреть файл

@ -70,6 +70,13 @@ global.openOptionsPage = extension => {
};
extensions.registerModules({
browserAction: {
url: "chrome://geckoview/content/ext-browserAction.js",
schema: "chrome://extensions/content/schemas/browser_action.json",
scopes: ["addon_parent"],
manifest: ["browser_action"],
paths: [["browserAction"]],
},
browsingData: {
url: "chrome://geckoview/content/ext-browsingData.js",
schema: "chrome://geckoview/content/schemas/browsing_data.json",
@ -77,6 +84,13 @@ extensions.registerModules({
manifest: ["browsing_data"],
paths: [["browsingData"]],
},
pageAction: {
url: "chrome://geckoview/content/ext-pageAction.js",
schema: "chrome://extensions/content/schemas/page_action.json",
scopes: ["addon_parent"],
manifest: ["page_action"],
paths: [["pageAction"]],
},
tabs: {
url: "chrome://geckoview/content/ext-tabs.js",
schema: "chrome://geckoview/content/schemas/tabs.json",

Просмотреть файл

@ -0,0 +1,128 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
// The ext-* files are imported into the same scopes.
/* import-globals-from ext-utils.js */
XPCOMUtils.defineLazyModuleGetters(this, {
GeckoViewWebExtension: "resource://gre/modules/GeckoViewWebExtension.jsm",
ExtensionActionHelper: "resource://gre/modules/GeckoViewWebExtension.jsm",
});
const { BrowserActionBase } = ChromeUtils.import(
"resource://gre/modules/ExtensionActions.jsm"
);
const BROWSER_ACTION_PROPERTIES = [
"title",
"icon",
"popup",
"badgeText",
"badgeBackgroundColor",
"badgeTextColor",
"enabled",
"patternMatching",
];
class BrowserAction extends BrowserActionBase {
constructor(extension, clickDelegate) {
const tabContext = new TabContext(tabId => this.getContextData(null));
super(tabContext, extension);
this.clickDelegate = clickDelegate;
this.helper = new ExtensionActionHelper({
extension,
tabTracker,
windowTracker,
tabContext,
properties: BROWSER_ACTION_PROPERTIES,
});
}
updateOnChange(tab) {
const tabId = tab ? tab.id : null;
const action = tab
? this.getContextData(tab)
: this.helper.extractProperties(this.globals);
this.helper.sendRequestForResult(tabId, {
action,
type: "GeckoView:BrowserAction:Update",
});
}
openPopup() {
const tab = tabTracker.activeTab;
const action = this.getContextData(tab);
this.helper.sendRequest(tab.id, {
action,
type: "GeckoView:BrowserAction:OpenPopup",
});
}
getTab(tabId) {
return this.helper.getTab(tabId);
}
getWindow(windowId) {
return this.helper.getWindow(windowId);
}
click() {
this.clickDelegate.onClick();
}
}
this.browserAction = class extends ExtensionAPI {
async onManifestEntry(entryName) {
const { extension } = this;
this.action = new BrowserAction(extension, this);
await this.action.loadIconData();
GeckoViewWebExtension.browserActions.set(extension, this.action);
// Notify the embedder of this action
this.action.updateOnChange(null);
}
onShutdown() {
const { extension } = this;
this.action.onShutdown();
GeckoViewWebExtension.browserActions.delete(extension);
}
onClick() {
this.emit("click", tabTracker.activeTab);
}
getAPI(context) {
const { extension } = context;
const { tabManager } = extension;
const { action } = this;
return {
browserAction: {
...action.api(context),
onClicked: new EventManager({
context,
name: "browserAction.onClicked",
register: fire => {
const listener = (event, tab) => {
fire.async(tabManager.convert(tab));
};
this.on("click", listener);
return () => {
this.off("click", listener);
};
},
}).api(),
openPopup: function() {
action.openPopup();
},
},
};
}
};
global.browserActionFor = this.browserAction.for;

Просмотреть файл

@ -0,0 +1,122 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
// The ext-* files are imported into the same scopes.
/* import-globals-from ext-utils.js */
XPCOMUtils.defineLazyModuleGetters(this, {
GeckoViewWebExtension: "resource://gre/modules/GeckoViewWebExtension.jsm",
ExtensionActionHelper: "resource://gre/modules/GeckoViewWebExtension.jsm",
});
const { PageActionBase } = ChromeUtils.import(
"resource://gre/modules/ExtensionActions.jsm"
);
const PAGE_ACTION_PROPERTIES = [
"title",
"icon",
"popup",
"badgeText",
"enabled",
"patternMatching",
];
class PageAction extends PageActionBase {
constructor(extension, clickDelegate) {
const tabContext = new TabContext(tabId => this.getContextData(null));
super(tabContext, extension);
this.clickDelegate = clickDelegate;
this.helper = new ExtensionActionHelper({
extension,
tabTracker,
windowTracker,
tabContext,
properties: PAGE_ACTION_PROPERTIES,
});
}
updateOnChange(tab) {
const tabId = tab ? tab.id : null;
// The embedder only gets the override, not the full object
const action = tab
? this.getContextData(tab)
: this.helper.extractProperties(this.globals);
this.helper.sendRequestForResult(tabId, {
action,
type: "GeckoView:PageAction:Update",
});
}
openPopup() {
const action = this.getContextData(tabTracker.activeTab);
this.helper.sendRequest(tabTracker.activeTab.id, {
action,
type: "GeckoView:PageAction:OpenPopup",
});
}
getTab(tabId) {
return this.helper.getTab(tabId);
}
click() {
this.clickDelegate.onClick();
}
}
this.pageAction = class extends ExtensionAPI {
async onManifestEntry(entryName) {
const { extension } = this;
const action = new PageAction(extension, this);
await action.loadIconData();
this.action = action;
GeckoViewWebExtension.pageActions.set(extension, action);
// Notify the embedder of this action
action.updateOnChange(null);
}
onClick() {
this.emit("click", tabTracker.activeTab);
}
onShutdown() {
const { extension, action } = this;
action.onShutdown();
GeckoViewWebExtension.pageActions.delete(extension);
}
getAPI(context) {
const { extension } = context;
const { tabManager } = extension;
const { action } = this;
return {
pageAction: {
...action.api(context),
onClicked: new EventManager({
context,
name: "pageAction.onClicked",
register: fire => {
const listener = (event, tab) => {
fire.async(tabManager.convert(tab));
};
this.on("click", listener);
return () => {
this.off("click", listener);
};
},
}).api(),
openPopup() {
action.openPopup();
},
},
};
}
};

Просмотреть файл

@ -69,6 +69,8 @@ GeckoViewStartup.prototype = {
GeckoViewUtils.addLazyGetter(this, "GeckoViewWebExtension", {
module: "resource://gre/modules/GeckoViewWebExtension.jsm",
ged: [
"GeckoView:BrowserAction:Click",
"GeckoView:PageAction:Click",
"GeckoView:RegisterWebExtension",
"GeckoView:UnregisterWebExtension",
"GeckoView:WebExtension:PortDisconnect",

Просмотреть файл

@ -601,6 +601,7 @@ package org.mozilla.geckoview {
method @UiThread public void getSurfaceBounds(@NonNull Rect);
method @AnyThread @NonNull public SessionTextInput getTextInput();
method @AnyThread @NonNull public GeckoResult<String> getUserAgent();
method @AnyThread @Nullable public WebExtension.ActionDelegate getWebExtensionActionDelegate(@NonNull WebExtension);
method @AnyThread public void goBack();
method @AnyThread public void goForward();
method @AnyThread public void gotoHistoryIndex(int);
@ -639,6 +640,7 @@ package org.mozilla.geckoview {
method @AnyThread public void setPromptDelegate(@Nullable GeckoSession.PromptDelegate);
method @UiThread public void setScrollDelegate(@Nullable GeckoSession.ScrollDelegate);
method @UiThread public void setSelectionActionDelegate(@Nullable GeckoSession.SelectionActionDelegate);
method @AnyThread public void setWebExtensionActionDelegate(@NonNull WebExtension, @Nullable WebExtension.ActionDelegate);
method @AnyThread public void stop();
method @UiThread protected void setShouldPinOnScreen(boolean);
field public static final Parcelable.Creator<GeckoSession> CREATOR;
@ -1387,12 +1389,37 @@ package org.mozilla.geckoview {
public class WebExtension {
ctor public WebExtension(@NonNull String, @NonNull String, long);
ctor public WebExtension(@NonNull String);
method @AnyThread public void setActionDelegate(@Nullable WebExtension.ActionDelegate);
method @UiThread public void setMessageDelegate(@Nullable WebExtension.MessageDelegate, @NonNull String);
field public final long flags;
field @NonNull public final String id;
field @NonNull public final String location;
}
@AnyThread public static class WebExtension.Action {
ctor protected Action();
method @UiThread public void click();
method @NonNull public WebExtension.Action withDefault(@NonNull WebExtension.Action);
field @Nullable public final Integer badgeBackgroundColor;
field @Nullable public final String badgeText;
field @Nullable public final Integer badgeTextColor;
field @Nullable public final Boolean enabled;
field @Nullable public final WebExtension.ActionIcon icon;
field @Nullable public final String title;
}
public static interface WebExtension.ActionDelegate {
method @UiThread default public void onBrowserAction(@NonNull WebExtension, @Nullable GeckoSession, @NonNull WebExtension.Action);
method @UiThread @Nullable default public GeckoResult<GeckoSession> onOpenPopup(@NonNull WebExtension, @NonNull WebExtension.Action);
method @UiThread default public void onPageAction(@NonNull WebExtension, @Nullable GeckoSession, @NonNull WebExtension.Action);
method @UiThread @Nullable default public GeckoResult<GeckoSession> onTogglePopup(@NonNull WebExtension, @NonNull WebExtension.Action);
}
public static class WebExtension.ActionIcon {
ctor protected ActionIcon();
method @AnyThread @NonNull public GeckoResult<Bitmap> get(int);
}
public static class WebExtension.Flags {
ctor protected Flags();
field public static final long ALLOW_CONTENT_MESSAGING = 1L;

Просмотреть файл

@ -0,0 +1,140 @@
const port = browser.runtime.connectNative("browser");
port.onMessage.addListener(message => {
handleMessage(message, null);
});
browser.runtime.onMessage.addListener((message, sender) => {
handleMessage(message, sender.tab.id);
});
browser.pageAction.onClicked.addListener(tab => {
port.postMessage({ method: "onClicked", tabId: tab.id, type: "pageAction" });
});
browser.browserAction.onClicked.addListener(tab => {
port.postMessage({
method: "onClicked",
tabId: tab.id,
type: "browserAction",
});
});
function handlePageActionMessage(message, tabId) {
switch (message.action) {
case "enable":
browser.pageAction.show(tabId);
break;
case "disable":
browser.pageAction.hide(tabId);
break;
case "setPopup":
browser.pageAction.setPopup({
tabId,
popup: message.popup,
});
break;
case "setTitle":
browser.pageAction.setTitle({
tabId,
title: message.title,
});
break;
case "setIcon":
browser.pageAction.setIcon({
tabId,
imageData: message.imageData,
path: message.path,
});
break;
default:
throw new Error(`Page Action does not support ${message.action}`);
}
}
function handleBrowserActionMessage(message, tabId) {
switch (message.action) {
case "enable":
browser.browserAction.enable(tabId);
break;
case "disable":
browser.browserAction.disable(tabId);
break;
case "setBadgeText":
browser.browserAction.setBadgeText({
tabId,
text: message.text,
});
break;
case "setBadgeTextColor":
browser.browserAction.setBadgeTextColor({
tabId,
color: message.color,
});
break;
case "setBadgeBackgroundColor":
browser.browserAction.setBadgeBackgroundColor({
tabId,
color: message.color,
});
break;
case "setPopup":
browser.browserAction.setPopup({
tabId,
popup: message.popup,
});
break;
case "setTitle":
browser.browserAction.setTitle({
tabId,
title: message.title,
});
break;
case "setIcon":
browser.browserAction.setIcon({
tabId,
imageData: message.imageData,
path: message.path,
});
break;
default:
throw new Error(`Browser Action does not support ${message.action}`);
}
}
function handleMessage(message, tabId) {
switch (message.type) {
case "ping":
port.postMessage({ method: "pong" });
return;
case "load":
browser.tabs.update(tabId, {
url: message.url,
});
return;
case "browserAction":
handleBrowserActionMessage(message, tabId);
return;
case "pageAction":
handlePageActionMessage(message, tabId);
return;
default:
throw new Error(`Unsupported message type ${message.type}`);
}
}

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.0 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 225 B

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 225 B

Просмотреть файл

@ -0,0 +1 @@
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 500 500" height="500px" id="Layer_1" version="1.1" viewBox="0 0 500 500" width="500px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path clip-rule="evenodd" d="M131.889,150.061v63.597h-27.256 c-20.079,0-36.343,16.263-36.343,36.342v181.711c0,20.078,16.264,36.34,36.343,36.34h290.734c20.078,0,36.345-16.262,36.345-36.34 V250c0-20.079-16.267-36.342-36.345-36.342h-27.254v-63.597c0-65.232-52.882-118.111-118.112-118.111 S131.889,84.828,131.889,150.061z M177.317,213.658v-63.597c0-40.157,32.525-72.685,72.683-72.685 c40.158,0,72.685,32.528,72.685,72.685v63.597H177.317z M213.658,313.599c0-20.078,16.263-36.341,36.342-36.341 s36.341,16.263,36.341,36.341c0,12.812-6.634,24.079-16.625,30.529c0,0,3.55,21.446,7.542,46.699 c0,7.538-6.087,13.625-13.629,13.625h-27.258c-7.541,0-13.627-6.087-13.627-13.625l7.542-46.699 C220.294,337.678,213.658,326.41,213.658,313.599z" fill="#010101" fill-rule="evenodd"/></svg>

После

Ширина:  |  Высота:  |  Размер: 1.1 KiB

Просмотреть файл

@ -0,0 +1,4 @@
const port = browser.runtime.connectNative("browser");
port.onMessage.addListener(message => {
browser.runtime.sendMessage(message);
});

Просмотреть файл

@ -0,0 +1,30 @@
{
"manifest_version": 2,
"name": "actions",
"version": "1.0",
"description": "Defines Page and Browser actions",
"browser_action": {
"default_title": "Test action default"
},
"page_action": {
"default_title": "Test action default",
"default_icon": {
"19": "button/geo-19.png",
"38": "button/geo-38.png"
}
},
"background": {
"scripts": ["background.js"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
],
"permissions": [
"tabs",
"geckoViewAddons",
"nativeMessaging"
]
}

Просмотреть файл

@ -0,0 +1,10 @@
<html>
<head>
<script type="text/javascript" src="test-open-popup-browser-action.js"></script>
</head>
<body>
<body style="height: 100%">
<p>Hello, world!</p>
</body>
</body>
</html>

Просмотреть файл

@ -0,0 +1,7 @@
window.addEventListener("DOMContentLoaded", init);
function init() {
document.body.addEventListener("click", event => {
browser.browserAction.openPopup();
});
}

Просмотреть файл

@ -0,0 +1,10 @@
<html>
<head>
<script type="text/javascript" src="test-open-popup-page-action.js"></script>
</head>
<body>
<body style="height: 100%">
<p>Hello, world!</p>
</body>
</body>
</html>

Просмотреть файл

@ -0,0 +1,7 @@
window.addEventListener("DOMContentLoaded", init);
function init() {
document.body.addEventListener("click", event => {
browser.pageAction.openPopup();
});
}

Просмотреть файл

@ -0,0 +1 @@
<h1> HELLO </h1>

Просмотреть файл

@ -0,0 +1 @@
<?xml version="1.0" ?><!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg enable-background="new 0 0 500 500" height="500px" id="Layer_1" version="1.1" viewBox="0 0 500 500" width="500px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path clip-rule="evenodd" d="M131.889,150.061v63.597h-27.256 c-20.079,0-36.343,16.263-36.343,36.342v181.711c0,20.078,16.264,36.34,36.343,36.34h290.734c20.078,0,36.345-16.262,36.345-36.34 V250c0-20.079-16.267-36.342-36.345-36.342h-27.254v-63.597c0-65.232-52.882-118.111-118.112-118.111 S131.889,84.828,131.889,150.061z M177.317,213.658v-63.597c0-40.157,32.525-72.685,72.683-72.685 c40.158,0,72.685,32.528,72.685,72.685v63.597H177.317z M213.658,313.599c0-20.078,16.263-36.341,36.342-36.341 s36.341,16.263,36.341,36.341c0,12.812-6.634,24.079-16.625,30.529c0,0,3.55,21.446,7.542,46.699 c0,7.538-6.087,13.625-13.629,13.625h-27.258c-7.541,0-13.627-6.087-13.627-13.625l7.542-46.699 C220.294,337.678,213.658,326.41,213.658,313.599z" fill="#010101" fill-rule="evenodd"/></svg>

После

Ширина:  |  Высота:  |  Размер: 1.1 KiB

Просмотреть файл

@ -0,0 +1,540 @@
package org.mozilla.geckoview.test
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.support.test.InstrumentationRegistry
import android.support.test.filters.MediumTest
import org.hamcrest.Matchers.equalTo
import org.json.JSONObject
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeThat
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.mozilla.geckoview.GeckoResult
import org.mozilla.geckoview.GeckoSession
import org.mozilla.geckoview.WebExtension
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule
@MediumTest
@RunWith(Parameterized::class)
class ExtensionActionTest : BaseSessionTest() {
var extension: WebExtension? = null
var default: WebExtension.Action? = null
var backgroundPort: WebExtension.Port? = null
var windowPort: WebExtension.Port? = null
companion object {
@get:Parameterized.Parameters(name = "{0}")
@JvmStatic
val parameters: List<Array<out Any>> = listOf(
arrayOf("#pageAction"),
arrayOf("#browserAction"))
}
@field:Parameterized.Parameter(0) @JvmField var id: String = ""
@Before
fun setup() {
// This method installs the extension, opens up ports with the background script and the
// content script and captures the default action definition from the manifest
val browserActionDefaultResult = GeckoResult<WebExtension.Action>()
val pageActionDefaultResult = GeckoResult<WebExtension.Action>()
val windowPortResult = GeckoResult<WebExtension.Port>()
val backgroundPortResult = GeckoResult<WebExtension.Port>()
extension = WebExtension("resource://android/assets/web_extensions/actions/",
"actions", WebExtension.Flags.ALLOW_CONTENT_MESSAGING)
sessionRule.session.setMessageDelegate(
extension!!,
object : WebExtension.MessageDelegate {
override fun onConnect(port: WebExtension.Port) {
windowPortResult.complete(port)
}
}, "browser")
extension!!.setMessageDelegate(object : WebExtension.MessageDelegate {
override fun onConnect(port: WebExtension.Port) {
backgroundPortResult.complete(port)
}
}, "browser")
sessionRule.addExternalDelegateDuringNextWait(
WebExtension.ActionDelegate::class,
extension!!::setActionDelegate,
{ extension!!.setActionDelegate(null) },
object : WebExtension.ActionDelegate {
override fun onBrowserAction(extension: WebExtension, session: GeckoSession?, action: WebExtension.Action) {
assertEquals(action.title, "Test action default")
browserActionDefaultResult.complete(action)
}
override fun onPageAction(extension: WebExtension, session: GeckoSession?, action: WebExtension.Action) {
assertEquals(action.title, "Test action default")
pageActionDefaultResult.complete(action)
}
})
sessionRule.waitForResult(sessionRule.runtime.registerWebExtension(extension!!))
sessionRule.session.loadUri("http://example.com")
sessionRule.waitForPageStop()
default = when (id) {
"#pageAction" -> sessionRule.waitForResult(pageActionDefaultResult)
"#browserAction" -> sessionRule.waitForResult(browserActionDefaultResult)
else -> throw IllegalArgumentException()
}
windowPort = sessionRule.waitForResult(windowPortResult)
backgroundPort = sessionRule.waitForResult(backgroundPortResult)
if (id == "#pageAction") {
// Make sure that the pageAction starts enabled for this tab
testActionApi("""{"action": "enable"}""") { action ->
assertEquals(action.enabled, true)
}
}
}
private var type: String = ""
get() = when(id) {
"#pageAction" -> "pageAction"
"#browserAction" -> "browserAction"
else -> throw IllegalArgumentException()
}
@After
fun tearDown() {
sessionRule.waitForResult(sessionRule.runtime.unregisterWebExtension(extension!!))
}
private fun testBackgroundActionApi(message: String, tester: (WebExtension.Action) -> Unit) {
val result = GeckoResult<Void>()
val json = JSONObject(message)
json.put("type", type)
backgroundPort!!.postMessage(json)
sessionRule.addExternalDelegateDuringNextWait(
WebExtension.ActionDelegate::class,
extension!!::setActionDelegate,
{ extension!!.setActionDelegate(null) },
object : WebExtension.ActionDelegate {
override fun onBrowserAction(extension: WebExtension, session: GeckoSession?, action: WebExtension.Action) {
assertEquals(id, "#browserAction")
default = action
tester(action)
result.complete(null)
}
override fun onPageAction(extension: WebExtension, session: GeckoSession?, action: WebExtension.Action) {
assertEquals(id, "#pageAction")
default = action
tester(action)
result.complete(null)
}
})
sessionRule.waitForResult(result)
}
private fun testActionApi(message: String, tester: (WebExtension.Action) -> Unit) {
val result = GeckoResult<Void>()
val json = JSONObject(message)
json.put("type", type)
windowPort!!.postMessage(json)
sessionRule.addExternalDelegateDuringNextWait(
WebExtension.ActionDelegate::class,
{ delegate ->
sessionRule.session.setWebExtensionActionDelegate(extension!!, delegate) },
{ sessionRule.session.setWebExtensionActionDelegate(extension!!, null) },
object : WebExtension.ActionDelegate {
override fun onBrowserAction(extension: WebExtension, session: GeckoSession?, action: WebExtension.Action) {
assertEquals(id, "#browserAction")
val resolved = action.withDefault(default!!)
tester(resolved)
result.complete(null)
}
override fun onPageAction(extension: WebExtension, session: GeckoSession?, action: WebExtension.Action) {
assertEquals(id, "#pageAction")
val resolved = action.withDefault(default!!)
tester(resolved)
result.complete(null)
}
})
sessionRule.waitForResult(result)
}
@Test
fun disableTest() {
testActionApi("""{"action": "disable"}""") { action ->
assertEquals(action.title, "Test action default")
assertEquals(action.enabled, false)
}
}
@Test
fun enableTest() {
// First, make sure the action is disabled
testActionApi("""{"action": "disable"}""") { action ->
assertEquals(action.title, "Test action default")
assertEquals(action.enabled, false)
}
testActionApi("""{"action": "enable"}""") { action ->
assertEquals(action.title, "Test action default")
assertEquals(action.enabled, true)
}
}
@Test
fun setOverridenTitle() {
testActionApi("""{
"action": "setTitle",
"title": "overridden title"
}""") { action ->
assertEquals(action.title, "overridden title")
assertEquals(action.enabled, true)
}
}
@Test
fun setBadgeText() {
assumeThat("Only browserAction supports this API.", id, equalTo("#browserAction"))
testActionApi("""{
"action": "setBadgeText",
"text": "12"
}""") { action ->
assertEquals(action.title, "Test action default")
assertEquals(action.badgeText, "12")
assertEquals(action.enabled, true)
}
}
@Test
fun setBadgeBackgroundColor() {
assumeThat("Only browserAction supports this API.", id, equalTo("#browserAction"))
colorTest("setBadgeBackgroundColor", "#ABCDEF", "#FFABCDEF")
colorTest("setBadgeBackgroundColor", "#F0A", "#FFFF00AA")
colorTest("setBadgeBackgroundColor", "red", "#FFFF0000")
colorTest("setBadgeBackgroundColor", "rgb(0, 0, 255)", "#FF0000FF")
colorTest("setBadgeBackgroundColor", "rgba(0, 255, 0, 0.5)", "#8000FF00")
colorRawTest("setBadgeBackgroundColor", "[0, 0, 255, 128]", "#800000FF")
}
private fun colorTest(actionName: String, color: String, expectedHex: String) {
colorRawTest(actionName, "\"$color\"", expectedHex)
}
private fun colorRawTest(actionName: String, color: String, expectedHex: String) {
testActionApi("""{
"action": "$actionName",
"color": $color
}""") { action ->
assertEquals(action.title, "Test action default")
assertEquals(action.badgeText, "")
assertEquals(action.enabled, true)
val result = when (actionName) {
"setBadgeTextColor" -> action.badgeTextColor!!
"setBadgeBackgroundColor" -> action.badgeBackgroundColor!!
else -> throw IllegalArgumentException()
}
val hexColor = String.format("#%08X", result)
assertEquals(hexColor, "$expectedHex")
}
}
@Test
fun setBadgeTextColor() {
assumeThat("Only browserAction supports this API.", id, equalTo("#browserAction"))
colorTest("setBadgeTextColor", "#ABCDEF", "#FFABCDEF")
colorTest("setBadgeTextColor", "#F0A", "#FFFF00AA")
colorTest("setBadgeTextColor", "red", "#FFFF0000")
colorTest("setBadgeTextColor", "rgb(0, 0, 255)", "#FF0000FF")
colorTest("setBadgeTextColor", "rgba(0, 255, 0, 0.5)", "#8000FF00")
colorRawTest("setBadgeTextColor", "[0, 0, 255, 128]", "#800000FF")
}
@Test
fun setDefaultTitle() {
assumeThat("Only browserAction supports default properties.", id, equalTo("#browserAction"))
// Setting a default value will trigger the default handler on the extension object
testBackgroundActionApi("""{
"action": "setTitle",
"title": "new default title"
}""") { action ->
assertEquals(action.title, "new default title")
assertEquals(action.badgeText, "")
assertEquals(action.enabled, true)
}
// When an overridden title is set, the default has no effect
testActionApi("""{
"action": "setTitle",
"title": "test override"
}""") { action ->
assertEquals(action.title, "test override")
assertEquals(action.badgeText, "")
assertEquals(action.enabled, true)
}
// When the override is null, the new default takes effect
testActionApi("""{
"action": "setTitle",
"title": null
}""") { action ->
assertEquals(action.title, "new default title")
assertEquals(action.badgeText, "")
assertEquals(action.enabled, true)
}
// When the default value is null, the manifest value is used
testBackgroundActionApi("""{
"action": "setTitle",
"title": null
}""") { action ->
assertEquals(action.title, "Test action default")
assertEquals(action.badgeText, "")
assertEquals(action.enabled, true)
}
}
private fun compareBitmap(expectedLocation: String, actual: Bitmap) {
val stream = InstrumentationRegistry.getTargetContext().assets
.open(expectedLocation)
val expected = BitmapFactory.decodeStream(stream)
for (x in 0 until actual.height) {
for (y in 0 until actual.width) {
assertEquals(expected.getPixel(x, y), actual.getPixel(x, y))
}
}
}
@Test
fun setIconSvg() {
val svg = GeckoResult<Void>()
testActionApi("""{
"action": "setIcon",
"path": "button/icon.svg"
}""") { action ->
assertEquals(action.title, "Test action default")
assertEquals(action.enabled, true)
action.icon!!.get(100).accept { actual ->
compareBitmap("web_extensions/actions/button/expected.png", actual!!)
svg.complete(null)
}
}
sessionRule.waitForResult(svg)
}
@Test
fun setIconPng() {
val png100 = GeckoResult<Void>()
val png38 = GeckoResult<Void>()
val png19 = GeckoResult<Void>()
val png10 = GeckoResult<Void>()
testActionApi("""{
"action": "setIcon",
"path": {
"19": "button/geo-19.png",
"38": "button/geo-38.png"
}
}""") { action ->
assertEquals(action.title, "Test action default")
assertEquals(action.enabled, true)
action.icon!!.get(100).accept { actual ->
compareBitmap("web_extensions/actions/button/geo-38.png", actual!!)
png100.complete(null)
}
action.icon!!.get(38).accept { actual ->
compareBitmap("web_extensions/actions/button/geo-38.png", actual!!)
png38.complete(null)
}
action.icon!!.get(19).accept { actual ->
compareBitmap("web_extensions/actions/button/geo-19.png", actual!!)
png19.complete(null)
}
action.icon!!.get(10).accept { actual ->
compareBitmap("web_extensions/actions/button/geo-19.png", actual!!)
png10.complete(null)
}
}
sessionRule.waitForResult(png100)
sessionRule.waitForResult(png38)
sessionRule.waitForResult(png19)
sessionRule.waitForResult(png10)
}
@Test
fun setIconError() {
val error = GeckoResult<Void>()
testActionApi("""{
"action": "setIcon",
"path": "invalid/path/image.png"
}""") { action ->
action.icon!!.get(38).accept({
error.completeExceptionally(RuntimeException("Should not succeed."))
}, { exception ->
assertTrue(exception is IllegalArgumentException)
error.complete(null)
})
}
sessionRule.waitForResult(error)
}
@Test
@GeckoSessionTestRule.WithDisplay(width=100, height=100)
@Ignore // this test fails intermittently on try :(
fun testOpenPopup() {
// First, let's make sure we have a popup set
val actionResult = GeckoResult<Void>()
testActionApi("""{
"action": "setPopup",
"popup": "test-popup.html"
}""") { action ->
assertEquals(action.title, "Test action default")
assertEquals(action.enabled, true)
actionResult.complete(null)
}
val url = when(id) {
"#browserAction" -> "/test-open-popup-browser-action.html"
"#pageAction" -> "/test-open-popup-page-action.html"
else -> throw IllegalArgumentException()
}
windowPort!!.postMessage(JSONObject("""{
"type": "load",
"url": "$url"
}"""))
val openPopup = GeckoResult<Void>()
sessionRule.session.setWebExtensionActionDelegate(extension!!,
object : WebExtension.ActionDelegate {
override fun onOpenPopup(extension: WebExtension,
popupAction: WebExtension.Action): GeckoResult<GeckoSession>? {
assertEquals(extension, this@ExtensionActionTest.extension)
// assertEquals(popupAction, this@ExtensionActionTest.default)
openPopup.complete(null)
return null
}
})
sessionRule.waitForPageStops(2)
// openPopup needs user activation
sessionRule.session.synthesizeTap(50, 50)
sessionRule.waitForResult(openPopup)
}
@Test
fun testClickWhenPopupIsNotDefined() {
val pong = GeckoResult<Void>()
backgroundPort!!.setDelegate(object : WebExtension.PortDelegate {
override fun onPortMessage(message: Any, port: WebExtension.Port) {
val json = message as JSONObject
if (json.getString("method") == "pong") {
pong.complete(null)
} else {
// We should NOT receive onClicked here
pong.completeExceptionally(IllegalArgumentException(
"Received unexpected: ${json.getString("method")}"))
}
}
})
val actionResult = GeckoResult<WebExtension.Action>()
testActionApi("""{
"action": "setPopup",
"popup": "test-popup.html"
}""") { action ->
assertEquals(action.title, "Test action default")
assertEquals(action.enabled, true)
actionResult.complete(action)
}
val togglePopup = GeckoResult<Void>()
val action = sessionRule.waitForResult(actionResult)
extension!!.setActionDelegate(object : WebExtension.ActionDelegate {
override fun onTogglePopup(extension: WebExtension,
popupAction: WebExtension.Action): GeckoResult<GeckoSession>? {
assertEquals(extension, this@ExtensionActionTest.extension)
assertEquals(popupAction, action)
togglePopup.complete(null)
return null
}
})
// This click() will not cause an onClicked callback because popup is set
action.click()
// but it will cause togglePopup to be called
sessionRule.waitForResult(togglePopup)
// If the response to ping reaches us before the onClicked we know onClicked wasn't called
backgroundPort!!.postMessage(JSONObject("""{
"type": "ping"
}"""))
sessionRule.waitForResult(pong)
}
@Test
fun testClickWhenPopupIsDefined() {
val onClicked = GeckoResult<Void>()
backgroundPort!!.setDelegate(object : WebExtension.PortDelegate {
override fun onPortMessage(message: Any, port: WebExtension.Port) {
val json = message as JSONObject
assertEquals(json.getString("method"), "onClicked")
assertEquals(json.getString("type"), type)
onClicked.complete(null)
}
})
testActionApi("""{
"action": "setPopup",
"popup": null
}""") { action ->
assertEquals(action.title, "Test action default")
assertEquals(action.enabled, true)
// This click() WILL cause an onClicked callback
action.click()
}
sessionRule.waitForResult(onClicked)
}
}

Просмотреть файл

@ -363,9 +363,11 @@ public class GeckoSession implements Parcelable {
private final class WebExtensionListener implements BundleEventListener {
final private HashMap<WebExtensionSender, WebExtension.MessageDelegate> mMessageDelegates;
final private HashMap<String, WebExtension.ActionDelegate> mActionDelegates;
public WebExtensionListener() {
mMessageDelegates = new HashMap<>();
mActionDelegates = new HashMap<>();
}
/* package */ void registerListeners() {
@ -374,17 +376,32 @@ public class GeckoSession implements Parcelable {
"GeckoView:WebExtension:PortMessage",
"GeckoView:WebExtension:Connect",
"GeckoView:WebExtension:CloseTab",
// Browser and Page Actions
"GeckoView:BrowserAction:Update",
"GeckoView:BrowserAction:OpenPopup",
"GeckoView:PageAction:Update",
"GeckoView:PageAction:OpenPopup",
null);
}
public void setDelegate(final WebExtension webExtension,
public void setActionDelegate(final WebExtension webExtension,
final WebExtension.ActionDelegate delegate) {
mActionDelegates.put(webExtension.id, delegate);
}
public WebExtension.ActionDelegate getActionDelegate(final WebExtension webExtension) {
return mActionDelegates.get(webExtension.id);
}
public void setMessageDelegate(final WebExtension webExtension,
final WebExtension.MessageDelegate delegate,
final String nativeApp) {
mMessageDelegates.put(new WebExtensionSender(webExtension.id, nativeApp), delegate);
}
public WebExtension.MessageDelegate getDelegate(final WebExtension webExtension,
final String nativeApp) {
public WebExtension.MessageDelegate getMessageDelegate(final WebExtension webExtension,
final String nativeApp) {
return mMessageDelegates.get(new WebExtensionSender(webExtension.id, nativeApp));
}
@ -397,11 +414,17 @@ public class GeckoSession implements Parcelable {
if ("GeckoView:WebExtension:Message".equals(event)
|| "GeckoView:WebExtension:PortMessage".equals(event)
|| "GeckoView:WebExtension:Connect".equals(event)) {
|| "GeckoView:WebExtension:Connect".equals(event)
|| "GeckoView:PageAction:Update".equals(event)
|| "GeckoView:PageAction:OpenPopup".equals(event)
|| "GeckoView:BrowserAction:Update".equals(event)
|| "GeckoView:BrowserAction:OpenPopup".equals(event)) {
mWindow.runtime.getWebExtensionDispatcher()
.handleMessage(event, message, callback, GeckoSession.this);
return;
} else if ("GeckoView:WebExtension:CloseTab".equals(event)) {
mWindow.runtime.getWebExtensionController().closeTab(message, callback, GeckoSession.this);
return;
}
}
}
@ -421,7 +444,7 @@ public class GeckoSession implements Parcelable {
public @Nullable WebExtension.MessageDelegate getMessageDelegate(
final @NonNull WebExtension webExtension,
final @NonNull String nativeApp) {
return mWebExtensionListener.getDelegate(webExtension, nativeApp);
return mWebExtensionListener.getMessageDelegate(webExtension, nativeApp);
}
/**
@ -450,7 +473,41 @@ public class GeckoSession implements Parcelable {
public void setMessageDelegate(final @NonNull WebExtension webExtension,
final @Nullable WebExtension.MessageDelegate delegate,
final @NonNull String nativeApp) {
mWebExtensionListener.setDelegate(webExtension, delegate, nativeApp);
mWebExtensionListener.setMessageDelegate(webExtension, delegate, nativeApp);
}
/**
* Set the Action delegate for this session.
*
* This delegate will receive page and browser action overrides specific to
* this session. The default Action will be received by the delegate set
* by {@link WebExtension#setActionDelegate}.
*
* @param webExtension the {@link WebExtension} object this delegate will
* receive updates for
* @param delegate the {@link WebExtension.ActionDelegate} that will
* receive updates.
* @see WebExtension.Action
*/
@AnyThread
public void setWebExtensionActionDelegate(final @NonNull WebExtension webExtension,
final @Nullable WebExtension.ActionDelegate delegate) {
mWebExtensionListener.setActionDelegate(webExtension, delegate);
}
/**
* Get the Action delegate for this session.
*
* @param webExtension {@link WebExtension} that this delegates receive
* updates for.
* @return {@link WebExtension.ActionDelegate} for this
* session
*/
@AnyThread
@Nullable
public WebExtension.ActionDelegate getWebExtensionActionDelegate(
final @NonNull WebExtension webExtension) {
return mWebExtensionListener.getActionDelegate(webExtension);
}
private final GeckoSessionHandler<ContentDelegate> mContentHandler =

Просмотреть файл

@ -1,5 +1,8 @@
package org.mozilla.geckoview;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.support.annotation.AnyThread;
import android.support.annotation.IntDef;
import android.support.annotation.LongDef;
import android.support.annotation.NonNull;
@ -15,7 +18,10 @@ import org.mozilla.gecko.util.GeckoBundle;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@ -50,6 +56,8 @@ public class WebExtension {
*/
/* package */ final @NonNull Map<String, MessageDelegate> messageDelegates;
/* package */ @NonNull ActionDelegate actionDelegate;
@Override
public String toString() {
return "WebExtension {" +
@ -473,4 +481,455 @@ public class WebExtension {
return this.isTopLevel;
}
}
/**
* Represents the Icon for a {@link Action}.
*/
public static class ActionIcon {
private Map<Integer, String> mIconUris;
/**
* Get the best version of this icon for size <code>pixelSize</code>.
*
* Embedders are encouraged to cache the result of this method keyed with this instance.
*
* @param pixelSize pixel size at which this icon will be displayed at.
*
* @return A {@link GeckoResult} that resolves to the bitmap when ready.
*/
@AnyThread
@NonNull
public GeckoResult<Bitmap> get(final int pixelSize) {
int size;
if (mIconUris.containsKey(pixelSize)) {
// If this size matches exactly, return it
size = pixelSize;
} else {
// Otherwise, find the smallest larger image (or the largest image if they are all
// smaller)
List<Integer> sizes = new ArrayList<>();
sizes.addAll(mIconUris.keySet());
Collections.sort(sizes, (a, b) -> Integer.compare(b - pixelSize, a - pixelSize));
size = sizes.get(0);
}
final String uri = mIconUris.get(size);
return ImageDecoder.instance().decode(uri, pixelSize);
}
/* package */ ActionIcon(final GeckoBundle bundle) {
mIconUris = new HashMap<>();
for (final String key: bundle.keys()) {
final Integer intKey = Integer.valueOf(key);
if (intKey == null) {
Log.e(LOGTAG, "Non-integer icon key: " + intKey);
if (BuildConfig.DEBUG) {
throw new RuntimeException("Non-integer icon key: " + key);
}
continue;
}
mIconUris.put(intKey, bundle.getString(key));
}
}
/** Override for tests. */
protected ActionIcon() {
mIconUris = null;
}
@Override
public boolean equals(final Object o) {
if (o == this) {
return true;
}
if (!(o instanceof ActionIcon)) {
return false;
}
return mIconUris.equals(((ActionIcon) o).mIconUris);
}
@Override
public int hashCode() {
return mIconUris.hashCode();
}
}
/**
* Represents either a Browser Action or a Page Action from the
* WebExtension API.
*
* Instances of this class may represent the default <code>Action</code>
* which applies to all WebExtension tabs or a tab-specific override. To
* reconstruct the full <code>Action</code> object, you can use
* {@link Action#withDefault}.
*
* Tab specific overrides can be obtained by registering a delegate using
* {@link GeckoSession#setWebExtensionActionDelegate}, while default values
* can be obtained by registering a delegate using
* {@link #setActionDelegate}.
*
* <br>
* See also
* <ul>
* <li><a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction">
* WebExtensions/API/browserAction
* </a></li>
* <li><a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction">
* WebExtensions/API/pageAction
* </a></li>
* </ul>
*/
@AnyThread
public static class Action {
/**
* Title of this Action.
*
* See also:
* <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction/getTitle">
* pageAction/getTitle</a>,
* <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/getTitle">
* browserAction/getTitle</a>
*/
final public @Nullable String title;
/**
* Icon for this Action.
*
* See also:
* <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction/setIcon">
* pageAction/setIcon</a>,
* <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/setIcon">
* browserAction/setIcon</a>
*/
final public @Nullable ActionIcon icon;
/**
* URI of the Popup to display when the user taps on the icon for this
* Action.
*
* See also:
* <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction/getPopup">
* pageAction/getPopup</a>,
* <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/getPopup">
* browserAction/getPopup</a>
*/
final private @Nullable String mPopupUri;
/**
* Whether this action is enabled and should be visible.
*
* Note: for page action, this is <code>true</code> when the extension calls
* <code>pageAction.show</code> and <code>false</code> when the extension
* calls <code>pageAction.hide</code>.
*
* See also:
* <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction/show">
* pageAction/show</a>,
* <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/enabled">
* browserAction/enabled</a>
*/
final public @Nullable Boolean enabled;
/**
* Badge text for this action.
*
* See also:
* <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/getBadgeText">
* browserAction/getBadgeText</a>
*/
final public @Nullable String badgeText;
/**
* Background color for the badge for this Action.
*
* This method will return an Android color int that can be used in
* {@link android.widget.TextView#setBackgroundColor(int)} and similar
* methods.
*
* See also:
* <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/getBadgeBackgroundColor">
* browserAction/getBadgeBackgroundColor</a>
*/
final public @Nullable Integer badgeBackgroundColor;
/**
* Text color for the badge for this Action.
*
* This method will return an Android color int that can be used in
* {@link android.widget.TextView#setTextColor(int)} and similar
* methods.
*
* See also:
* <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction/getBadgeTextColor">
* browserAction/getBadgeTextColor</a>
*/
final public @Nullable Integer badgeTextColor;
final private WebExtension mExtension;
/* package */ final static int TYPE_BROWSER_ACTION = 1;
/* package */ final static int TYPE_PAGE_ACTION = 2;
@Retention(RetentionPolicy.SOURCE)
@IntDef({TYPE_BROWSER_ACTION, TYPE_PAGE_ACTION})
/* package */ @interface ActionType {}
/* package */ final @ActionType int type;
/* package */ Action(final @ActionType int type,
final GeckoBundle bundle, final WebExtension extension) {
mExtension = extension;
mPopupUri = bundle.getString("popup");
this.type = type;
title = bundle.getString("title");
badgeText = bundle.getString("badgeText");
badgeBackgroundColor = colorFromRgbaArray(
bundle.getDoubleArray("badgeBackgroundColor"));
badgeTextColor = colorFromRgbaArray(
bundle.getDoubleArray("badgeTextColor"));
if (bundle.containsKey("icon")) {
icon = new ActionIcon(bundle.getBundle("icon"));
} else {
icon = null;
}
if (bundle.getBoolean("patternMatching", false)) {
// This action was enabled by pattern matching
enabled = true;
} else if (bundle.containsKey("enabled")) {
enabled = bundle.getBoolean("enabled");
} else {
enabled = null;
}
}
private Integer colorFromRgbaArray(final double[] c) {
if (c == null) {
return null;
}
return Color.argb((int) c[3], (int) c[0], (int) c[1], (int) c[2]);
}
@Override
public String toString() {
return "Action {\n"
+ "\ttitle: " + this.title + ",\n"
+ "\ticon: " + this.icon + ",\n"
+ "\tpopupUri: " + this.mPopupUri + ",\n"
+ "\tenabled: " + this.enabled + ",\n"
+ "\tbadgeText: " + this.badgeText + ",\n"
+ "\tbadgeTextColor: " + this.badgeTextColor + ",\n"
+ "\tbadgeBackgroundColor: " + this.badgeBackgroundColor + ",\n"
+ "}";
}
// For testing
protected Action() {
type = TYPE_BROWSER_ACTION;
mExtension = null;
mPopupUri = null;
title = null;
icon = null;
enabled = null;
badgeText = null;
badgeTextColor = null;
badgeBackgroundColor = null;
}
/**
* Merges values from this Action with the default Action.
*
* @param defaultValue the default Action as received from
* {@link ActionDelegate#onBrowserAction}
* or {@link ActionDelegate#onPageAction}.
*
* @return an {@link Action} where all <code>null</code> values from
* this instance are replaced with values from
* <code>defaultValue</code>.
* @throws IllegalArgumentException if defaultValue is not of the same
* type, e.g. if this Action is a Page Action and default
* value is a Browser Action.
*/
@NonNull
public Action withDefault(final @NonNull Action defaultValue) {
return new Action(this, defaultValue);
}
/** @see Action#withDefault */
private Action(final Action source, final Action defaultValue) {
if (source.type != defaultValue.type) {
throw new IllegalArgumentException(
"defaultValue must be of the same type.");
}
type = source.type;
mExtension = source.mExtension;
title = source.title != null ? source.title : defaultValue.title;
icon = source.icon != null ? source.icon : defaultValue.icon;
mPopupUri = source.mPopupUri != null ? source.mPopupUri : defaultValue.mPopupUri;
enabled = source.enabled != null ? source.enabled : defaultValue.enabled;
badgeText = source.badgeText != null ? source.badgeText : defaultValue.badgeText;
badgeTextColor = source.badgeTextColor != null
? source.badgeTextColor : defaultValue.badgeTextColor;
badgeBackgroundColor = source.badgeBackgroundColor != null
? source.badgeBackgroundColor : defaultValue.badgeBackgroundColor;
}
@UiThread
public void click() {
if (mPopupUri != null && !mPopupUri.isEmpty()) {
if (mExtension.actionDelegate == null) {
return;
}
GeckoResult<GeckoSession> popup =
mExtension.actionDelegate.onTogglePopup(mExtension, this);
openPopup(popup);
// When popupUri is specified, the extension doesn't get a callback
return;
}
final GeckoBundle bundle = new GeckoBundle(1);
bundle.putString("extensionId", mExtension.id);
if (type == TYPE_BROWSER_ACTION) {
EventDispatcher.getInstance().dispatch(
"GeckoView:BrowserAction:Click", bundle);
} else if (type == TYPE_PAGE_ACTION) {
EventDispatcher.getInstance().dispatch(
"GeckoView:PageAction:Click", bundle);
} else {
throw new IllegalStateException("Unknown Action type");
}
}
/* package */ void openPopup(final GeckoResult<GeckoSession> popup) {
if (popup == null) {
return;
}
popup.accept(session -> {
if (session == null) {
return;
}
session.getSettings().setIsPopup(true);
session.loadUri(mPopupUri);
});
}
}
/**
* Receives updates whenever a Browser action or a Page action has been
* defined by an extension.
*
* This delegate will receive the default action when registered with
* {@link WebExtension#setActionDelegate}. To receive
* {@link GeckoSession}-specific overrides you can use
* {@link GeckoSession#setWebExtensionActionDelegate}.
*/
public interface ActionDelegate {
/**
* Called whenever a browser action is defined or updated.
*
* This method will be called whenever an extension that defines a
* browser action is registered or the properties of the Action are
* updated.
*
* See also <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/browserAction">
* WebExtensions/API/browserAction
* </a>,
* <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_action">
* WebExtensions/manifest.json/browser_action
* </a>.
*
* @param extension The extension that defined this browser action.
* @param session Either the {@link GeckoSession} corresponding to the
* tab to which this Action override applies.
* <code>null</code> if <code>action</code> is the new
* default value.
* @param action {@link Action} containing the override values for this
* {@link GeckoSession} or the default value if
* <code>session</code> is <code>null</code>.
*/
@UiThread
default void onBrowserAction(final @NonNull WebExtension extension,
final @Nullable GeckoSession session,
final @NonNull Action action) {}
/**
* Called whenever a page action is defined or updated.
*
* This method will be called whenever an extension that defines a page
* action is registered or the properties of the Action are updated.
*
* See also <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/pageAction">
* WebExtensions/API/pageAction
* </a>,
* <a target=_blank href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/page_action">
* WebExtensions/manifest.json/page_action
* </a>.
*
* @param extension The extension that defined this page action.
* @param session Either the {@link GeckoSession} corresponding to the
* tab to which this Action override applies.
* <code>null</code> if <code>action</code> is the new
* default value.
* @param action {@link Action} containing the override values for this
* {@link GeckoSession} or the default value if
* <code>session</code> is <code>null</code>.
*/
@UiThread
default void onPageAction(final @NonNull WebExtension extension,
final @Nullable GeckoSession session,
final @NonNull Action action) {}
/**
* Called whenever the action wants to toggle a popup view.
*
* @param extension The extension that wants to display a popup
* @param action The action where the popup is defined
* @return A GeckoSession that will be used to display the pop-up,
* null if no popup will be displayed.
*/
@UiThread
@Nullable
default GeckoResult<GeckoSession> onTogglePopup(final @NonNull WebExtension extension,
final @NonNull Action action) {
return null;
}
/**
* Called whenever the action wants to open a popup view.
*
* @param extension The extension that wants to display a popup
* @param action The action where the popup is defined
* @return A GeckoSession that will be used to display the pop-up,
* null if no popup will be displayed.
*/
@UiThread
@Nullable
default GeckoResult<GeckoSession> onOpenPopup(final @NonNull WebExtension extension,
final @NonNull Action action) {
return null;
}
}
/**
* Set the Action delegate for this WebExtension.
*
* This delegate will receive updates every time the default Action value
* changes.
*
* To listen for {@link GeckoSession}-specific updates, use
* {@link GeckoSession#setWebExtensionActionDelegate}
*
* @param delegate {@link ActionDelegate} that will receive updates.
*/
@AnyThread
public void setActionDelegate(final @Nullable ActionDelegate delegate) {
actionDelegate = delegate;
}
}

Просмотреть файл

@ -29,7 +29,13 @@ import java.util.Map;
"GeckoView:WebExtension:Message",
"GeckoView:WebExtension:PortMessage",
"GeckoView:WebExtension:Connect",
"GeckoView:WebExtension:Disconnect"
"GeckoView:WebExtension:Disconnect",
// {Browser,Page}Actions
"GeckoView:BrowserAction:Update",
"GeckoView:BrowserAction:OpenPopup",
"GeckoView:PageAction:Update",
"GeckoView:PageAction:OpenPopup"
);
mHandlerRegistered = true;
}
@ -231,6 +237,69 @@ import java.util.Map;
exception -> callback.sendError(exception));
}
private WebExtension extensionFromBundle(final GeckoBundle message) {
final String extensionId = message.getString("extensionId");
final WebExtension extension = mExtensions.get(extensionId);
if (extension == null) {
if (BuildConfig.DEBUG) {
throw new RuntimeException("Could not find extension: " + extensionId);
}
Log.e(LOGTAG, "Could not find extension: " + extensionId);
}
return extension;
}
private void openPopup(final GeckoBundle message, final GeckoSession session,
final @WebExtension.Action.ActionType int actionType) {
final WebExtension extension = extensionFromBundle(message);
if (extension == null) {
return;
}
final WebExtension.Action action = new WebExtension.Action(
actionType, message.getBundle("action"), extension);
final WebExtension.ActionDelegate delegate = actionDelegateFor(extension, session);
if (delegate == null) {
return;
}
final GeckoResult<GeckoSession> popup = delegate.onOpenPopup(extension, action);
action.openPopup(popup);
}
private WebExtension.ActionDelegate actionDelegateFor(final WebExtension extension,
final GeckoSession session) {
if (session == null) {
return extension.actionDelegate;
}
return session.getWebExtensionActionDelegate(extension);
}
private void actionUpdate(final GeckoBundle message, final GeckoSession session,
final @WebExtension.Action.ActionType int actionType) {
final WebExtension extension = extensionFromBundle(message);
if (extension == null) {
return;
}
final WebExtension.ActionDelegate delegate = actionDelegateFor(extension, session);
if (delegate == null) {
return;
}
final WebExtension.Action action = new WebExtension.Action(
actionType, message.getBundle("action"), extension);
if (actionType == WebExtension.Action.TYPE_BROWSER_ACTION) {
delegate.onBrowserAction(extension, session, action);
} else if (actionType == WebExtension.Action.TYPE_PAGE_ACTION) {
delegate.onPageAction(extension, session, action);
}
}
public void handleMessage(final String event, final GeckoBundle message,
final EventCallback callback, final GeckoSession session) {
if ("GeckoView:WebExtension:Disconnect".equals(event)) {
@ -239,6 +308,18 @@ import java.util.Map;
} else if ("GeckoView:WebExtension:PortMessage".equals(event)) {
portMessage(message, callback);
return;
} else if ("GeckoView:BrowserAction:Update".equals(event)) {
actionUpdate(message, session, WebExtension.Action.TYPE_BROWSER_ACTION);
return;
} else if ("GeckoView:PageAction:Update".equals(event)) {
actionUpdate(message, session, WebExtension.Action.TYPE_PAGE_ACTION);
return;
} else if ("GeckoView:BrowserAction:OpenPopup".equals(event)) {
openPopup(message, session, WebExtension.Action.TYPE_BROWSER_ACTION);
return;
} else if ("GeckoView:PageAction:OpenPopup".equals(event)) {
openPopup(message, session, WebExtension.Action.TYPE_PAGE_ACTION);
return;
}
final String nativeApp = message.getString("nativeApp");

Просмотреть файл

@ -40,6 +40,9 @@ exclude: true
- Added [`GeckoView.setViewBackend`][72.11] to set whether GeckoView should be
backed by a [`TextureView`][72.12] or a [`SurfaceView`][72.13].
([bug 1530402]({{bugzilla}}1530402))
- Added support for Browser and Page Action from the WebExtension API.
See [`WebExtension.Action`][72.14].
([bug 1530402]({{bugzilla}}1530402))
[72.1]: {{javadoc_uri}}/GeckoSession.NavigationDelegate.LoadRequest#hasUserGesture-
[72.2]: {{javadoc_uri}}/Autofill.html
@ -54,6 +57,7 @@ exclude: true
[72.11]: {{javadoc_uri}}/GeckoView.html#setViewBackend-int-
[72.12]: https://developer.android.com/reference/android/view/TextureView
[72.13]: https://developer.android.com/reference/android/view/SurfaceView
[72.14]: {{javadoc_uri}}/WebExtension.Action.html
## v71
- Added a content blocking flag for blocked social cookies to [`ContentBlocking`][70.17].
@ -451,4 +455,4 @@ exclude: true
[65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
[65.25]: {{javadoc_uri}}/GeckoResult.html
[api-version]: cff8d49f3436c4b3b5ae91f96f333b8a5d55ab96
[api-version]: d4fbf3825322768a22d225f79c659bfd36eebbc6

Просмотреть файл

@ -4,7 +4,11 @@
"use strict";
var EXPORTED_SYMBOLS = ["GeckoViewConnection", "GeckoViewWebExtension"];
var EXPORTED_SYMBOLS = [
"ExtensionActionHelper",
"GeckoViewConnection",
"GeckoViewWebExtension",
];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
@ -18,10 +22,76 @@ XPCOMUtils.defineLazyModuleGetters(this, {
EventDispatcher: "resource://gre/modules/Messaging.jsm",
Extension: "resource://gre/modules/Extension.jsm",
ExtensionChild: "resource://gre/modules/ExtensionChild.jsm",
GeckoViewTabBridge: "resource://gre/modules/GeckoViewTab.jsm",
});
const { debug, warn } = GeckoViewUtils.initLogging("Console"); // eslint-disable-line no-unused-vars
/** Provides common logic between page and browser actions */
class ExtensionActionHelper {
constructor({
tabTracker,
windowTracker,
tabContext,
properties,
extension,
}) {
this.tabTracker = tabTracker;
this.windowTracker = windowTracker;
this.tabContext = tabContext;
this.properties = properties;
this.extension = extension;
}
getTab(aTabId) {
if (aTabId !== null) {
return this.tabTracker.getTab(aTabId);
}
return null;
}
getWindow(aWindowId) {
if (aWindowId !== null) {
return this.windowTracker.getWindow(aWindowId);
}
return null;
}
extractProperties(aAction) {
const merged = {};
for (const p of this.properties) {
merged[p] = aAction[p];
}
return merged;
}
eventDispatcherFor(aTabId) {
if (!aTabId) {
return EventDispatcher.instance;
}
const windowId = GeckoViewTabBridge.tabIdToWindowId(aTabId);
const window = this.windowTracker.getWindow(windowId);
return window.WindowEventDispatcher;
}
sendRequestForResult(aTabId, aData) {
return this.eventDispatcherFor(aTabId).sendRequestForResult({
...aData,
aTabId,
extensionId: this.extension.id,
});
}
sendRequest(aTabId, aData) {
return this.eventDispatcherFor(aTabId).sendRequest({
...aData,
aTabId,
extensionId: this.extension.id,
});
}
}
class EmbedderPort extends ExtensionChild.Port {
constructor(...args) {
super(...args);
@ -197,10 +267,47 @@ var GeckoViewWebExtension = {
}
},
extensionById(aId) {
const scope = this.extensionScopes.get(aId);
if (!scope) {
return null;
}
return scope.extension;
},
onEvent(aEvent, aData, aCallback) {
debug`onEvent ${aEvent} ${aData}`;
switch (aEvent) {
case "GeckoView:BrowserAction:Click": {
const extension = this.extensionById(aData.extensionId);
if (!extension) {
return;
}
const browserAction = this.browserActions.get(extension);
if (!browserAction) {
return;
}
browserAction.click();
break;
}
case "GeckoView:PageAction:Click": {
const extension = this.extensionById(aData.extensionId);
if (!extension) {
return;
}
const pageAction = this.pageActions.get(extension);
if (!pageAction) {
return;
}
pageAction.click();
break;
}
case "GeckoView:RegisterWebExtension": {
const uri = Services.io.newURI(aData.locationUri);
if (
@ -260,3 +367,7 @@ var GeckoViewWebExtension = {
};
GeckoViewWebExtension.extensionScopes = new Map();
// WeakMap[Extension -> BrowserAction]
GeckoViewWebExtension.browserActions = new WeakMap();
// WeakMap[Extension -> PageAction]
GeckoViewWebExtension.pageActions = new WeakMap();

Просмотреть файл

@ -6,6 +6,7 @@ toolkit.jar:
% content extensions %content/extensions/
content/extensions/schemas/activity_log.json
content/extensions/schemas/alarms.json
content/extensions/schemas/browser_action.json
content/extensions/schemas/browser_settings.json
#ifndef ANDROID
content/extensions/schemas/captive_portal.json
@ -34,6 +35,7 @@ toolkit.jar:
content/extensions/schemas/native_manifest.json
content/extensions/schemas/network_status.json
content/extensions/schemas/notifications.json
content/extensions/schemas/page_action.json
content/extensions/schemas/permissions.json
content/extensions/schemas/proxy.json
content/extensions/schemas/privacy.json