зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
b085048f45
Коммит
644a663e7d
|
@ -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
|
||||
|
|
|
@ -66,6 +66,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",
|
||||
|
@ -73,6 +80,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",
|
||||
|
|
|
@ -605,6 +605,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);
|
||||
|
@ -643,6 +644,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;
|
||||
|
@ -1378,12 +1380,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}`);
|
||||
}
|
||||
}
|
Двоичные данные
mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/button/expected.png
Normal file
Двоичные данные
mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/button/expected.png
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 1.0 KiB |
Двоичные данные
mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/button/geo-19.png
Normal file
Двоичные данные
mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/button/geo-19.png
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 225 B |
Двоичные данные
mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/button/geo-38.png
Normal file
Двоичные данные
mobile/android/geckoview/src/androidTest/assets/web_extensions/actions/button/geo-38.png
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -355,9 +355,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() {
|
||||
|
@ -366,17 +368,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));
|
||||
}
|
||||
|
||||
|
@ -389,11 +406,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -413,7 +436,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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -442,7 +465,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");
|
||||
|
|
|
@ -27,6 +27,9 @@ exclude: true
|
|||
- Added [`GeckoView.setViewBackend`][72.6] to set whether GeckoView should be
|
||||
backed by a [`TextureView`][72.7] or a [`SurfaceView`][72.8].
|
||||
([bug 1530402]({{bugzilla}}1530402))
|
||||
- Added support for Browser and Page Action from the WebExtension API.
|
||||
See [`WebExtension.Action`][72.9].
|
||||
([bug 1530402]({{bugzilla}}1530402))
|
||||
|
||||
[72.1]: {{javadoc_uri}}/GeckoSession.NavigationDelegate.LoadRequest#hasUserGesture-
|
||||
[72.2]: {{javadoc_uri}}/Autofill.html
|
||||
|
@ -36,6 +39,7 @@ exclude: true
|
|||
[72.6]: {{javadoc_uri}}/GeckoView.html#setViewBackend-int-
|
||||
[72.7]: https://developer.android.com/reference/android/view/TextureView
|
||||
[72.8]: https://developer.android.com/reference/android/view/SurfaceView
|
||||
[72.9]: {{javadoc_uri}}/WebExtension.Action.html
|
||||
|
||||
## v71
|
||||
- Added a content blocking flag for blocked social cookies to [`ContentBlocking`][70.17].
|
||||
|
@ -431,4 +435,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]: 368d42e73eab610d9b53f8815c73933f630a688a
|
||||
[api-version]: 2cd1b6f37379153bb6abfb62fba0ff5fe3765e38
|
||||
|
|
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче