зеркало из 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:
Родитель
e8fa5b32d8
Коммит
d39cf52795
|
@ -9,7 +9,7 @@
|
||||||
},
|
},
|
||||||
"browserAction": {
|
"browserAction": {
|
||||||
"url": "chrome://browser/content/parent/ext-browserAction.js",
|
"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"],
|
"scopes": ["addon_parent"],
|
||||||
"manifest": ["browser_action"],
|
"manifest": ["browser_action"],
|
||||||
"paths": [
|
"paths": [
|
||||||
|
@ -140,7 +140,7 @@
|
||||||
},
|
},
|
||||||
"pageAction": {
|
"pageAction": {
|
||||||
"url": "chrome://browser/content/parent/ext-pageAction.js",
|
"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"],
|
"scopes": ["addon_parent"],
|
||||||
"manifest": ["page_action"],
|
"manifest": ["page_action"],
|
||||||
"paths": [
|
"paths": [
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
browser.jar:
|
browser.jar:
|
||||||
content/browser/schemas/bookmarks.json
|
content/browser/schemas/bookmarks.json
|
||||||
content/browser/schemas/browser_action.json
|
|
||||||
content/browser/schemas/browsing_data.json
|
content/browser/schemas/browsing_data.json
|
||||||
content/browser/schemas/chrome_settings_overrides.json
|
content/browser/schemas/chrome_settings_overrides.json
|
||||||
content/browser/schemas/commands.json
|
content/browser/schemas/commands.json
|
||||||
|
@ -18,7 +17,6 @@ browser.jar:
|
||||||
content/browser/schemas/menus_child.json
|
content/browser/schemas/menus_child.json
|
||||||
content/browser/schemas/normandyAddonStudy.json
|
content/browser/schemas/normandyAddonStudy.json
|
||||||
content/browser/schemas/omnibox.json
|
content/browser/schemas/omnibox.json
|
||||||
content/browser/schemas/page_action.json
|
|
||||||
content/browser/schemas/pkcs11.json
|
content/browser/schemas/pkcs11.json
|
||||||
content/browser/schemas/search.json
|
content/browser/schemas/search.json
|
||||||
content/browser/schemas/sessions.json
|
content/browser/schemas/sessions.json
|
||||||
|
|
|
@ -70,6 +70,13 @@ global.openOptionsPage = extension => {
|
||||||
};
|
};
|
||||||
|
|
||||||
extensions.registerModules({
|
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: {
|
browsingData: {
|
||||||
url: "chrome://geckoview/content/ext-browsingData.js",
|
url: "chrome://geckoview/content/ext-browsingData.js",
|
||||||
schema: "chrome://geckoview/content/schemas/browsing_data.json",
|
schema: "chrome://geckoview/content/schemas/browsing_data.json",
|
||||||
|
@ -77,6 +84,13 @@ extensions.registerModules({
|
||||||
manifest: ["browsing_data"],
|
manifest: ["browsing_data"],
|
||||||
paths: [["browsingData"]],
|
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: {
|
tabs: {
|
||||||
url: "chrome://geckoview/content/ext-tabs.js",
|
url: "chrome://geckoview/content/ext-tabs.js",
|
||||||
schema: "chrome://geckoview/content/schemas/tabs.json",
|
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", {
|
GeckoViewUtils.addLazyGetter(this, "GeckoViewWebExtension", {
|
||||||
module: "resource://gre/modules/GeckoViewWebExtension.jsm",
|
module: "resource://gre/modules/GeckoViewWebExtension.jsm",
|
||||||
ged: [
|
ged: [
|
||||||
|
"GeckoView:BrowserAction:Click",
|
||||||
|
"GeckoView:PageAction:Click",
|
||||||
"GeckoView:RegisterWebExtension",
|
"GeckoView:RegisterWebExtension",
|
||||||
"GeckoView:UnregisterWebExtension",
|
"GeckoView:UnregisterWebExtension",
|
||||||
"GeckoView:WebExtension:PortDisconnect",
|
"GeckoView:WebExtension:PortDisconnect",
|
||||||
|
|
|
@ -601,6 +601,7 @@ package org.mozilla.geckoview {
|
||||||
method @UiThread public void getSurfaceBounds(@NonNull Rect);
|
method @UiThread public void getSurfaceBounds(@NonNull Rect);
|
||||||
method @AnyThread @NonNull public SessionTextInput getTextInput();
|
method @AnyThread @NonNull public SessionTextInput getTextInput();
|
||||||
method @AnyThread @NonNull public GeckoResult<String> getUserAgent();
|
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 goBack();
|
||||||
method @AnyThread public void goForward();
|
method @AnyThread public void goForward();
|
||||||
method @AnyThread public void gotoHistoryIndex(int);
|
method @AnyThread public void gotoHistoryIndex(int);
|
||||||
|
@ -639,6 +640,7 @@ package org.mozilla.geckoview {
|
||||||
method @AnyThread public void setPromptDelegate(@Nullable GeckoSession.PromptDelegate);
|
method @AnyThread public void setPromptDelegate(@Nullable GeckoSession.PromptDelegate);
|
||||||
method @UiThread public void setScrollDelegate(@Nullable GeckoSession.ScrollDelegate);
|
method @UiThread public void setScrollDelegate(@Nullable GeckoSession.ScrollDelegate);
|
||||||
method @UiThread public void setSelectionActionDelegate(@Nullable GeckoSession.SelectionActionDelegate);
|
method @UiThread public void setSelectionActionDelegate(@Nullable GeckoSession.SelectionActionDelegate);
|
||||||
|
method @AnyThread public void setWebExtensionActionDelegate(@NonNull WebExtension, @Nullable WebExtension.ActionDelegate);
|
||||||
method @AnyThread public void stop();
|
method @AnyThread public void stop();
|
||||||
method @UiThread protected void setShouldPinOnScreen(boolean);
|
method @UiThread protected void setShouldPinOnScreen(boolean);
|
||||||
field public static final Parcelable.Creator<GeckoSession> CREATOR;
|
field public static final Parcelable.Creator<GeckoSession> CREATOR;
|
||||||
|
@ -1387,12 +1389,37 @@ package org.mozilla.geckoview {
|
||||||
public class WebExtension {
|
public class WebExtension {
|
||||||
ctor public WebExtension(@NonNull String, @NonNull String, long);
|
ctor public WebExtension(@NonNull String, @NonNull String, long);
|
||||||
ctor public WebExtension(@NonNull String);
|
ctor public WebExtension(@NonNull String);
|
||||||
|
method @AnyThread public void setActionDelegate(@Nullable WebExtension.ActionDelegate);
|
||||||
method @UiThread public void setMessageDelegate(@Nullable WebExtension.MessageDelegate, @NonNull String);
|
method @UiThread public void setMessageDelegate(@Nullable WebExtension.MessageDelegate, @NonNull String);
|
||||||
field public final long flags;
|
field public final long flags;
|
||||||
field @NonNull public final String id;
|
field @NonNull public final String id;
|
||||||
field @NonNull public final String location;
|
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 {
|
public static class WebExtension.Flags {
|
||||||
ctor protected Flags();
|
ctor protected Flags();
|
||||||
field public static final long ALLOW_CONTENT_MESSAGING = 1L;
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -363,9 +363,11 @@ public class GeckoSession implements Parcelable {
|
||||||
|
|
||||||
private final class WebExtensionListener implements BundleEventListener {
|
private final class WebExtensionListener implements BundleEventListener {
|
||||||
final private HashMap<WebExtensionSender, WebExtension.MessageDelegate> mMessageDelegates;
|
final private HashMap<WebExtensionSender, WebExtension.MessageDelegate> mMessageDelegates;
|
||||||
|
final private HashMap<String, WebExtension.ActionDelegate> mActionDelegates;
|
||||||
|
|
||||||
public WebExtensionListener() {
|
public WebExtensionListener() {
|
||||||
mMessageDelegates = new HashMap<>();
|
mMessageDelegates = new HashMap<>();
|
||||||
|
mActionDelegates = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ void registerListeners() {
|
/* package */ void registerListeners() {
|
||||||
|
@ -374,17 +376,32 @@ public class GeckoSession implements Parcelable {
|
||||||
"GeckoView:WebExtension:PortMessage",
|
"GeckoView:WebExtension:PortMessage",
|
||||||
"GeckoView:WebExtension:Connect",
|
"GeckoView:WebExtension:Connect",
|
||||||
"GeckoView:WebExtension:CloseTab",
|
"GeckoView:WebExtension:CloseTab",
|
||||||
|
|
||||||
|
// Browser and Page Actions
|
||||||
|
"GeckoView:BrowserAction:Update",
|
||||||
|
"GeckoView:BrowserAction:OpenPopup",
|
||||||
|
"GeckoView:PageAction:Update",
|
||||||
|
"GeckoView:PageAction:OpenPopup",
|
||||||
null);
|
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 WebExtension.MessageDelegate delegate,
|
||||||
final String nativeApp) {
|
final String nativeApp) {
|
||||||
mMessageDelegates.put(new WebExtensionSender(webExtension.id, nativeApp), delegate);
|
mMessageDelegates.put(new WebExtensionSender(webExtension.id, nativeApp), delegate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public WebExtension.MessageDelegate getDelegate(final WebExtension webExtension,
|
public WebExtension.MessageDelegate getMessageDelegate(final WebExtension webExtension,
|
||||||
final String nativeApp) {
|
final String nativeApp) {
|
||||||
return mMessageDelegates.get(new WebExtensionSender(webExtension.id, nativeApp));
|
return mMessageDelegates.get(new WebExtensionSender(webExtension.id, nativeApp));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -397,11 +414,17 @@ public class GeckoSession implements Parcelable {
|
||||||
|
|
||||||
if ("GeckoView:WebExtension:Message".equals(event)
|
if ("GeckoView:WebExtension:Message".equals(event)
|
||||||
|| "GeckoView:WebExtension:PortMessage".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()
|
mWindow.runtime.getWebExtensionDispatcher()
|
||||||
.handleMessage(event, message, callback, GeckoSession.this);
|
.handleMessage(event, message, callback, GeckoSession.this);
|
||||||
|
return;
|
||||||
} else if ("GeckoView:WebExtension:CloseTab".equals(event)) {
|
} else if ("GeckoView:WebExtension:CloseTab".equals(event)) {
|
||||||
mWindow.runtime.getWebExtensionController().closeTab(message, callback, GeckoSession.this);
|
mWindow.runtime.getWebExtensionController().closeTab(message, callback, GeckoSession.this);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -421,7 +444,7 @@ public class GeckoSession implements Parcelable {
|
||||||
public @Nullable WebExtension.MessageDelegate getMessageDelegate(
|
public @Nullable WebExtension.MessageDelegate getMessageDelegate(
|
||||||
final @NonNull WebExtension webExtension,
|
final @NonNull WebExtension webExtension,
|
||||||
final @NonNull String nativeApp) {
|
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,
|
public void setMessageDelegate(final @NonNull WebExtension webExtension,
|
||||||
final @Nullable WebExtension.MessageDelegate delegate,
|
final @Nullable WebExtension.MessageDelegate delegate,
|
||||||
final @NonNull String nativeApp) {
|
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 =
|
private final GeckoSessionHandler<ContentDelegate> mContentHandler =
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
package org.mozilla.geckoview;
|
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.IntDef;
|
||||||
import android.support.annotation.LongDef;
|
import android.support.annotation.LongDef;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
@ -15,7 +18,10 @@ import org.mozilla.gecko.util.GeckoBundle;
|
||||||
import java.lang.annotation.Retention;
|
import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ -50,6 +56,8 @@ public class WebExtension {
|
||||||
*/
|
*/
|
||||||
/* package */ final @NonNull Map<String, MessageDelegate> messageDelegates;
|
/* package */ final @NonNull Map<String, MessageDelegate> messageDelegates;
|
||||||
|
|
||||||
|
/* package */ @NonNull ActionDelegate actionDelegate;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "WebExtension {" +
|
return "WebExtension {" +
|
||||||
|
@ -473,4 +481,455 @@ public class WebExtension {
|
||||||
return this.isTopLevel;
|
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:Message",
|
||||||
"GeckoView:WebExtension:PortMessage",
|
"GeckoView:WebExtension:PortMessage",
|
||||||
"GeckoView:WebExtension:Connect",
|
"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;
|
mHandlerRegistered = true;
|
||||||
}
|
}
|
||||||
|
@ -231,6 +237,69 @@ import java.util.Map;
|
||||||
exception -> callback.sendError(exception));
|
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,
|
public void handleMessage(final String event, final GeckoBundle message,
|
||||||
final EventCallback callback, final GeckoSession session) {
|
final EventCallback callback, final GeckoSession session) {
|
||||||
if ("GeckoView:WebExtension:Disconnect".equals(event)) {
|
if ("GeckoView:WebExtension:Disconnect".equals(event)) {
|
||||||
|
@ -239,6 +308,18 @@ import java.util.Map;
|
||||||
} else if ("GeckoView:WebExtension:PortMessage".equals(event)) {
|
} else if ("GeckoView:WebExtension:PortMessage".equals(event)) {
|
||||||
portMessage(message, callback);
|
portMessage(message, callback);
|
||||||
return;
|
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");
|
final String nativeApp = message.getString("nativeApp");
|
||||||
|
|
|
@ -38,6 +38,9 @@ exclude: true
|
||||||
- Added [`GeckoView.setViewBackend`][72.11] to set whether GeckoView should be
|
- Added [`GeckoView.setViewBackend`][72.11] to set whether GeckoView should be
|
||||||
backed by a [`TextureView`][72.12] or a [`SurfaceView`][72.13].
|
backed by a [`TextureView`][72.12] or a [`SurfaceView`][72.13].
|
||||||
([bug 1530402]({{bugzilla}}1530402))
|
([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.1]: {{javadoc_uri}}/GeckoSession.NavigationDelegate.LoadRequest#hasUserGesture-
|
||||||
[72.2]: {{javadoc_uri}}/Autofill.html
|
[72.2]: {{javadoc_uri}}/Autofill.html
|
||||||
|
@ -52,6 +55,7 @@ exclude: true
|
||||||
[72.11]: {{javadoc_uri}}/GeckoView.html#setViewBackend-int-
|
[72.11]: {{javadoc_uri}}/GeckoView.html#setViewBackend-int-
|
||||||
[72.12]: https://developer.android.com/reference/android/view/TextureView
|
[72.12]: https://developer.android.com/reference/android/view/TextureView
|
||||||
[72.13]: https://developer.android.com/reference/android/view/SurfaceView
|
[72.13]: https://developer.android.com/reference/android/view/SurfaceView
|
||||||
|
[72.14]: {{javadoc_uri}}/WebExtension.Action.html
|
||||||
|
|
||||||
## v71
|
## v71
|
||||||
- Added a content blocking flag for blocked social cookies to [`ContentBlocking`][70.17].
|
- Added a content blocking flag for blocked social cookies to [`ContentBlocking`][70.17].
|
||||||
|
@ -449,4 +453,4 @@ exclude: true
|
||||||
[65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
|
[65.24]: {{javadoc_uri}}/CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
|
||||||
[65.25]: {{javadoc_uri}}/GeckoResult.html
|
[65.25]: {{javadoc_uri}}/GeckoResult.html
|
||||||
|
|
||||||
[api-version]: cff8d49f3436c4b3b5ae91f96f333b8a5d55ab96
|
[api-version]: d4fbf3825322768a22d225f79c659bfd36eebbc6
|
||||||
|
|
|
@ -4,7 +4,11 @@
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
var EXPORTED_SYMBOLS = ["GeckoViewConnection", "GeckoViewWebExtension"];
|
var EXPORTED_SYMBOLS = [
|
||||||
|
"ExtensionActionHelper",
|
||||||
|
"GeckoViewConnection",
|
||||||
|
"GeckoViewWebExtension",
|
||||||
|
];
|
||||||
|
|
||||||
const { XPCOMUtils } = ChromeUtils.import(
|
const { XPCOMUtils } = ChromeUtils.import(
|
||||||
"resource://gre/modules/XPCOMUtils.jsm"
|
"resource://gre/modules/XPCOMUtils.jsm"
|
||||||
|
@ -18,10 +22,76 @@ XPCOMUtils.defineLazyModuleGetters(this, {
|
||||||
EventDispatcher: "resource://gre/modules/Messaging.jsm",
|
EventDispatcher: "resource://gre/modules/Messaging.jsm",
|
||||||
Extension: "resource://gre/modules/Extension.jsm",
|
Extension: "resource://gre/modules/Extension.jsm",
|
||||||
ExtensionChild: "resource://gre/modules/ExtensionChild.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
|
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 {
|
class EmbedderPort extends ExtensionChild.Port {
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...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) {
|
onEvent(aEvent, aData, aCallback) {
|
||||||
debug`onEvent ${aEvent} ${aData}`;
|
debug`onEvent ${aEvent} ${aData}`;
|
||||||
|
|
||||||
switch (aEvent) {
|
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": {
|
case "GeckoView:RegisterWebExtension": {
|
||||||
const uri = Services.io.newURI(aData.locationUri);
|
const uri = Services.io.newURI(aData.locationUri);
|
||||||
if (
|
if (
|
||||||
|
@ -260,3 +367,7 @@ var GeckoViewWebExtension = {
|
||||||
};
|
};
|
||||||
|
|
||||||
GeckoViewWebExtension.extensionScopes = new Map();
|
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 %content/extensions/
|
||||||
content/extensions/schemas/activity_log.json
|
content/extensions/schemas/activity_log.json
|
||||||
content/extensions/schemas/alarms.json
|
content/extensions/schemas/alarms.json
|
||||||
|
content/extensions/schemas/browser_action.json
|
||||||
content/extensions/schemas/browser_settings.json
|
content/extensions/schemas/browser_settings.json
|
||||||
#ifndef ANDROID
|
#ifndef ANDROID
|
||||||
content/extensions/schemas/captive_portal.json
|
content/extensions/schemas/captive_portal.json
|
||||||
|
@ -34,6 +35,7 @@ toolkit.jar:
|
||||||
content/extensions/schemas/native_manifest.json
|
content/extensions/schemas/native_manifest.json
|
||||||
content/extensions/schemas/network_status.json
|
content/extensions/schemas/network_status.json
|
||||||
content/extensions/schemas/notifications.json
|
content/extensions/schemas/notifications.json
|
||||||
|
content/extensions/schemas/page_action.json
|
||||||
content/extensions/schemas/permissions.json
|
content/extensions/schemas/permissions.json
|
||||||
content/extensions/schemas/proxy.json
|
content/extensions/schemas/proxy.json
|
||||||
content/extensions/schemas/privacy.json
|
content/extensions/schemas/privacy.json
|
||||||
|
|
Загрузка…
Ссылка в новой задаче