Backed out 13 changesets (bug 1530402, bug 1533156) for multiple failures e.g columnrule-linestyles.html and test_composite.html. CLOSED TREE

Backed out changeset 9e55fee783ff (bug 1530402)
Backed out changeset f6af9d6a5482 (bug 1530402)
Backed out changeset bf09025d6f98 (bug 1530402)
Backed out changeset 9717ba255826 (bug 1530402)
Backed out changeset e34768ee01db (bug 1530402)
Backed out changeset b540d7b34ef6 (bug 1530402)
Backed out changeset 126af041394b (bug 1530402)
Backed out changeset cf16e02e62cd (bug 1530402)
Backed out changeset 3638fcff5bed (bug 1530402)
Backed out changeset 9630bed0ca2c (bug 1530402)
Backed out changeset 1aa5898efa5f (bug 1530402)
Backed out changeset 7462bbe4e676 (bug 1533156)
Backed out changeset 536a4cac3ffe (bug 1530402)

--HG--
rename : toolkit/components/extensions/schemas/browser_action.json => browser/components/extensions/schemas/browser_action.json
rename : toolkit/components/extensions/schemas/page_action.json => browser/components/extensions/schemas/page_action.json
This commit is contained in:
Csoregi Natalia 2019-11-14 03:46:37 +02:00
parent f61715aa1b
commit 4cf34e2666
64 changed files with 2435 additions and 3540 deletions

View File

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

View File

@ -32,17 +32,16 @@ ChromeUtils.defineModuleGetter(
"resource:///modules/ExtensionPopups.jsm"
);
var { DefaultWeakMap } = ExtensionUtils;
var { DefaultWeakMap, ExtensionError } = ExtensionUtils;
var { ExtensionParent } = ChromeUtils.import(
"resource://gre/modules/ExtensionParent.jsm"
);
var { BrowserActionBase } = ChromeUtils.import(
"resource://gre/modules/ExtensionActions.jsm"
);
var { IconDetails, StartupCache } = ExtensionParent;
XPCOMUtils.defineLazyGlobalGetters(this, ["InspectorUtils"]);
const POPUP_PRELOAD_TIMEOUT_MS = 200;
// WeakMap[Extension -> BrowserAction]
@ -57,47 +56,6 @@ XPCOMUtils.defineLazyGetter(this, "browserAreas", () => {
};
});
class BrowserAction extends BrowserActionBase {
constructor(extension, buttonDelegate) {
let tabContext = new TabContext(target => {
let window = target.ownerGlobal;
if (target === window) {
return this.getContextData(null);
}
return tabContext.get(window);
});
super(tabContext, extension);
this.buttonDelegate = buttonDelegate;
}
updateOnChange(target) {
if (target) {
let window = target.ownerGlobal;
if (target === window || target.selected) {
this.buttonDelegate.updateWindow(window);
}
} else {
for (let window of windowTracker.browserWindows()) {
this.buttonDelegate.updateWindow(window);
}
}
}
getTab(tabId) {
if (tabId !== null) {
return tabTracker.getTab(tabId);
}
return null;
}
getWindow(windowId) {
if (windowId !== null) {
return windowTracker.getWindow(windowId);
}
return null;
}
}
this.browserAction = class extends ExtensionAPI {
static for(extension) {
return browserActionMap.get(extension);
@ -108,18 +66,7 @@ this.browserAction = class extends ExtensionAPI {
let options = extension.manifest.browser_action;
this.action = new BrowserAction(extension, this);
await this.action.loadIconData();
this.iconData = new DefaultWeakMap(icons => this.getIconData(icons));
this.iconData.set(
this.action.getIcon(),
await StartupCache.get(
extension,
["browserAction", "default_icon_data"],
() => this.getIconData(this.action.getIcon())
)
);
let widgetId = makeWidgetId(extension.id);
this.id = `${widgetId}-browser-action`;
@ -131,17 +78,71 @@ this.browserAction = class extends ExtensionAPI {
this.eventQueue = [];
this.tabManager = extension.tabManager;
this.defaults = {
enabled: true,
title: options.default_title || extension.name,
badgeText: "",
badgeBackgroundColor: [0xd9, 0, 0, 255],
badgeDefaultColor: [255, 255, 255, 255],
badgeTextColor: null,
popup: options.default_popup || "",
area: browserAreas[options.default_area || "navbar"],
};
this.globals = Object.create(this.defaults);
this.browserStyle = options.browser_style;
browserActionMap.set(extension, this);
this.defaults.icon = await StartupCache.get(
extension,
["browserAction", "default_icon"],
() =>
IconDetails.normalize(
{
path: options.default_icon || extension.manifest.icons,
iconType: "browserAction",
themeIcons: options.theme_icons,
},
extension
)
);
this.iconData.set(
this.defaults.icon,
await StartupCache.get(
extension,
["browserAction", "default_icon_data"],
() => this.getIconData(this.defaults.icon)
)
);
this.tabContext = new TabContext(target => {
let window = target.ownerGlobal;
if (target === window) {
return this.globals;
}
return this.tabContext.get(window);
});
// eslint-disable-next-line mozilla/balanced-listeners
this.tabContext.on("location-change", this.handleLocationChange.bind(this));
this.build();
}
handleLocationChange(eventType, tab, fromBrowse) {
if (fromBrowse) {
this.tabContext.clear(tab);
this.updateOnChange(tab);
}
}
onShutdown() {
browserActionMap.delete(this.extension);
this.action.onShutdown();
this.tabContext.shutdown();
CustomizableUI.destroyWidget(this.id);
this.clearPopup();
@ -153,9 +154,9 @@ this.browserAction = class extends ExtensionAPI {
viewId: this.viewId,
type: "view",
removable: true,
label: this.action.getProperty(null, "title"),
tooltiptext: this.action.getProperty(null, "title"),
defaultArea: browserAreas[this.action.getDefaultArea()],
label: this.defaults.title || this.extension.name,
tooltiptext: this.defaults.title || "",
defaultArea: this.defaults.area,
showInPrivateBrowsing: this.extension.privateBrowsingAllowed,
// Don't attempt to load properties from the built-in widget string
@ -201,7 +202,7 @@ this.browserAction = class extends ExtensionAPI {
node.onmouseout = event => this.handleEvent(event);
node.onauxclick = event => this.handleEvent(event);
this.updateButton(node, this.action.getContextData(null), true);
this.updateButton(node, this.globals, true);
},
onBeforeCommand: event => {
@ -222,7 +223,7 @@ this.browserAction = class extends ExtensionAPI {
let tabbrowser = document.defaultView.gBrowser;
let tab = tabbrowser.selectedTab;
let popupURL = this.action.getProperty(tab, "popup");
let popupURL = this.getProperty(tab, "popup");
this.tabManager.addActiveTabPermission(tab);
// Popups are shown only if a popup URL is defined; otherwise
@ -268,6 +269,11 @@ this.browserAction = class extends ExtensionAPI {
},
});
// eslint-disable-next-line mozilla/balanced-listeners
this.tabContext.on("tab-select", (evt, tab) => {
this.updateWindow(tab.ownerGlobal);
});
this.widget = widget;
}
@ -290,14 +296,14 @@ this.browserAction = class extends ExtensionAPI {
let widget = this.widget.forWindow(window);
let tab = window.gBrowser.selectedTab;
if (!widget.node || !this.action.getProperty(tab, "enabled")) {
if (!widget.node || !this.getProperty(tab, "enabled")) {
return;
}
// Popups are shown only if a popup URL is defined; otherwise
// a "click" event is dispatched. This is done for compatibility with the
// Google Chrome onClicked extension API.
if (this.action.getProperty(tab, "popup")) {
if (this.getProperty(tab, "popup")) {
if (this.widget.areaType == CustomizableUI.TYPE_MENU_PANEL) {
await window.document.getElementById("nav-bar").overflowable.show();
}
@ -324,8 +330,8 @@ this.browserAction = class extends ExtensionAPI {
// Begin pre-loading the browser for the popup, so it's more likely to
// be ready by the time we get a complete click.
let tab = window.gBrowser.selectedTab;
let popupURL = this.action.getProperty(tab, "popup");
let enabled = this.action.getProperty(tab, "enabled");
let popupURL = this.getProperty(tab, "popup");
let enabled = this.getProperty(tab, "enabled");
if (
popupURL &&
@ -371,8 +377,8 @@ this.browserAction = class extends ExtensionAPI {
// Begin pre-loading the browser for the popup, so it's more likely to
// be ready by the time we get a complete click.
let tab = window.gBrowser.selectedTab;
let popupURL = this.action.getProperty(tab, "popup");
let enabled = this.action.getProperty(tab, "enabled");
let popupURL = this.getProperty(tab, "popup");
let enabled = this.getProperty(tab, "enabled");
if (
popupURL &&
@ -422,7 +428,7 @@ this.browserAction = class extends ExtensionAPI {
}
let { gBrowser } = window;
if (this.action.getProperty(gBrowser.selectedTab, "enabled")) {
if (this.getProperty(gBrowser.selectedTab, "enabled")) {
this.lastClickInfo = {
button: 1,
modifiers: clickModifiersFromEvent(event),
@ -541,7 +547,7 @@ this.browserAction = class extends ExtensionAPI {
"badgeStyle",
[
`background-color: ${serializeColor(tabData.badgeBackgroundColor)}`,
`color: ${serializeColor(this.action.getTextColor(tabData))}`,
`color: ${serializeColor(this.getTextColor(tabData))}`,
].join("; ")
);
@ -591,19 +597,189 @@ this.browserAction = class extends ExtensionAPI {
let node = this.widget.forWindow(window).node;
if (node) {
let tab = window.gBrowser.selectedTab;
this.updateButton(node, this.action.getContextData(tab));
this.updateButton(node, this.tabContext.get(tab));
}
}
/**
* Update the toolbar button when the extension changes the icon, title, url, etc.
* If it only changes a parameter for a single tab, `target` will be that tab.
* If it only changes a parameter for a single window, `target` will be that window.
* Otherwise `target` will be null.
*
* @param {XULElement|ChromeWindow|null} target
* Browser tab or browser chrome window, may be null.
*/
updateOnChange(target) {
if (target) {
let window = target.ownerGlobal;
if (target === window || target.selected) {
this.updateWindow(window);
}
} else {
for (let window of windowTracker.browserWindows()) {
this.updateWindow(window);
}
}
}
/**
* Gets the target object corresponding to the `details` parameter of the various
* get* and set* API methods.
*
* @param {Object} details
* An object with optional `tabId` or `windowId` properties.
* @throws if both `tabId` and `windowId` are specified, or if they are invalid.
* @returns {XULElement|ChromeWindow|null}
* If a `tabId` was specified, the corresponding XULElement tab.
* If a `windowId` was specified, the corresponding ChromeWindow.
* Otherwise, `null`.
*/
getTargetFromDetails({ tabId, windowId }) {
if (tabId != null && windowId != null) {
throw new ExtensionError(
"Only one of tabId and windowId can be specified."
);
}
if (tabId != null) {
return tabTracker.getTab(tabId);
} else if (windowId != null) {
return windowTracker.getWindow(windowId);
}
return null;
}
/**
* Gets the data associated with a tab, window, or the global one.
*
* @param {XULElement|ChromeWindow|null} target
* A XULElement tab, a ChromeWindow, or null for the global data.
* @returns {Object}
* The icon, title, badge, etc. associated with the target.
*/
getContextData(target) {
if (target) {
return this.tabContext.get(target);
}
return this.globals;
}
/**
* Set a global, window specific or tab specific property.
*
* @param {XULElement|ChromeWindow|null} target
* A XULElement tab, a ChromeWindow, or null for the global data.
* @param {string} prop
* String property to set. Should should be one of "icon", "title", "badgeText",
* "popup", "badgeBackgroundColor", "badgeTextColor" or "enabled".
* @param {string} value
* Value for prop.
* @returns {Object}
* The object to which the property has been set.
*/
setProperty(target, prop, value) {
let values = this.getContextData(target);
if (value === null) {
delete values[prop];
} else {
values[prop] = value;
}
this.updateOnChange(target);
return values;
}
/**
* Retrieve the value of a global, window specific or tab specific property.
*
* @param {XULElement|ChromeWindow|null} target
* A XULElement tab, a ChromeWindow, or null for the global data.
* @param {string} prop
* String property to retrieve. Should should be one of "icon", "title",
* "badgeText", "popup", "badgeBackgroundColor" or "enabled".
* @returns {string} value
* Value of prop.
*/
getProperty(target, prop) {
return this.getContextData(target)[prop];
}
setPropertyFromDetails(details, prop, value) {
return this.setProperty(this.getTargetFromDetails(details), prop, value);
}
getPropertyFromDetails(details, prop) {
return this.getProperty(this.getTargetFromDetails(details), prop);
}
/**
* Determines the text badge color to be used in a tab, window, or globally.
*
* @param {Object} values
* The values associated with the tab or window, or global values.
* @returns {ColorArray}
*/
getTextColor(values) {
// If a text color has been explicitly provided, use it.
let { badgeTextColor } = values;
if (badgeTextColor) {
return badgeTextColor;
}
// Otherwise, check if the default color to be used has been cached previously.
let { badgeDefaultColor } = values;
if (badgeDefaultColor) {
return badgeDefaultColor;
}
// Choose a color among white and black, maximizing contrast with background
// according to https://www.w3.org/TR/WCAG20-TECHS/G18.html#G18-procedure
let [r, g, b] = values.badgeBackgroundColor
.slice(0, 3)
.map(function(channel) {
channel /= 255;
if (channel <= 0.03928) {
return channel / 12.92;
}
return ((channel + 0.055) / 1.055) ** 2.4;
});
let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
// The luminance is 0 for black, 1 for white, and `lum` for the background color.
// Since `0 <= lum`, the contrast ratio for black is `c0 = (lum + 0.05) / 0.05`.
// Since `lum <= 1`, the contrast ratio for white is `c1 = 1.05 / (lum + 0.05)`.
// We want to maximize contrast, so black is chosen if `c1 < c0`, that is, if
// `1.05 * 0.05 < (L + 0.05) ** 2`. Otherwise white is chosen.
let channel = 1.05 * 0.05 < (lum + 0.05) ** 2 ? 0 : 255;
let result = [channel, channel, channel, 255];
// Cache the result as high as possible in the prototype chain
while (!Object.getOwnPropertyDescriptor(values, "badgeDefaultColor")) {
values = Object.getPrototypeOf(values);
}
values.badgeDefaultColor = result;
return result;
}
getAPI(context) {
let { extension } = context;
let { tabManager } = extension;
let { action } = this;
let browserAction = this;
function parseColor(color, kind) {
if (typeof color == "string") {
let rgba = InspectorUtils.colorToRGBA(color);
if (!rgba) {
throw new ExtensionError(`Invalid badge ${kind} color: "${color}"`);
}
color = [rgba.r, rgba.g, rgba.b, Math.round(rgba.a * 255)];
}
return color;
}
return {
browserAction: {
...action.api(context),
onClicked: new EventManager({
context,
name: "browserAction.onClicked",
@ -613,20 +789,117 @@ this.browserAction = class extends ExtensionAPI {
context.withPendingBrowser(browser, () =>
fire.sync(
tabManager.convert(tabTracker.activeTab),
this.lastClickInfo
browserAction.lastClickInfo
)
);
};
this.on("click", listener);
browserAction.on("click", listener);
return () => {
this.off("click", listener);
browserAction.off("click", listener);
};
},
}).api(),
openPopup: () => {
enable: function(tabId) {
browserAction.setPropertyFromDetails({ tabId }, "enabled", true);
},
disable: function(tabId) {
browserAction.setPropertyFromDetails({ tabId }, "enabled", false);
},
isEnabled: function(details) {
return browserAction.getPropertyFromDetails(details, "enabled");
},
setTitle: function(details) {
browserAction.setPropertyFromDetails(details, "title", details.title);
},
getTitle: function(details) {
return browserAction.getPropertyFromDetails(details, "title");
},
setIcon: function(details) {
details.iconType = "browserAction";
let icon = IconDetails.normalize(details, extension, context);
if (!Object.keys(icon).length) {
icon = null;
}
browserAction.setPropertyFromDetails(details, "icon", icon);
},
setBadgeText: function(details) {
browserAction.setPropertyFromDetails(
details,
"badgeText",
details.text
);
},
getBadgeText: function(details) {
return browserAction.getPropertyFromDetails(details, "badgeText");
},
setPopup: function(details) {
// Note: Chrome resolves arguments to setIcon relative to the calling
// context, but resolves arguments to setPopup relative to the extension
// root.
// For internal consistency, we currently resolve both relative to the
// calling context.
let url = details.popup && context.uri.resolve(details.popup);
if (url && !context.checkLoadURL(url)) {
return Promise.reject({ message: `Access denied for URL ${url}` });
}
browserAction.setPropertyFromDetails(details, "popup", url);
},
getPopup: function(details) {
return browserAction.getPropertyFromDetails(details, "popup");
},
setBadgeBackgroundColor: function(details) {
let color = parseColor(details.color, "background");
let values = browserAction.setPropertyFromDetails(
details,
"badgeBackgroundColor",
color
);
if (color === null) {
// Let the default text color inherit after removing background color
delete values.badgeDefaultColor;
} else {
// Invalidate a cached default color calculated with the old background
values.badgeDefaultColor = null;
}
},
getBadgeBackgroundColor: function(details, callback) {
return browserAction.getPropertyFromDetails(
details,
"badgeBackgroundColor"
);
},
setBadgeTextColor: function(details) {
let color = parseColor(details.color, "text");
browserAction.setPropertyFromDetails(
details,
"badgeTextColor",
color
);
},
getBadgeTextColor: function(details) {
let target = browserAction.getTargetFromDetails(details);
let values = browserAction.getContextData(target);
return browserAction.getTextColor(values);
},
openPopup: function() {
let window = windowTracker.topWindow;
this.triggerAction(window);
browserAction.triggerAction(window);
},
},
};

View File

@ -22,34 +22,17 @@ ChromeUtils.defineModuleGetter(
"resource:///modules/ExtensionPopups.jsm"
);
var { DefaultWeakMap } = ExtensionUtils;
var { PageActionBase } = ChromeUtils.import(
"resource://gre/modules/ExtensionActions.jsm"
var { ExtensionParent } = ChromeUtils.import(
"resource://gre/modules/ExtensionParent.jsm"
);
var { IconDetails, StartupCache } = ExtensionParent;
var { DefaultWeakMap } = ExtensionUtils;
// WeakMap[Extension -> PageAction]
let pageActionMap = new WeakMap();
class PageAction extends PageActionBase {
constructor(extension, buttonDelegate) {
let tabContext = new TabContext(tab => this.getContextData(null));
super(tabContext, extension);
this.buttonDelegate = buttonDelegate;
}
updateOnChange(target) {
this.buttonDelegate.updateButton(target.ownerGlobal);
}
getTab(tabId) {
if (tabId !== null) {
return tabTracker.getTab(tabId);
}
return null;
}
}
this.pageAction = class extends ExtensionAPI {
static for(extension) {
return pageActionMap.get(extension);
@ -59,18 +42,57 @@ this.pageAction = class extends ExtensionAPI {
let { extension } = this;
let options = extension.manifest.page_action;
this.action = new PageAction(extension, this);
await this.action.loadIconData();
let widgetId = makeWidgetId(extension.id);
this.id = widgetId + "-page-action";
this.tabManager = extension.tabManager;
// `show` can have three different values:
// - `false`. This means the page action is not shown.
// It's set as default if show_matches is empty. Can also be set in a tab via
// `pageAction.hide(tabId)`, e.g. in order to override show_matches.
// - `true`. This means the page action is shown.
// It's never set as default because <all_urls> doesn't really match all URLs
// (e.g. "about:" URLs). But can be set in a tab via `pageAction.show(tabId)`.
// - `undefined`.
// This is the default value when there are some patterns in show_matches.
// Can't be set as a tab-specific value.
let show, showMatches, hideMatches;
let show_matches = options.show_matches || [];
let hide_matches = options.hide_matches || [];
if (!show_matches.length) {
// Always hide by default. No need to do any pattern matching.
show = false;
} else {
// Might show or hide depending on the URL. Enable pattern matching.
const { restrictSchemes } = extension;
showMatches = new MatchPatternSet(show_matches, { restrictSchemes });
hideMatches = new MatchPatternSet(hide_matches, { restrictSchemes });
}
this.defaults = {
show,
showMatches,
hideMatches,
title: options.default_title || extension.name,
popup: options.default_popup || "",
pinned: options.pinned,
};
this.browserStyle = options.browser_style;
this.tabContext = new TabContext(tab => this.defaults);
this.tabContext.on("location-change", this.handleLocationChange.bind(this)); // eslint-disable-line mozilla/balanced-listeners
pageActionMap.set(extension, this);
this.defaults.icon = await StartupCache.get(
extension,
["pageAction", "default_icon"],
() => this.normalize({ path: options.default_icon || "" })
);
this.lastValues = new DefaultWeakMap(() => ({}));
if (!this.browserPageAction) {
@ -100,10 +122,10 @@ this.pageAction = class extends ExtensionAPI {
new PageActions.Action({
id: widgetId,
extensionID: extension.id,
title: this.action.getProperty(null, "title"),
iconURL: this.action.getProperty(null, "title"),
pinnedToUrlbar: this.action.getPinned(),
disabled: !this.action.getProperty(null, "enabled"),
title: this.defaults.title,
iconURL: this.defaults.icon,
pinnedToUrlbar: this.defaults.pinned,
disabled: !this.defaults.show,
onCommand: (event, buttonNode) => {
this.lastClickInfo = {
button: event.button || 0,
@ -129,10 +151,10 @@ this.pageAction = class extends ExtensionAPI {
// If the page action is only enabled in some URLs, do pattern matching in
// the active tabs and update the button if necessary.
if (this.action.getProperty(null, "enabled") === undefined) {
if (show === undefined) {
for (let window of windowTracker.browserWindows()) {
let tab = window.gBrowser.selectedTab;
if (this.action.isShownForTab(tab)) {
if (this.isShown(tab)) {
this.updateButton(window);
}
}
@ -142,7 +164,8 @@ this.pageAction = class extends ExtensionAPI {
onShutdown(isAppShutdown) {
pageActionMap.delete(this.extension);
this.action.onShutdown();
this.tabContext.shutdown();
// Removing the browser page action causes PageActions to forget about it
// across app restarts, so don't remove it on app shutdown, but do remove
@ -154,15 +177,46 @@ this.pageAction = class extends ExtensionAPI {
}
}
// Returns the value of the property |prop| for the given tab, where
// |prop| is one of "show", "title", "icon", "popup".
getProperty(tab, prop) {
return this.tabContext.get(tab)[prop];
}
// Sets the value of the property |prop| for the given tab to the
// given value, symmetrically to |getProperty|.
//
// If |tab| is currently selected, updates the page action button to
// reflect the new value.
setProperty(tab, prop, value) {
if (value != null) {
this.tabContext.get(tab)[prop] = value;
} else {
delete this.tabContext.get(tab)[prop];
}
if (tab.selected) {
this.updateButton(tab.ownerGlobal);
}
}
normalize(details, context = null) {
let icon = IconDetails.normalize(details, this.extension, context);
if (!Object.keys(icon).length) {
icon = null;
}
return icon;
}
// Updates the page action button in the given window to reflect the
// properties of the currently selected tab:
//
// Updates "tooltiptext" and "aria-label" to match "title" property.
// Updates "image" to match the "icon" property.
// Enables or disables the icon, based on the "enabled" and "patternMatching" properties.
// Enables or disables the icon, based on the "show" and "patternMatching" properties.
updateButton(window) {
let tab = window.gBrowser.selectedTab;
let tabData = this.action.getContextData(tab);
let tabData = this.tabContext.get(tab);
let last = this.lastValues.get(window);
window.requestAnimationFrame(() => {
@ -178,11 +232,10 @@ this.pageAction = class extends ExtensionAPI {
last.title = title;
}
let enabled =
tabData.enabled != null ? tabData.enabled : tabData.patternMatching;
if (last.enabled !== enabled) {
this.browserPageAction.setDisabled(!enabled, window);
last.enabled = enabled;
let show = tabData.show != null ? tabData.show : tabData.patternMatching;
if (last.show !== show) {
this.browserPageAction.setDisabled(!show, window);
last.show = show;
}
let icon = tabData.icon;
@ -193,6 +246,28 @@ this.pageAction = class extends ExtensionAPI {
});
}
// Checks whether the tab action is shown when the specified tab becomes active.
// Does pattern matching if necessary, and caches the result as a tab-specific value.
// @param {XULElement} tab
// The tab to be checked
// @return boolean
isShown(tab) {
let tabData = this.tabContext.get(tab);
// If there is a "show" value, return it. Can be due to show(), hide() or empty show_matches.
if (tabData.show !== undefined) {
return tabData.show;
}
// Otherwise pattern matching must have been configured. Do it, caching the result.
if (tabData.patternMatching === undefined) {
let uri = tab.linkedBrowser.currentURI;
tabData.patternMatching =
tabData.showMatches.matches(uri) && !tabData.hideMatches.matches(uri);
}
return tabData.patternMatching;
}
/**
* Triggers this page action for the given window, with the same effects as
* if it were clicked by a user.
@ -202,7 +277,7 @@ this.pageAction = class extends ExtensionAPI {
* @param {Window} window
*/
triggerAction(window) {
if (this.action.isShownForTab(window.gBrowser.selectedTab)) {
if (this.isShown(window.gBrowser.selectedTab)) {
this.lastClickInfo = { button: 0, modifiers: [] };
this.handleClick(window);
}
@ -240,7 +315,7 @@ this.pageAction = class extends ExtensionAPI {
ExtensionTelemetry.pageActionPopupOpen.stopwatchStart(extension, this);
let tab = window.gBrowser.selectedTab;
let popupURL = this.action.getProperty(tab, "popup");
let popupURL = this.tabContext.get(tab).popup;
this.tabManager.addActiveTabPermission(tab);
@ -285,15 +360,48 @@ this.pageAction = class extends ExtensionAPI {
}
}
/**
* Updates the `tabData` for any location change, however it only updates the button
* when the selected tab has a location change, or the selected tab has changed.
*
* @param {string} eventType
* The type of the event, should be "location-change".
* @param {XULElement} tab
* The tab whose location changed, or which has become selected.
* @param {boolean} [fromBrowse]
* - `true` if navigation occurred in `tab`.
* - `false` if the location changed but no navigation occurred, e.g. due to
a hash change or `history.pushState`.
* - Omitted if TabSelect has occurred, tabData does not need to be updated.
*/
handleLocationChange(eventType, tab, fromBrowse) {
if (fromBrowse === true) {
// Clear tab data on navigation.
this.tabContext.clear(tab);
} else if (fromBrowse === false) {
// Clear pattern matching cache when URL changes.
let tabData = this.tabContext.get(tab);
if (tabData.patternMatching !== undefined) {
tabData.patternMatching = undefined;
}
}
if (tab.selected) {
// isShown will do pattern matching (if necessary) and store the result
// so that updateButton knows whether the page action should be shown.
this.isShown(tab);
this.updateButton(tab.ownerGlobal);
}
}
getAPI(context) {
const { extension } = context;
let { extension } = context;
const { tabManager } = extension;
const { action } = this;
const pageAction = this;
return {
pageAction: {
...action.api(context),
onClicked: new EventManager({
context,
name: "pageAction.onClicked",
@ -305,16 +413,73 @@ this.pageAction = class extends ExtensionAPI {
);
};
this.on("click", listener);
pageAction.on("click", listener);
return () => {
this.off("click", listener);
pageAction.off("click", listener);
};
},
}).api(),
openPopup: () => {
show(tabId) {
let tab = tabTracker.getTab(tabId);
pageAction.setProperty(tab, "show", true);
},
hide(tabId) {
let tab = tabTracker.getTab(tabId);
pageAction.setProperty(tab, "show", false);
},
isShown(details) {
let tab = tabTracker.getTab(details.tabId);
return pageAction.isShown(tab);
},
setTitle(details) {
let tab = tabTracker.getTab(details.tabId);
pageAction.setProperty(tab, "title", details.title);
},
getTitle(details) {
let tab = tabTracker.getTab(details.tabId);
let title = pageAction.getProperty(tab, "title");
return Promise.resolve(title);
},
setIcon(details) {
let tab = tabTracker.getTab(details.tabId);
let icon = pageAction.normalize(details, context);
pageAction.setProperty(tab, "icon", icon);
},
setPopup(details) {
let tab = tabTracker.getTab(details.tabId);
// Note: Chrome resolves arguments to setIcon relative to the calling
// context, but resolves arguments to setPopup relative to the extension
// root.
// For internal consistency, we currently resolve both relative to the
// calling context.
let url = details.popup && context.uri.resolve(details.popup);
if (url && !context.checkLoadURL(url)) {
return Promise.reject({ message: `Access denied for URL ${url}` });
}
pageAction.setProperty(tab, "popup", url);
},
getPopup(details) {
let tab = tabTracker.getTab(details.tabId);
let popup = pageAction.getProperty(tab, "popup");
return Promise.resolve(popup);
},
openPopup: function() {
let window = windowTracker.topWindow;
this.triggerAction(window);
pageAction.triggerAction(window);
},
},
};

View File

@ -4,6 +4,7 @@
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
@ -17,6 +18,7 @@ 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

View File

@ -22,11 +22,11 @@ this.pageActionExtras = class extends ExtensionAPI {
return {
pageActionExtras: {
async setDefaultTitle(title) {
pageActionAPI.action.getContextData(null).title = title;
pageActionAPI.defaults.title = title;
// Make sure the new default title is considered right away
for (const window of windowTracker.browserWindows()) {
const tab = window.gBrowser.selectedTab;
if (pageActionAPI.action.isShownForTab(tab)) {
if (pageActionAPI.isShown(tab)) {
pageActionAPI.updateButton(window);
}
}

View File

@ -6,10 +6,8 @@
#include "nsISupports.idl"
interface nsIChannel;
interface nsIEventTarget;
interface nsIInputStream;
interface nsIURI;
interface imgIContainer;
interface imgILoader;
interface imgICache;
@ -54,29 +52,6 @@ interface imgITools : nsISupports
imgIContainer decodeImageFromArrayBuffer(in jsval aArrayBuffer,
in ACString aMimeType);
/**
* decodeImageFromChannelAsync
* See decodeImage. The main difference between this method and decodeImage
* is that here the operation is done async on a thread from the decode
* pool. When the operation is completed, the callback is executed with the
* result.
*
* @param aURI
* The original URI of the image
* @param aChannel
* Channel to the image to be decoded.
* @param aCallback
* The callback is executed when the imgContainer is fully created.
* @param aObserver
* Optional observer for the decoded image, the caller should make
* sure the observer is kept alive as long as necessary, as ImageLib
* does not keep a strong reference to the observer.
*/
void decodeImageFromChannelAsync(in nsIURI aURI,
in nsIChannel aChannel,
in imgIContainerCallback aCallback,
in imgINotificationObserver aObserver);
/**
* decodeImageAsync
* See decodeImage. The main difference between this method and decodeImage

View File

@ -23,10 +23,8 @@
#include "nsStringStream.h"
#include "nsContentUtils.h"
#include "nsProxyRelease.h"
#include "nsIStreamListener.h"
#include "ImageFactory.h"
#include "Image.h"
#include "IProgressObserver.h"
#include "ScriptedNotificationObserver.h"
#include "imgIScriptedNotificationObserver.h"
#include "gfxPlatform.h"
@ -41,145 +39,6 @@ namespace image {
namespace {
static nsresult sniff_mimetype_callback(nsIInputStream* in, void* data,
const char* fromRawSegment,
uint32_t toOffset, uint32_t count,
uint32_t* writeCount) {
nsCString* mimeType = static_cast<nsCString*>(data);
MOZ_ASSERT(mimeType, "mimeType is null!");
if (count > 0) {
imgLoader::GetMimeTypeFromContent(fromRawSegment, count, *mimeType);
}
*writeCount = 0;
return NS_ERROR_FAILURE;
}
// Provides WeakPtr for imgINotificationObserver
class NotificationObserverWrapper : public imgINotificationObserver,
public mozilla::SupportsWeakPtr<NotificationObserverWrapper> {
public:
NS_DECL_ISUPPORTS
NS_FORWARD_IMGINOTIFICATIONOBSERVER(mObserver->)
MOZ_DECLARE_WEAKREFERENCE_TYPENAME(nsGeolocationRequest)
explicit NotificationObserverWrapper(imgINotificationObserver* observer) : mObserver(observer) {}
private:
virtual ~NotificationObserverWrapper() = default;
nsCOMPtr<imgINotificationObserver> mObserver;
};
NS_IMPL_ISUPPORTS(NotificationObserverWrapper, imgINotificationObserver)
class ImageDecoderListener final : public nsIStreamListener,
public IProgressObserver,
public imgIContainer {
public:
NS_DECL_ISUPPORTS
ImageDecoderListener(nsIURI* aURI, imgIContainerCallback* aCallback,
imgINotificationObserver* aObserver)
: mURI(aURI),
mImage(nullptr),
mCallback(aCallback),
mObserver(new NotificationObserverWrapper(aObserver)) {
MOZ_ASSERT(NS_IsMainThread());
}
NS_IMETHOD
OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aInputStream,
uint64_t aOffset, uint32_t aCount) override {
if (!mImage) {
nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
nsCString mimeType;
channel->GetContentType(mimeType);
if (aInputStream) {
// Look at the first few bytes and see if we can tell what the data is from
// that since servers tend to lie. :(
uint32_t unused;
aInputStream->ReadSegments(sniff_mimetype_callback, &mimeType, aCount, &unused);
}
RefPtr<ProgressTracker> tracker = new ProgressTracker();
if (mObserver) {
tracker->AddObserver(this);
}
mImage = ImageFactory::CreateImage(channel, tracker, mimeType, mURI,
/* aIsMultiPart */ false, 0);
if (mImage->HasError()) {
return NS_ERROR_FAILURE;
}
}
return mImage->OnImageDataAvailable(aRequest, nullptr, aInputStream,
aOffset, aCount);
}
NS_IMETHOD
OnStartRequest(nsIRequest* aRequest) override {
return NS_OK;
}
NS_IMETHOD
OnStopRequest(nsIRequest* aRequest, nsresult aStatus) override {
// Depending on the error, we might not have received any data yet, in which case we would not
// have an |mImage|
if (mImage) {
mImage->OnImageDataComplete(aRequest, nullptr, aStatus, true);
}
nsCOMPtr<imgIContainer> container;
if (NS_SUCCEEDED(aStatus)) {
container = this;
}
mCallback->OnImageReady(container, aStatus);
return NS_OK;
}
virtual void Notify(int32_t aType,
const nsIntRect* aRect = nullptr) override {
if (mObserver) {
mObserver->Notify(nullptr, aType, aRect);
}
}
virtual void OnLoadComplete(bool aLastPart) override {}
// Other notifications are ignored.
virtual void SetHasImage() override {}
virtual bool NotificationsDeferred() const override { return false; }
virtual void MarkPendingNotify() override {}
virtual void ClearPendingNotify() override {}
// imgIContainer
NS_FORWARD_IMGICONTAINER(mImage->)
nsresult GetNativeSizes(nsTArray<nsIntSize>& aNativeSizes) const override {
return mImage->GetNativeSizes(aNativeSizes);
}
size_t GetNativeSizesLength() const override {
return mImage->GetNativeSizesLength();
}
private:
virtual ~ImageDecoderListener() = default;
nsCOMPtr<nsIURI> mURI;
RefPtr<image::Image> mImage;
nsCOMPtr<imgIContainerCallback> mCallback;
WeakPtr<NotificationObserverWrapper> mObserver;
};
NS_IMPL_ISUPPORTS(ImageDecoderListener, nsIStreamListener, imgIContainer)
class ImageDecoderHelper final : public Runnable,
public nsIInputStreamCallback {
public:
@ -375,22 +234,6 @@ imgTools::DecodeImageFromBuffer(const char* aBuffer, uint32_t aSize,
return NS_OK;
}
NS_IMETHODIMP
imgTools::DecodeImageFromChannelAsync(nsIURI* aURI, nsIChannel* aChannel,
imgIContainerCallback* aCallback,
imgINotificationObserver* aObserver) {
MOZ_ASSERT(NS_IsMainThread());
NS_ENSURE_ARG_POINTER(aURI);
NS_ENSURE_ARG_POINTER(aChannel);
NS_ENSURE_ARG_POINTER(aCallback);
RefPtr<ImageDecoderListener> listener =
new ImageDecoderListener(aURI, aCallback, aObserver);
return aChannel->AsyncOpen(listener);
}
NS_IMETHODIMP
imgTools::DecodeImageAsync(nsIInputStream* aInStr, const nsACString& aMimeType,
imgIContainerCallback* aCallback,

View File

@ -197,11 +197,6 @@ var ModuleManager = {
Object.assign(this._settings, aSettings);
this._frozenSettings = Object.freeze(Object.assign({}, this._settings));
const windowType = aSettings.isPopup
? "navigator:popup"
: "navigator:geckoview";
window.document.documentElement.setAttribute("windowType", windowType);
this.forEach(module => {
if (module.impl) {
module.impl.onSettingsUpdate();

View File

@ -1,5 +1,11 @@
"use strict";
ChromeUtils.defineModuleGetter(
this,
"Services",
"resource://gre/modules/Services.jsm"
);
// This function is pretty tightly tied to Extension.jsm.
// Its job is to fill in the |tab| property of the sender.
const getSender = (extension, target, sender) => {
@ -68,7 +74,7 @@ global.openOptionsPage = extension => {
extensions.registerModules({
browserAction: {
url: "chrome://geckoview/content/ext-browserAction.js",
schema: "chrome://extensions/content/schemas/browser_action.json",
schema: "chrome://geckoview/content/schemas/browser_action.json",
scopes: ["addon_parent"],
manifest: ["browser_action"],
paths: [["browserAction"]],
@ -82,7 +88,7 @@ extensions.registerModules({
},
pageAction: {
url: "chrome://geckoview/content/ext-pageAction.js",
schema: "chrome://extensions/content/schemas/page_action.json",
schema: "chrome://geckoview/content/schemas/page_action.json",
scopes: ["addon_parent"],
manifest: ["page_action"],
paths: [["pageAction"]],
@ -93,7 +99,12 @@ extensions.registerModules({
scopes: ["addon_parent"],
paths: [["tabs"]],
},
geckoViewAddons: {
schema: "chrome://geckoview/content/schemas/gecko_view_addons.json",
},
});
if (!Services.androidBridge.isFennec) {
extensions.registerModules({
geckoViewAddons: {
schema: "chrome://geckoview/content/schemas/gecko_view_addons.json",
},
});
}

View File

@ -5,124 +5,199 @@
// 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"
// Import the android BrowserActions module.
ChromeUtils.defineModuleGetter(
this,
"BrowserActions",
"resource://gre/modules/BrowserActions.jsm"
);
const BROWSER_ACTION_PROPERTIES = [
"title",
"icon",
"popup",
"badgeText",
"badgeBackgroundColor",
"badgeTextColor",
"enabled",
"patternMatching",
];
// WeakMap[Extension -> BrowserAction]
let browserActionMap = new WeakMap();
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,
class BrowserAction extends EventEmitter {
constructor(options, extension) {
super();
this.uuid = `{${extension.uuid}}`;
this.defaults = {
name: options.default_title || extension.name,
popup: options.default_popup,
};
this.tabContext = new TabContext(tabId => this.defaults);
this.tabManager = extension.tabManager;
// eslint-disable-next-line mozilla/balanced-listeners
this.tabContext.on("tab-selected", (evt, tabId) => {
this.onTabSelected(tabId);
});
// eslint-disable-next-line mozilla/balanced-listeners
this.tabContext.on("tab-closed", (evt, tabId) => {
this.onTabClosed(tabId);
});
BrowserActions.register(this);
}
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() {
/**
* Required by the BrowserActions module. This event will get
* called whenever the browser action is clicked on.
*/
onClicked() {
const tab = tabTracker.activeTab;
const action = this.getContextData(tab);
this.helper.sendRequest(tab.id, {
action,
type: "GeckoView:BrowserAction:OpenPopup",
});
this.tabManager.addActiveTabPermission(tab);
let popup = this.tabContext.get(tab.id).popup || this.defaults.popup;
if (popup) {
tabTracker.openExtensionPopupTab(popup);
} else {
this.emit("click", tab);
}
}
getTab(tabId) {
return this.helper.getTab(tabId);
/**
* Updates the browser action whenever a tab is selected.
* @param {string} tabId The tab id to update.
*/
onTabSelected(tabId) {
let name = this.tabContext.get(tabId).name || this.defaults.name;
BrowserActions.update(this.uuid, { name });
}
getWindow(windowId) {
return this.helper.getWindow(windowId);
/**
* Removes the tab from the property map now that it is closed.
* @param {string} tabId The tab id of the closed tab.
*/
onTabClosed(tabId) {
this.tabContext.clear(tabId);
}
click() {
this.clickDelegate.onClick();
/**
* Sets a property for the browser action for the specified tab. If the property is set
* for the active tab, the browser action is also updated.
*
* @param {Object} tab The tab to set. If null, the browser action's default value is set.
* @param {string} prop The property to update. Currently only "name" is supported.
* @param {string} value The value to set the property to.
*/
setProperty(tab, prop, value) {
if (tab == null) {
if (value) {
this.defaults[prop] = value;
}
} else {
let properties = this.tabContext.get(tab.id);
if (value) {
properties[prop] = value;
} else {
delete properties[prop];
}
}
if (!tab || tab.getActive()) {
BrowserActions.update(this.uuid, { [prop]: value });
}
}
/**
* Retreives a property of the browser action for the specified tab.
*
* @param {Object} tab The tab to retrieve the property from. If null, the default value is returned.
* @param {string} prop The property to retreive. Currently only "name" is supported.
* @returns {string} the value stored for the specified property. If the value is undefined, then the
* default value is returned.
*/
getProperty(tab, prop) {
if (tab == null) {
return this.defaults[prop];
}
return this.tabContext.get(tab.id)[prop] || this.defaults[prop];
}
/**
* Unregister the browser action from the BrowserActions module.
*/
shutdown() {
this.tabContext.shutdown();
BrowserActions.unregister(this.uuid);
}
}
this.browserAction = class extends ExtensionAPI {
async onManifestEntry(entryName) {
const { extension } = this;
this.action = new BrowserAction(extension, this);
await this.action.loadIconData();
onManifestEntry(entryName) {
let { extension } = this;
let { manifest } = extension;
GeckoViewWebExtension.browserActions.set(extension, this.action);
// Notify the embedder of this action
this.action.updateOnChange(null);
let browserAction = new BrowserAction(manifest.browser_action, extension);
browserActionMap.set(extension, browserAction);
}
onShutdown() {
const { extension } = this;
this.action.onShutdown();
GeckoViewWebExtension.browserActions.delete(extension);
}
let { extension } = this;
onClick() {
this.emit("click", tabTracker.activeTab);
if (browserActionMap.has(extension)) {
browserActionMap.get(extension).shutdown();
browserActionMap.delete(extension);
}
}
getAPI(context) {
const { extension } = context;
const { tabManager } = extension;
const { action } = this;
function getTab(tabId) {
if (tabId !== null) {
return tabTracker.getTab(tabId);
}
return null;
}
return {
browserAction: {
...action.api(context),
onClicked: new EventManager({
context,
name: "browserAction.onClicked",
register: fire => {
const listener = (event, tab) => {
let listener = (event, tab) => {
fire.async(tabManager.convert(tab));
};
this.on("click", listener);
browserActionMap.get(extension).on("click", listener);
return () => {
this.off("click", listener);
browserActionMap.get(extension).off("click", listener);
};
},
}).api(),
openPopup: function() {
action.openPopup();
setTitle: function(details) {
let { tabId, title } = details;
let tab = getTab(tabId);
browserActionMap.get(extension).setProperty(tab, "name", title);
},
getTitle: function(details) {
let { tabId } = details;
let tab = getTab(tabId);
let title = browserActionMap.get(extension).getProperty(tab, "name");
return Promise.resolve(title);
},
setPopup(details) {
let tab = getTab(details.tabId);
let url = details.popup && context.uri.resolve(details.popup);
browserActionMap.get(extension).setProperty(tab, "popup", url);
},
getPopup(details) {
let tab = getTab(details.tabId);
let popup = browserActionMap.get(extension).getProperty(tab, "popup");
return Promise.resolve(popup);
},
},
};
}
};
global.browserActionFor = this.browserAction.for;

View File

@ -6,115 +6,285 @@
// 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"
ChromeUtils.defineModuleGetter(
this,
"Services",
"resource://gre/modules/Services.jsm"
);
const PAGE_ACTION_PROPERTIES = [
"title",
"icon",
"popup",
"badgeText",
"enabled",
"patternMatching",
];
// Import the android PageActions module.
ChromeUtils.defineModuleGetter(
this,
"PageActions",
"resource://gre/modules/PageActions.jsm"
);
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,
var { ExtensionParent } = ChromeUtils.import(
"resource://gre/modules/ExtensionParent.jsm"
);
var { IconDetails } = ExtensionParent;
// WeakMap[Extension -> PageAction]
let pageActionMap = new WeakMap();
class PageAction extends EventEmitter {
constructor(manifest, extension) {
super();
this.id = null;
this.extension = extension;
this.defaults = {
icons: IconDetails.normalize({ path: manifest.default_icon }, extension),
popup: manifest.default_popup,
};
this.tabManager = extension.tabManager;
this.context = null;
this.tabContext = new TabContext(tabId => this.defaults);
this.options = {
title: manifest.default_title || extension.name,
id: `{${extension.uuid}}`,
clickCallback: () => {
let tab = tabTracker.activeTab;
this.tabManager.addActiveTabPermission(tab);
let popup = this.tabContext.get(tab.id).popup || this.defaults.popup;
if (popup) {
tabTracker.openExtensionPopupTab(popup);
} else {
this.emit("click", tab);
}
},
};
this.shouldShow = false;
// eslint-disable-next-line mozilla/balanced-listeners
this.tabContext.on("tab-selected", (evt, tabId) => {
this.onTabSelected(tabId);
});
// eslint-disable-next-line mozilla/balanced-listeners
this.tabContext.on("tab-closed", (evt, tabId) => {
this.onTabClosed(tabId);
});
}
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",
});
/**
* Updates the page action whenever a tab is selected.
* @param {Integer} tabId The ID of the selected tab.
*/
onTabSelected(tabId) {
if (this.options.icon) {
this.hide();
let shouldShow = this.tabContext.get(tabId).show;
if (shouldShow) {
this.show();
}
}
}
openPopup() {
const action = this.getContextData(tabTracker.activeTab);
this.helper.sendRequest(tabTracker.activeTab.id, {
action,
type: "GeckoView:PageAction:OpenPopup",
});
/**
* Removes the tab from the property map now that it is closed.
* @param {Integer} tabId The ID of the closed tab.
*/
onTabClosed(tabId) {
this.tabContext.clear(tabId);
}
getTab(tabId) {
return this.helper.getTab(tabId);
/**
* Sets the context for the page action.
* @param {Object} context The extension context.
*/
setContext(context) {
this.context = context;
}
click() {
this.clickDelegate.onClick();
/**
* Sets a property for the page action for the specified tab. If the property is set
* for the active tab, the page action is also updated.
*
* @param {Object} tab The tab to set.
* @param {string} prop The property to update - either "show" or "popup".
* @param {string} value The value to set the property to. If falsy, the property is deleted.
* @returns {Object} Promise which resolves when the property is set and the page action is
* shown if necessary.
*/
setProperty(tab, prop, value) {
if (tab == null) {
throw new Error("Tab must not be null");
}
let properties = this.tabContext.get(tab.id);
if (value) {
properties[prop] = value;
} else {
delete properties[prop];
}
if (prop === "show" && tab.id == tabTracker.activeTab.id) {
if (this.id && !value) {
return this.hide();
} else if (!this.id && value) {
return this.show();
}
}
}
/**
* Retreives a property of the page action for the specified tab.
*
* @param {Object} tab The tab to retrieve the property from. If null, the default value is returned.
* @param {string} prop The property to retreive - currently only "popup" is supported.
* @returns {string} the value stored for the specified property. If the value for the tab is undefined, then the
* default value is returned.
*/
getProperty(tab, prop) {
if (tab == null) {
return this.defaults[prop];
}
return this.tabContext.get(tab.id)[prop] || this.defaults[prop];
}
/**
* Show the page action for the active tab.
* @returns {Promise} resolves when the page action is shown.
*/
show() {
// The PageAction icon has been created or it is being converted.
if (this.id || this.shouldShow) {
return Promise.resolve();
}
if (this.options.icon) {
this.id = PageActions.add(this.options);
return Promise.resolve();
}
this.shouldShow = true;
// Bug 1372782: Remove dependency on contentWindow from this file. It should
// be put in a separate file called ext-c-pageAction.js.
// Note: Fennec is not going to be multi-process for the foreseaable future,
// so this layering violation has no immediate impact. However, it is should
// be done at some point.
let { contentWindow } = this.context.xulBrowser;
// Bug 1372783: Why is this contentWindow.devicePixelRatio, while
// convertImageURLToDataURL uses browserWindow.devicePixelRatio?
let { icon } = IconDetails.getPreferredIcon(
this.defaults.icons,
this.extension,
16 * contentWindow.devicePixelRatio
);
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
return IconDetails.convertImageURLToDataURL(
icon,
contentWindow,
browserWindow
)
.then(dataURI => {
if (this.shouldShow) {
this.options.icon = dataURI;
this.id = PageActions.add(this.options);
}
})
.catch(() => {
// The "icon conversion" promise has been rejected, set `this.shouldShow` to `false`
// so that we will try again on the next `pageAction.show` call.
this.shouldShow = false;
return Promise.reject({
message: "Failed to load PageAction icon",
});
});
}
/**
* Hides the page action for the active tab.
*/
hide() {
this.shouldShow = false;
if (this.id) {
PageActions.remove(this.id);
this.id = null;
}
}
shutdown() {
this.tabContext.shutdown();
this.hide();
}
}
this.pageAction = class extends ExtensionAPI {
async onManifestEntry(entryName) {
const { extension } = this;
const action = new PageAction(extension, this);
await action.loadIconData();
this.action = action;
onManifestEntry(entryName) {
let { extension } = this;
let { manifest } = extension;
GeckoViewWebExtension.pageActions.set(extension, action);
// Notify the embedder of this action
action.updateOnChange(null);
}
onClick() {
this.emit("click", tabTracker.activeTab);
let pageAction = new PageAction(manifest.page_action, extension);
pageActionMap.set(extension, pageAction);
}
onShutdown() {
const { extension, action } = this;
action.onShutdown();
GeckoViewWebExtension.pageActions.delete(extension);
let { extension } = this;
if (pageActionMap.has(extension)) {
pageActionMap.get(extension).shutdown();
pageActionMap.delete(extension);
}
}
getAPI(context) {
const { extension } = context;
const { tabManager } = extension;
const { action } = this;
pageActionMap.get(extension).setContext(context);
return {
pageAction: {
...action.api(context),
onClicked: new EventManager({
context,
name: "pageAction.onClicked",
register: fire => {
const listener = (event, tab) => {
let listener = (event, tab) => {
fire.async(tabManager.convert(tab));
};
this.on("click", listener);
pageActionMap.get(extension).on("click", listener);
return () => {
this.off("click", listener);
pageActionMap.get(extension).off("click", listener);
};
},
}).api(),
openPopup() {
action.openPopup();
show(tabId) {
let tab = tabTracker.getTab(tabId);
return pageActionMap.get(extension).setProperty(tab, "show", true);
},
hide(tabId) {
let tab = tabTracker.getTab(tabId);
pageActionMap.get(extension).setProperty(tab, "show", false);
},
setPopup(details) {
let tab = tabTracker.getTab(details.tabId);
let url = details.popup && context.uri.resolve(details.popup);
pageActionMap.get(extension).setProperty(tab, "popup", url);
},
getPopup(details) {
let tab = tabTracker.getTab(details.tabId);
let popup = pageActionMap.get(extension).getProperty(tab, "popup");
return Promise.resolve(popup);
},
},
};

View File

@ -38,7 +38,9 @@ const BrowserStatusFilter = Components.Constructor(
"addProgressListener"
);
const WINDOW_TYPE = "navigator:geckoview";
const WINDOW_TYPE = Services.androidBridge.isFennec
? "navigator:browser"
: "navigator:geckoview";
let tabTracker;
let windowTracker;
@ -100,7 +102,7 @@ class BrowserProgressListener {
const PROGRESS_LISTENER_FLAGS =
Ci.nsIWebProgress.NOTIFY_STATE_ALL | Ci.nsIWebProgress.NOTIFY_LOCATION;
class ProgressListenerWrapper {
class GeckoViewProgressListenerWrapper {
constructor(window, listener) {
this.listener = new BrowserProgressListener(
window.BrowserApp.selectedBrowser,
@ -114,6 +116,92 @@ class ProgressListenerWrapper {
}
}
/**
* Handles wrapping a tab progress listener in browser-specific
* BrowserProgressListener instances, an attaching them to each tab in a given
* browser window.
*
* @param {DOMWindow} window
* The browser window to which to attach the listeners.
* @param {object} listener
* The tab progress listener to wrap.
*/
class FennecProgressListenerWrapper {
constructor(window, listener) {
this.window = window;
this.listener = listener;
this.listeners = new WeakMap();
for (let nativeTab of this.window.BrowserApp.tabs) {
this.addBrowserProgressListener(nativeTab.browser);
}
this.window.BrowserApp.deck.addEventListener("TabOpen", this);
}
/**
* Destroy the wrapper, removing any remaining listeners it has added.
*/
destroy() {
this.window.BrowserApp.deck.removeEventListener("TabOpen", this);
for (let nativeTab of this.window.BrowserApp.tabs) {
this.removeProgressListener(nativeTab.browser);
}
}
/**
* Adds a progress listener to the given XUL browser element.
*
* @param {XULElement} browser
* The XUL browser to add the listener to.
* @private
*/
addBrowserProgressListener(browser) {
this.removeProgressListener(browser);
let listener = new BrowserProgressListener(
browser,
this.listener,
this.flags
);
this.listeners.set(browser, listener);
}
/**
* Removes a progress listener from the given XUL browser element.
*
* @param {XULElement} browser
* The XUL browser to remove the listener from.
* @private
*/
removeProgressListener(browser) {
let listener = this.listeners.get(browser);
if (listener) {
listener.destroy();
this.listeners.delete(browser);
}
}
/**
* Handles tab open events, and adds the necessary progress listeners to the
* new tabs.
*
* @param {Event} event
* The DOM event to handle.
* @private
*/
handleEvent(event) {
if (event.type === "TabOpen") {
this.addBrowserProgressListener(event.originalTarget);
}
}
}
const ProgressListenerWrapper = Services.androidBridge.isFennec
? FennecProgressListenerWrapper
: GeckoViewProgressListenerWrapper;
class WindowTracker extends WindowTrackerBase {
constructor(...args) {
super(...args);
@ -195,7 +283,7 @@ global.makeGlobalEvent = function makeGlobalEvent(
}).api();
};
class TabTracker extends TabTrackerBase {
class GeckoViewTabTracker extends TabTrackerBase {
init() {
if (this.initialized) {
return;
@ -268,8 +356,242 @@ class TabTracker extends TabTrackerBase {
}
}
class FennecTabTracker extends TabTrackerBase {
constructor() {
super();
// Keep track of the extension popup tab.
this._extensionPopupTabWeak = null;
// Keep track of the selected tabId
this._selectedTabId = null;
}
init() {
if (this.initialized) {
return;
}
this.initialized = true;
windowTracker.addListener("TabClose", this);
windowTracker.addListener("TabOpen", this);
// Register a listener for the Tab:Selected global event,
// so that we can close the popup when a popup tab has been
// unselected.
GlobalEventDispatcher.registerListener(this, ["Tab:Selected"]);
}
/**
* Returns the currently opened popup tab if any
*/
get extensionPopupTab() {
if (this._extensionPopupTabWeak) {
const tab = this._extensionPopupTabWeak.get();
// Return the native tab only if the tab has not been removed in the meantime.
if (tab.browser) {
return tab;
}
// Clear the tracked popup tab if it has been closed in the meantime.
this._extensionPopupTabWeak = null;
}
return undefined;
}
/**
* Open a pageAction/browserAction popup url in a tab and keep track of
* its weak reference (to be able to customize the activedTab using the tab parentId,
* to skip it in the tabs.query and to set the parent tab as active when the popup
* tab is currently selected).
*
* @param {string} popup
* The popup url to open in a tab.
*/
openExtensionPopupTab(popup) {
let win = windowTracker.topWindow;
if (!win) {
throw new ExtensionError(
`Unable to open a popup without an active window`
);
}
if (this.extensionPopupTab) {
win.BrowserApp.closeTab(this.extensionPopupTab);
}
this.init();
let { browser, id } = win.BrowserApp.selectedTab;
let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser);
this._extensionPopupTabWeak = Cu.getWeakReference(
win.BrowserApp.addTab(popup, {
selected: true,
parentId: id,
isPrivate,
})
);
}
getId(nativeTab) {
return nativeTab.id;
}
getTab(id, default_ = undefined) {
let win = windowTracker.topWindow;
if (win) {
let nativeTab = win.BrowserApp.getTabForId(id);
if (nativeTab) {
return nativeTab;
}
}
if (default_ !== undefined) {
return default_;
}
throw new ExtensionError(`Invalid tab ID: ${id}`);
}
/**
* Handles tab open and close events, and emits the appropriate internal
* events for them.
*
* @param {Event} event
* A DOM event to handle.
* @private
*/
handleEvent(event) {
const { BrowserApp } = event.target.ownerGlobal;
const nativeTab = BrowserApp.getTabForBrowser(event.target);
switch (event.type) {
case "TabOpen":
this.emitCreated(nativeTab);
break;
case "TabClose":
this.emitRemoved(nativeTab, false);
break;
}
}
/**
* Required by the GlobalEventDispatcher module. This event will get
* called whenever one of the registered listeners fires.
* @param {string} event The event which fired.
* @param {object} data Information about the event which fired.
*/
onEvent(event, data) {
const { BrowserApp } = windowTracker.topWindow;
switch (event) {
case "Tab:Selected": {
this._selectedTabId = data.id;
// If a new tab has been selected while an extension popup tab is still open,
// close it immediately.
const nativeTab = BrowserApp.getTabForId(data.id);
const popupTab = tabTracker.extensionPopupTab;
if (popupTab && popupTab !== nativeTab) {
BrowserApp.closeTab(popupTab);
}
break;
}
}
}
/**
* Emits a "tab-created" event for the given tab element.
*
* @param {NativeTab} nativeTab
* The tab element which is being created.
* @private
*/
emitCreated(nativeTab) {
this.emit("tab-created", { nativeTab });
}
/**
* Emits a "tab-removed" event for the given tab element.
*
* @param {NativeTab} nativeTab
* The tab element which is being removed.
* @param {boolean} isWindowClosing
* True if the tab is being removed because the browser window is
* closing.
* @private
*/
emitRemoved(nativeTab, isWindowClosing) {
let windowId = windowTracker.getId(nativeTab.browser.ownerGlobal);
let tabId = this.getId(nativeTab);
if (this.extensionPopupTab && this.extensionPopupTab === nativeTab) {
this._extensionPopupTabWeak = null;
// Do not switch to the parent tab of the extension popup tab
// if the popup tab is not the selected tab.
if (this._selectedTabId !== tabId) {
return;
}
// Select the parent tab when the closed tab was an extension popup tab.
const { BrowserApp } = windowTracker.topWindow;
const popupParentTab = BrowserApp.getTabForId(nativeTab.parentId);
if (popupParentTab) {
BrowserApp.selectTab(popupParentTab);
}
}
Services.tm.dispatchToMainThread(() => {
this.emit("tab-removed", { nativeTab, tabId, windowId, isWindowClosing });
});
}
getBrowserData(browser) {
let result = {
tabId: -1,
windowId: -1,
};
let { BrowserApp } = browser.ownerGlobal;
if (BrowserApp) {
result.windowId = windowTracker.getId(browser.ownerGlobal);
let nativeTab = BrowserApp.getTabForBrowser(browser);
if (nativeTab) {
result.tabId = this.getId(nativeTab);
}
}
return result;
}
get activeTab() {
let win = windowTracker.topWindow;
if (win && win.BrowserApp) {
const selectedTab = win.BrowserApp.selectedTab;
// If the current tab is an extension popup tab, we use the parentId to retrieve
// and return the tab that was selected when the popup tab has been opened.
if (selectedTab === this.extensionPopupTab) {
return win.BrowserApp.getTabForId(selectedTab.parentId);
}
return selectedTab;
}
return null;
}
}
windowTracker = new WindowTracker();
tabTracker = new TabTracker();
if (Services.androidBridge.isFennec) {
tabTracker = new FennecTabTracker();
} else {
tabTracker = new GeckoViewTabTracker();
}
Object.assign(global, { tabTracker, windowTracker });
@ -404,35 +726,13 @@ class TabContext extends EventEmitter {
constructor(getDefaultPrototype) {
super();
windowTracker.addListener("progress", this);
this.getDefaultPrototype = getDefaultPrototype;
this.tabData = new Map();
}
onLocationChange(browser, webProgress, request, locationURI, flags) {
if (!webProgress.isTopLevel) {
// Only pageAction and browserAction are consuming the "location-change" event
// to update their per-tab status, and they should only do so in response of
// location changes related to the top level frame (See Bug 1493470 for a rationale).
return;
}
const gBrowser = browser.ownerGlobal.gBrowser;
const tab = gBrowser.getTabForBrowser(browser);
// fromBrowse will be false in case of e.g. a hash change or history.pushState
const fromBrowse = !(
flags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
);
this.emit(
"location-change",
{
id: tab.id,
linkedBrowser: browser,
// TODO: we don't support selected so we just alway say we are
selected: true,
},
fromBrowse
);
GlobalEventDispatcher.registerListener(this, [
"Tab:Selected",
"Tab:Closed",
]);
}
get(tabId) {
@ -448,8 +748,28 @@ class TabContext extends EventEmitter {
this.tabData.delete(tabId);
}
/**
* Required by the GlobalEventDispatcher module. This event will get
* called whenever one of the registered listeners fires.
* @param {string} event The event which fired.
* @param {object} data Information about the event which fired.
*/
onEvent(event, data) {
switch (event) {
case "Tab:Selected":
this.emit("tab-selected", data.id);
break;
case "Tab:Closed":
this.emit("tab-closed", data.tabId);
break;
}
}
shutdown() {
windowTracker.removeListener("progress", this);
GlobalEventDispatcher.unregisterListener(this, [
"Tab:Selected",
"Tab:Closed",
]);
}
}

View File

@ -0,0 +1,448 @@
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
[
{
"namespace": "manifest",
"types": [
{
"$extend": "WebExtensionManifest",
"properties": {
"browser_action": {
"type": "object",
"additionalProperties": { "$ref": "UnrecognizedProperty" },
"properties": {
"default_title": {
"type": "string",
"optional": true,
"preprocess": "localize"
},
"default_icon": {
"$ref": "IconPath",
"deprecated": "Unsupported on Android.",
"optional": true
},
"default_popup": {
"type": "string",
"format": "relativeUrl",
"optional": true,
"preprocess": "localize"
},
"browser_style": {
"type": "boolean",
"deprecated": "Unsupported on Android.",
"optional": true,
"default": false
},
"default_area": {
"description": "Defines the location the browserAction will appear by default. The default location is navbar.",
"type": "string",
"enum": ["navbar", "menupanel", "tabstrip", "personaltoolbar"],
"deprecated": "Unsupported on Android.",
"optional": true
}
},
"optional": true
}
}
}
]
},
{
"namespace": "browserAction",
"description": "Use browser actions to put icons in the main browser toolbar, to the right of the address bar. In addition to its icon, a browser action can also have a tooltip, a badge, and a popup.",
"permissions": ["manifest:browser_action"],
"types": [
{
"id": "ColorArray",
"type": "array",
"items": {
"type": "integer",
"minimum": 0,
"maximum": 255
},
"minItems": 4,
"maxItems": 4
},
{
"id": "ImageDataType",
"type": "object",
"isInstanceOf": "ImageData",
"additionalProperties": { "type": "any" },
"postprocess": "convertImageDataToURL",
"description": "Pixel data for an image. Must be an ImageData object (for example, from a <code>canvas</code> element)."
}
],
"functions": [
{
"name": "setTitle",
"type": "function",
"description": "Sets the title of the browser action. This shows up in the tooltip.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "The string the browser action should display when moused over."
},
"tabId": {
"type": "integer",
"optional": true,
"description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "getTitle",
"type": "function",
"description": "Gets the title of the browser action.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {
"type": "integer",
"optional": true,
"description": "Specify the tab to get the title from. If no tab is specified, the non-tab-specific title is returned."
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "result",
"type": "string"
}
]
}
]
},
{
"name": "setIcon",
"unsupported": true,
"type": "function",
"description": "Sets the icon for the browser action. The icon can be specified either as the path to an image file or as the pixel data from a canvas element, or as dictionary of either one of those. Either the <b>path</b> or the <b>imageData</b> property must be specified.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"imageData": {
"choices": [
{ "$ref": "ImageDataType" },
{
"type": "object",
"additionalProperties": {"$ref": "ImageDataType"}
}
],
"optional": true,
"description": "Either an ImageData object or a dictionary {size -> ImageData} representing icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then image with size <code>scale</code> * 19 will be selected. Initially only scales 1 and 2 will be supported. At least one image must be specified. Note that 'details.imageData = foo' is equivalent to 'details.imageData = {'19': foo}'"
},
"path": {
"choices": [
{ "type": "string" },
{
"type": "object",
"additionalProperties": {"type": "string"}
}
],
"optional": true,
"description": "Either a relative image path or a dictionary {size -> relative image path} pointing to icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then image with size <code>scale</code> * 19 will be selected. Initially only scales 1 and 2 will be supported. At least one image must be specified. Note that 'details.path = foo' is equivalent to 'details.imageData = {'19': foo}'"
},
"tabId": {
"type": "integer",
"optional": true,
"description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "setPopup",
"type": "function",
"description": "Sets the html document to be opened as a popup when the user clicks on the browser action's icon.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {
"type": "integer",
"optional": true,
"minimum": 0,
"description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
},
"popup": {
"type": "string",
"description": "The html file to show in a popup. If set to the empty string (''), no popup is shown."
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "getPopup",
"type": "function",
"description": "Gets the html document set as the popup for this browser action.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {
"type": "integer",
"optional": true,
"minimum": 0,
"description": "Specify the tab to get the popup from. If no tab is specified, the non-tab-specific popup is returned."
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "result",
"type": "string"
}
]
}
]
},
{
"name": "setBadgeText",
"unsupported": true,
"type": "function",
"description": "Sets the badge text for the browser action. The badge is displayed on top of the icon.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "Any number of characters can be passed, but only about four can fit in the space."
},
"tabId": {
"type": "integer",
"optional": true,
"description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "getBadgeText",
"unsupported": true,
"type": "function",
"description": "Gets the badge text of the browser action. If no tab is specified, the non-tab-specific badge text is returned.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {
"type": "integer",
"optional": true,
"description": "Specify the tab to get the badge text from. If no tab is specified, the non-tab-specific badge text is returned."
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "result",
"type": "string"
}
]
}
]
},
{
"name": "setBadgeBackgroundColor",
"unsupported": true,
"type": "function",
"description": "Sets the background color for the badge.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"color": {
"description": "An array of four integers in the range [0,255] that make up the RGBA color of the badge. For example, opaque red is <code>[255, 0, 0, 255]</code>. Can also be a string with a CSS value, with opaque red being <code>#FF0000</code> or <code>#F00</code>.",
"choices": [
{"type": "string"},
{"$ref": "ColorArray"}
]
},
"tabId": {
"type": "integer",
"optional": true,
"description": "Limits the change to when a particular tab is selected. Automatically resets when the tab is closed."
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "getBadgeBackgroundColor",
"unsupported": true,
"type": "function",
"description": "Gets the background color of the browser action.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {
"type": "integer",
"optional": true,
"description": "Specify the tab to get the badge background color from. If no tab is specified, the non-tab-specific badge background color is returned."
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "result",
"$ref": "ColorArray"
}
]
}
]
},
{
"name": "enable",
"unsupported": true,
"type": "function",
"description": "Enables the browser action for a tab. By default, browser actions are enabled.",
"async": "callback",
"parameters": [
{
"type": "integer",
"optional": true,
"name": "tabId",
"minimum": 0,
"description": "The id of the tab for which you want to modify the browser action."
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "disable",
"unsupported": true,
"type": "function",
"description": "Disables the browser action for a tab.",
"async": "callback",
"parameters": [
{
"type": "integer",
"optional": true,
"name": "tabId",
"minimum": 0,
"description": "The id of the tab for which you want to modify the browser action."
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "openPopup",
"unsupported": true,
"type": "function",
"description": "Opens the extension popup window in the active window but does not grant tab permissions.",
"async": "callback",
"parameters": [
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "popupView",
"type": "object",
"optional": true,
"description": "JavaScript 'window' object for the popup window if it was succesfully opened.",
"additionalProperties": { "type": "any" }
}
]
}
]
}
],
"events": [
{
"name": "onClicked",
"type": "function",
"description": "Fired when a browser action icon is clicked. This event will not fire if the browser action has a popup.",
"parameters": [
{
"name": "tab",
"$ref": "tabs.Tab"
}
]
}
]
}
]

View File

@ -3,6 +3,8 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
geckoview.jar:
content/schemas/browser_action.json
content/schemas/browsing_data.json
content/schemas/gecko_view_addons.json
content/schemas/page_action.json
content/schemas/tabs.json

View File

@ -0,0 +1,240 @@
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
[
{
"namespace": "manifest",
"types": [
{
"$extend": "WebExtensionManifest",
"properties": {
"page_action": {
"type": "object",
"additionalProperties": { "$ref": "UnrecognizedProperty" },
"properties": {
"default_title": {
"type": "string",
"optional": true,
"preprocess": "localize"
},
"default_icon": {
"$ref": "IconPath",
"optional": true
},
"default_popup": {
"type": "string",
"format": "relativeUrl",
"optional": true,
"preprocess": "localize"
},
"browser_style": {
"type": "boolean",
"optional": true,
"default": false
}
},
"optional": true
}
}
}
]
},
{
"namespace": "pageAction",
"description": "Use the <code>browser.pageAction</code> API to put icons inside the address bar. Page actions represent actions that can be taken on the current page, but that aren't applicable to all pages.",
"permissions": ["manifest:page_action"],
"types": [
{
"id": "ImageDataType",
"type": "object",
"isInstanceOf": "ImageData",
"additionalProperties": { "type": "any" },
"description": "Pixel data for an image. Must be an ImageData object (for example, from a <code>canvas</code> element)."
}
],
"functions": [
{
"name": "show",
"type": "function",
"description": "Shows the page action. The page action is shown whenever the tab is selected.",
"async": "callback",
"parameters": [
{"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "hide",
"type": "function",
"description": "Hides the page action.",
"async": "callback",
"parameters": [
{"type": "integer", "name": "tabId", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "setTitle",
"unsupported": true,
"type": "function",
"description": "Sets the title of the page action. This is displayed in a tooltip over the page action.",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
"title": {"type": "string", "description": "The tooltip string."}
}
}
]
},
{
"name": "getTitle",
"unsupported": true,
"type": "function",
"description": "Gets the title of the page action.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {
"type": "integer",
"description": "Specify the tab to get the title from."
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
"name": "result",
"type": "string"
}
]
}
]
},
{
"name": "setIcon",
"unsupported": true,
"type": "function",
"description": "Sets the icon for the page action. The icon can be specified either as the path to an image file or as the pixel data from a canvas element, or as dictionary of either one of those. Either the <b>path</b> or the <b>imageData</b> property must be specified.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
"imageData": {
"choices": [
{ "$ref": "ImageDataType" },
{
"type": "object",
"additionalProperties": {"$ref": "ImageDataType"}
}
],
"optional": true,
"description": "Either an ImageData object or a dictionary {size -> ImageData} representing icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then image with size <code>scale</code> * 19 will be selected. Initially only scales 1 and 2 will be supported. At least one image must be specified. Note that 'details.imageData = foo' is equivalent to 'details.imageData = {'19': foo}'"
},
"path": {
"choices": [
{ "type": "string" },
{
"type": "object",
"additionalProperties": {"type": "string"}
}
],
"optional": true,
"description": "Either a relative image path or a dictionary {size -> relative image path} pointing to icon to be set. If the icon is specified as a dictionary, the actual image to be used is chosen depending on screen's pixel density. If the number of image pixels that fit into one screen space unit equals <code>scale</code>, then image with size <code>scale</code> * 19 will be selected. Initially only scales 1 and 2 will be supported. At least one image must be specified. Note that 'details.path = foo' is equivalent to 'details.imageData = {'19': foo}'"
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "setPopup",
"type": "function",
"async": "callback",
"description": "Sets the html document to be opened as a popup when the user clicks on the page action's icon.",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
"popup": {
"type": "string",
"description": "The html file to show in a popup. If set to the empty string (''), no popup is shown."
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "getPopup",
"type": "function",
"description": "Gets the html document set as the popup for this page action.",
"async": "callback",
"parameters": [
{
"name": "details",
"type": "object",
"properties": {
"tabId": {
"type": "integer",
"description": "Specify the tab to get the popup from."
}
}
},
{
"type": "function",
"name": "callback",
"optional": true,
"parameters": []
}
]
}
],
"events": [
{
"name": "onClicked",
"type": "function",
"description": "Fired when a page action icon is clicked. This event will not fire if the page action has a popup.",
"parameters": [
{
"name": "tab",
"$ref": "tabs.Tab"
}
]
}
]
}
]

View File

@ -69,8 +69,6 @@ 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",

View File

@ -26,6 +26,7 @@ import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewStructure;
import android.view.autofill.AutofillValue;
@ -605,7 +606,6 @@ 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);
@ -644,7 +644,6 @@ 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;
@ -1152,12 +1151,10 @@ package org.mozilla.geckoview {
method public void setAutofillEnabled(boolean);
method @UiThread public void setSession(@NonNull GeckoSession);
method public void setVerticalClipping(int);
method public void setViewBackend(int);
method public boolean shouldPinOnScreen();
field public static final int BACKEND_SURFACE_VIEW = 1;
field public static final int BACKEND_TEXTURE_VIEW = 2;
field @NonNull protected final GeckoView.Display mDisplay;
field @Nullable protected GeckoSession mSession;
field @Nullable protected SurfaceView mSurfaceView;
}
@AnyThread public class GeckoWebExecutor {
@ -1380,37 +1377,12 @@ 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;

View File

@ -1,140 +0,0 @@
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}`);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 B

View File

@ -1 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

View File

@ -1,30 +0,0 @@
{
"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"
]
}

View File

@ -1,10 +0,0 @@
<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>

View File

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

View File

@ -1,10 +0,0 @@
<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>

View File

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

View File

@ -1 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,540 +0,0 @@
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)
}
}

View File

@ -1,173 +0,0 @@
package org.mozilla.gecko;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
/** Provides transparent access to either a SurfaceView or TextureView */
public class SurfaceViewWrapper {
private static final String LOGTAG = "SurfaceViewWrapper";
private ListenerWrapper mListenerWrapper;
private View mView;
// Only one of these will be non-null at any point in time
SurfaceView mSurfaceView;
TextureView mTextureView;
public SurfaceViewWrapper(final Context context) {
mListenerWrapper = new ListenerWrapper();
mSurfaceView = new SurfaceView(context);
mSurfaceView.getHolder().addCallback(mListenerWrapper);
mView = mSurfaceView;
}
public void useSurfaceView(final Context context) {
if (mTextureView != null) {
mListenerWrapper.onSurfaceTextureDestroyed(
mTextureView.getSurfaceTexture());
mTextureView = null;
}
mListenerWrapper.reset();
mSurfaceView = new SurfaceView(context);
mSurfaceView.getHolder().addCallback(mListenerWrapper);
mView = mSurfaceView;
}
public void useTextureView(final Context context) {
if (mSurfaceView != null) {
mListenerWrapper.surfaceDestroyed(mSurfaceView.getHolder());
mSurfaceView = null;
}
mListenerWrapper.reset();
mTextureView = new TextureView(context);
mTextureView.setSurfaceTextureListener(mListenerWrapper);
mView = mTextureView;
}
public void setBackgroundColor(final int color) {
if (mSurfaceView != null) {
mSurfaceView.setBackgroundColor(color);
} else {
Log.e(LOGTAG, "TextureView doesn't support background color.");
}
}
public void setListener(final Listener listener) {
mListenerWrapper.mListener = listener;
}
public int getWidth() {
if (mSurfaceView != null) {
return mSurfaceView.getHolder().getSurfaceFrame().left;
}
return mListenerWrapper.mWidth;
}
public int getHeight() {
if (mSurfaceView != null) {
return mSurfaceView.getHolder().getSurfaceFrame().bottom;
}
return mListenerWrapper.mHeight;
}
public Surface getSurface() {
if (mSurfaceView != null) {
return mSurfaceView.getHolder().getSurface();
}
return mListenerWrapper.mSurface;
}
public View getView() {
return mView;
}
/**
* Translates SurfaceTextureListener and SurfaceHolder.Callback into a common interface
* SurfaceViewWrapper.Listener
*/
private static class ListenerWrapper implements TextureView.SurfaceTextureListener,
SurfaceHolder.Callback {
private Listener mListener;
// TextureView doesn't provide getters for these so we keep track of them here
private Surface mSurface;
private int mWidth;
private int mHeight;
public void reset() {
mWidth = 0;
mHeight = 0;
mSurface = null;
}
// TextureView
@Override
public void onSurfaceTextureAvailable(final SurfaceTexture surface, final int width,
final int height) {
mSurface = new Surface(surface);
mWidth = width;
mHeight = height;
if (mListener != null) {
mListener.onSurfaceChanged(mSurface, width, height);
}
}
@Override
public void onSurfaceTextureSizeChanged(final SurfaceTexture surface, final int width,
final int height) {
mWidth = width;
mHeight = height;
if (mListener != null) {
mListener.onSurfaceChanged(mSurface, mWidth, mHeight);
}
}
@Override
public boolean onSurfaceTextureDestroyed(final SurfaceTexture surface) {
if (mListener != null) {
mListener.onSurfaceDestroyed();
}
mSurface = null;
return false;
}
@Override
public void onSurfaceTextureUpdated(final SurfaceTexture surface) {
mSurface = new Surface(surface);
if (mListener != null) {
mListener.onSurfaceChanged(mSurface, mWidth, mHeight);
}
}
// SurfaceView
@Override
public void surfaceCreated(final SurfaceHolder holder) {}
@Override
public void surfaceChanged(final SurfaceHolder holder, final int format, final int width,
final int height) {
if (mListener != null) {
mListener.onSurfaceChanged(holder.getSurface(), mWidth, mHeight);
}
}
@Override
public void surfaceDestroyed(final SurfaceHolder holder) {
if (mListener != null) {
mListener.onSurfaceDestroyed();
}
}
}
public interface Listener {
void onSurfaceChanged(Surface surface, int width, int height);
void onSurfaceDestroyed();
}
}

View File

@ -12,6 +12,7 @@ import java.lang.ref.WeakReference;
import java.nio.ByteBuffer;
import java.util.AbstractSequentialList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
@ -323,7 +324,81 @@ public class GeckoSession implements Parcelable {
}
};
private final WebExtension.Listener mWebExtensionListener;
private static class WebExtensionSender {
public String webExtensionId;
public String nativeApp;
public WebExtensionSender(final String webExtensionId, final String nativeApp) {
this.webExtensionId = webExtensionId;
this.nativeApp = nativeApp;
}
@Override
public boolean equals(final Object other) {
if (!(other instanceof WebExtensionSender)) {
return false;
}
WebExtensionSender o = (WebExtensionSender) other;
return webExtensionId.equals(o.webExtensionId) &&
nativeApp.equals(o.nativeApp);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (webExtensionId != null ? webExtensionId.hashCode() : 0);
result = 31 * result + (nativeApp != null ? nativeApp.hashCode() : 0);
return result;
}
}
private final class WebExtensionListener implements BundleEventListener {
final private HashMap<WebExtensionSender, WebExtension.MessageDelegate> mMessageDelegates;
public WebExtensionListener() {
mMessageDelegates = new HashMap<>();
}
/* package */ void registerListeners() {
getEventDispatcher().registerUiThreadListener(this,
"GeckoView:WebExtension:Message",
"GeckoView:WebExtension:PortMessage",
"GeckoView:WebExtension:Connect",
"GeckoView:WebExtension:CloseTab",
null);
}
public void setDelegate(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) {
return mMessageDelegates.get(new WebExtensionSender(webExtension.id, nativeApp));
}
@Override
public void handleMessage(final String event, final GeckoBundle message,
final EventCallback callback) {
if (mWindow == null) {
return;
}
if ("GeckoView:WebExtension:Message".equals(event)
|| "GeckoView:WebExtension:PortMessage".equals(event)
|| "GeckoView:WebExtension:Connect".equals(event)) {
mWindow.runtime.getWebExtensionDispatcher()
.handleMessage(event, message, callback, GeckoSession.this);
} else if ("GeckoView:WebExtension:CloseTab".equals(event)) {
mWindow.runtime.getWebExtensionController().closeTab(message, callback, GeckoSession.this);
}
}
}
private final WebExtensionListener mWebExtensionListener;
/**
* Get the message delegate for <code>nativeApp</code>.
@ -338,7 +413,7 @@ public class GeckoSession implements Parcelable {
public @Nullable WebExtension.MessageDelegate getMessageDelegate(
final @NonNull WebExtension webExtension,
final @NonNull String nativeApp) {
return mWebExtensionListener.getMessageDelegate(webExtension, nativeApp);
return mWebExtensionListener.getDelegate(webExtension, nativeApp);
}
/**
@ -367,41 +442,7 @@ public class GeckoSession implements Parcelable {
public void setMessageDelegate(final @NonNull WebExtension webExtension,
final @Nullable WebExtension.MessageDelegate delegate,
final @NonNull String 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);
mWebExtensionListener.setDelegate(webExtension, delegate, nativeApp);
}
private final GeckoSessionHandler<ContentDelegate> mContentHandler =
@ -1256,7 +1297,7 @@ public class GeckoSession implements Parcelable {
mSettings = new GeckoSessionSettings(settings, this);
mListener.registerListeners();
mWebExtensionListener = new WebExtension.Listener(this);
mWebExtensionListener = new WebExtensionListener();
mWebExtensionListener.registerListeners();
if (BuildConfig.DEBUG && handlersCount != mSessionHandlers.length) {
@ -1302,7 +1343,6 @@ public class GeckoSession implements Parcelable {
mEventDispatcher, mAccessibility != null ? mAccessibility.nativeProvider : null,
createInitData());
onWindowChanged(WINDOW_TRANSFER_IN, /* inProgress */ false);
mWebExtensionListener.runtime = mWindow.runtime;
}
}
@ -1423,7 +1463,6 @@ public class GeckoSession implements Parcelable {
final boolean isRemote = mSettings.getUseMultiprocess();
mWindow = new Window(runtime, this, mNativeQueue);
mWebExtensionListener.runtime = runtime;
onWindowChanged(WINDOW_OPEN, /* inProgress */ true);

View File

@ -336,13 +336,6 @@ public final class GeckoSessionSettings implements Parcelable {
private static final Key<Boolean> FULL_ACCESSIBILITY_TREE =
new Key<Boolean>("fullAccessibilityTree", /* initOnly */ false, /* values */ null);
/**
* Key to specify if this GeckoSession is a Popup or not. Popup sessions can paint over other
* sessions and are not exposed to the tabs WebExtension API.
*/
private static final Key<Boolean> IS_POPUP =
new Key<Boolean>("isPopup", /* initOnly */ false, /* values */ null);
/**
* Internal Gecko key to specify the session context ID.
* Derived from `UNSAFE_CONTEXT_ID`.
@ -385,7 +378,6 @@ public final class GeckoSessionSettings implements Parcelable {
mBundle.putBoolean(SUSPEND_MEDIA_WHEN_INACTIVE.name, false);
mBundle.putBoolean(ALLOW_JAVASCRIPT.name, true);
mBundle.putBoolean(FULL_ACCESSIBILITY_TREE.name, false);
mBundle.putBoolean(IS_POPUP.name, false);
mBundle.putInt(USER_AGENT_MODE.name, USER_AGENT_MODE_MOBILE);
mBundle.putString(USER_AGENT_OVERRIDE.name, null);
mBundle.putInt(VIEWPORT_MODE.name, VIEWPORT_MODE_MOBILE);
@ -457,10 +449,6 @@ public final class GeckoSessionSettings implements Parcelable {
setBoolean(FULL_ACCESSIBILITY_TREE, value);
}
/* package */ void setIsPopup(final boolean value) {
setBoolean(IS_POPUP, value);
}
private void setBoolean(final Key<Boolean> key, final boolean value) {
synchronized (mBundle) {
if (valueChangedLocked(key, value)) {
@ -534,10 +522,6 @@ public final class GeckoSessionSettings implements Parcelable {
return getBoolean(FULL_ACCESSIBILITY_TREE);
}
/* package */ boolean getIsPopup() {
return getBoolean(IS_POPUP);
}
private boolean getBoolean(final Key<Boolean> key) {
synchronized (mBundle) {
return mBundle.getBoolean(key.name);

View File

@ -9,7 +9,6 @@ package org.mozilla.geckoview;
import org.mozilla.gecko.AndroidGamepadManager;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.InputMethods;
import org.mozilla.gecko.SurfaceViewWrapper;
import org.mozilla.gecko.util.ActivityUtils;
import org.mozilla.gecko.util.ThreadUtils;
@ -30,7 +29,6 @@ import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.AnyThread;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
@ -41,9 +39,8 @@ import android.util.SparseArray;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStructure;
@ -54,9 +51,6 @@ import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@UiThread
public class GeckoView extends FrameLayout {
private static final String LOGTAG = "GeckoView";
@ -66,7 +60,7 @@ public class GeckoView extends FrameLayout {
protected @Nullable GeckoSession mSession;
private boolean mStateSaved;
private @Nullable SurfaceViewWrapper mSurfaceWrapper;
protected @Nullable SurfaceView mSurfaceView;
private boolean mIsResettingFocus;
@ -107,7 +101,7 @@ public class GeckoView extends FrameLayout {
};
}
private class Display implements SurfaceViewWrapper.Listener {
private class Display implements SurfaceHolder.Callback {
private final int[] mOrigin = new int[2];
private GeckoDisplay mDisplay;
@ -126,10 +120,10 @@ public class GeckoView extends FrameLayout {
// Tell display there is already a surface.
onGlobalLayout();
if (GeckoView.this.mSurfaceWrapper != null) {
final SurfaceViewWrapper wrapper = GeckoView.this.mSurfaceWrapper;
mDisplay.surfaceChanged(wrapper.getSurface(),
wrapper.getWidth(), wrapper.getHeight());
if (GeckoView.this.mSurfaceView != null) {
final SurfaceHolder holder = GeckoView.this.mSurfaceView.getHolder();
final Rect frame = holder.getSurfaceFrame();
mDisplay.surfaceChanged(holder.getSurface(), frame.right, frame.bottom);
GeckoView.this.setActive(true);
}
}
@ -147,11 +141,15 @@ public class GeckoView extends FrameLayout {
return display;
}
@Override // SurfaceListener
public void onSurfaceChanged(final Surface surface,
@Override // SurfaceHolder.Callback
public void surfaceCreated(final SurfaceHolder holder) {
}
@Override // SurfaceHolder.Callback
public void surfaceChanged(final SurfaceHolder holder, final int format,
final int width, final int height) {
if (mDisplay != null) {
mDisplay.surfaceChanged(surface, width, height);
mDisplay.surfaceChanged(holder.getSurface(), width, height);
if (!mValid) {
GeckoView.this.setActive(true);
}
@ -159,8 +157,8 @@ public class GeckoView extends FrameLayout {
mValid = true;
}
@Override // SurfaceListener
public void onSurfaceDestroyed() {
@Override // SurfaceHolder.Callback
public void surfaceDestroyed(final SurfaceHolder holder) {
if (mDisplay != null) {
mDisplay.surfaceDestroyed();
GeckoView.this.setActive(false);
@ -172,8 +170,8 @@ public class GeckoView extends FrameLayout {
if (mDisplay == null) {
return;
}
if (GeckoView.this.mSurfaceWrapper != null) {
GeckoView.this.mSurfaceWrapper.getView().getLocationOnScreen(mOrigin);
if (GeckoView.this.mSurfaceView != null) {
GeckoView.this.mSurfaceView.getLocationOnScreen(mOrigin);
mDisplay.screenOriginChanged(mOrigin[0], mOrigin[1]);
}
}
@ -231,15 +229,14 @@ public class GeckoView extends FrameLayout {
// transparent).
setWillNotCacheDrawing(false);
mSurfaceWrapper = new SurfaceViewWrapper(getContext());
mSurfaceWrapper.useSurfaceView(getContext());
mSurfaceWrapper.setListener(mDisplay);
mSurfaceWrapper.setBackgroundColor(Color.WHITE);
addView(mSurfaceWrapper.getView(),
mSurfaceView = new SurfaceView(getContext());
mSurfaceView.setBackgroundColor(Color.WHITE);
addView(mSurfaceView,
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
mSurfaceView.getHolder().addCallback(mDisplay);
final Activity activity = ActivityUtils.getActivityFromContext(getContext());
if (activity != null) {
mSelectionActionDelegate = new BasicSelectionActionDelegate(activity);
@ -258,42 +255,8 @@ public class GeckoView extends FrameLayout {
public void coverUntilFirstPaint(final int color) {
ThreadUtils.assertOnUiThread();
if (mSurfaceWrapper != null) {
mSurfaceWrapper.setBackgroundColor(color);
}
}
/**
* This GeckoView instance will be backed by a {@link SurfaceView}.
*
* This option offers the best performance at the price of not being
* able to animate GeckoView.
*/
public static final int BACKEND_SURFACE_VIEW = 1;
/**
* This GeckoView instance will be backed by a {@link TextureView}.
*
* This option offers worse performance compared to {@link #BACKEND_SURFACE_VIEW}
* but allows you to animate GeckoView or to paint a GeckoView on top of another GeckoView.
*/
public static final int BACKEND_TEXTURE_VIEW = 2;
@Retention(RetentionPolicy.SOURCE)
@IntDef({BACKEND_SURFACE_VIEW, BACKEND_TEXTURE_VIEW})
/* protected */ @interface ViewBackend {}
/**
* Set which view should be used by this GeckoView instance to display content.
*
* By default, GeckoView will use a {@link SurfaceView}.
*
* @param backend Any of {@link #BACKEND_SURFACE_VIEW BACKEND_*}.
*/
public void setViewBackend(final @ViewBackend int backend) {
if (backend == BACKEND_SURFACE_VIEW) {
mSurfaceWrapper.useSurfaceView(getContext());
} else if (backend == BACKEND_TEXTURE_VIEW) {
mSurfaceWrapper.useTextureView(getContext());
if (mSurfaceView != null) {
mSurfaceView.setBackgroundColor(color);
}
}
@ -525,7 +488,7 @@ public class GeckoView extends FrameLayout {
// For detecting changes in SurfaceView layout, we take a shortcut here and
// override gatherTransparentRegion, instead of registering a layout listener,
// which is more expensive.
if (mSurfaceWrapper != null) {
if (mSurfaceView != null) {
mDisplay.onGlobalLayout();
}
return super.gatherTransparentRegion(region);

View File

@ -1,82 +0,0 @@
package org.mozilla.geckoview;
import android.graphics.Bitmap;
import android.support.annotation.AnyThread;
import android.support.annotation.NonNull;
import org.mozilla.gecko.GeckoThread;
import org.mozilla.gecko.annotation.WrapForJNI;
/**
* Provides access to Gecko's Image processing library.
*/
@AnyThread
/* protected */ class ImageDecoder {
private static ImageDecoder instance;
private ImageDecoder() {}
public static ImageDecoder instance() {
if (instance == null) {
instance = new ImageDecoder();
}
return instance;
}
@WrapForJNI(dispatchTo = "gecko", stubName = "Decode")
private static native void nativeDecode(final String uri, final int desiredLength,
GeckoResult<Bitmap> result);
/**
* Fetches and decodes an image at the specified location.
* This method supports SVG, PNG, Bitmap and other formats supported by Gecko.
*
* @param uri location of the image. Can be either a remote https:// location, file:/// if the
* file is local or a resource://android/ if the file is located inside the APK.
*
* e.g. if the image file is locate at /assets/test.png inside the apk, set the uri
* to resource://android/assets/test.png.
* @return A {@link GeckoResult} to the decoded image.
*/
@NonNull
public GeckoResult<Bitmap> decode(final @NonNull String uri) {
return decode(uri, 0);
}
/**
* Fetches and decodes an image at the specified location and resizes it to the desired length.
* This method supports SVG, PNG, Bitmap and other formats supported by Gecko.
*
* Note: The final size might differ slightly from the requested output.
*
* @param uri location of the image. Can be either a remote https:// location, file:/// if the
* file is local or a resource://android/ if the file is located inside the APK.
*
* e.g. if the image file is locate at /assets/test.png inside the apk, set the uri
* to resource://android/assets/test.png.
* @param desiredLength Longest size for the image in device pixel units. The resulting image
* might be slightly different if the image cannot be resized efficiently.
* If desiredLength is 0 then the image will be decoded to its natural
* size.
* @return A {@link GeckoResult} to the decoded image.
*/
@NonNull
public GeckoResult<Bitmap> decode(final @NonNull String uri, final int desiredLength) {
if (uri == null) {
throw new IllegalArgumentException("Uri cannot be null");
}
final GeckoResult<Bitmap> result = new GeckoResult<>();
if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
nativeDecode(uri, desiredLength, result);
} else {
GeckoThread.queueNativeCallUntil(GeckoThread.State.PROFILE_READY, this,
"nativeDecode", String.class, uri, int.class, desiredLength,
GeckoResult.class, result);
}
return result;
}
}

View File

@ -1,8 +1,5 @@
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;
@ -13,17 +10,12 @@ import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.util.BundleEventListener;
import org.mozilla.gecko.util.EventCallback;
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;
@ -58,8 +50,6 @@ public class WebExtension {
*/
/* package */ final @NonNull Map<String, MessageDelegate> messageDelegates;
/* package */ @NonNull ActionDelegate actionDelegate;
@Override
public String toString() {
return "WebExtension {" +
@ -402,104 +392,6 @@ public class WebExtension {
}
};
private static class Sender {
public String webExtensionId;
public String nativeApp;
public Sender(final String webExtensionId, final String nativeApp) {
this.webExtensionId = webExtensionId;
this.nativeApp = nativeApp;
}
@Override
public boolean equals(final Object other) {
if (!(other instanceof Sender)) {
return false;
}
Sender o = (Sender) other;
return webExtensionId.equals(o.webExtensionId) &&
nativeApp.equals(o.nativeApp);
}
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (webExtensionId != null ? webExtensionId.hashCode() : 0);
result = 31 * result + (nativeApp != null ? nativeApp.hashCode() : 0);
return result;
}
}
/* package */ final static class Listener implements BundleEventListener {
final private HashMap<Sender, WebExtension.MessageDelegate> mMessageDelegates;
final private HashMap<String, WebExtension.ActionDelegate> mActionDelegates;
final private GeckoSession mSession;
public GeckoRuntime runtime;
public Listener(final GeckoSession session) {
mMessageDelegates = new HashMap<>();
mActionDelegates = new HashMap<>();
mSession = session;
}
/* package */ void registerListeners() {
mSession.getEventDispatcher().registerUiThreadListener(this,
"GeckoView:WebExtension:Message",
"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");
}
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 Sender(webExtension.id, nativeApp), delegate);
}
public WebExtension.MessageDelegate getMessageDelegate(final WebExtension webExtension,
final String nativeApp) {
return mMessageDelegates.get(new Sender(webExtension.id, nativeApp));
}
@Override
public void handleMessage(final String event, final GeckoBundle message,
final EventCallback callback) {
if (runtime == null) {
return;
}
if ("GeckoView:WebExtension:Message".equals(event)
|| "GeckoView:WebExtension:PortMessage".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)) {
runtime.getWebExtensionDispatcher()
.handleMessage(event, message, callback, mSession);
return;
} else if ("GeckoView:WebExtension:CloseTab".equals(event)) {
runtime.getWebExtensionController().closeTab(message, callback, mSession);
return;
}
}
}
/**
* Describes the sender of a message from a WebExtension.
@ -582,454 +474,20 @@ public class WebExtension {
}
}
/**
* 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;
}
private static final MessageDelegate NULL_MESSAGE_DELEGATE = new MessageDelegate() {
@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) {
public GeckoResult<Object> onMessage(final @NonNull String nativeApp,
final @NonNull Object message,
final @NonNull MessageSender sender) {
Log.d(LOGTAG, "Unhandled message from " + nativeApp + " id=" +
sender.webExtension.id + ": " + message.toString());
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;
@Override
public void onConnect(final @NonNull Port port) {
Log.d(LOGTAG, "Unhandled connect request from " +
port.sender.webExtension.id);
}
}
/**
* 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;
}
};
}

View File

@ -29,13 +29,7 @@ import java.util.Map;
"GeckoView:WebExtension:Message",
"GeckoView:WebExtension:PortMessage",
"GeckoView:WebExtension:Connect",
"GeckoView:WebExtension:Disconnect",
// {Browser,Page}Actions
"GeckoView:BrowserAction:Update",
"GeckoView:BrowserAction:OpenPopup",
"GeckoView:PageAction:Update",
"GeckoView:PageAction:OpenPopup"
"GeckoView:WebExtension:Disconnect"
);
mHandlerRegistered = true;
}
@ -237,69 +231,6 @@ 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)) {
@ -308,18 +239,6 @@ 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");

View File

@ -24,22 +24,12 @@ exclude: true
to [`WebResponse#setReadTimeoutMillis()`][72.4]. The default timeout value is reflected in
[`WebResponse#DEFAULT_READ_TIMEOUT_MS`][72.5], currently 30s.
([bug 1595145]({{bugzilla}}1595145))
- 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
[72.3]: {{javadoc_uri}}/WebResponse.html#body
[72.4]: {{javadoc_uri}}/WebResponse.html#setReadTimeoutMillis-long-
[72.5]: {{javadoc_uri}}/WebResponse.html#DEFAULT_READ_TIMEOUT_MS
[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].
@ -435,4 +425,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]: 2cd1b6f37379153bb6abfb62fba0ff5fe3765e38
[api-version]: f4f62b0476eb283fbaf4be55e91b78dede9f0099

View File

@ -1,18 +0,0 @@
package org.mozilla.geckoview_example;
import android.graphics.Bitmap;
public class ActionButton {
final Bitmap icon;
final String text;
final Integer textColor;
final Integer backgroundColor;
public ActionButton(final Bitmap icon, final String text, final Integer textColor,
final Integer backgroundColor) {
this.icon = icon;
this.text = text;
this.textColor = textColor;
this.backgroundColor = backgroundColor;
}
}

View File

@ -44,8 +44,11 @@ import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.SystemClock;
import android.security.keystore.KeyProperties;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
@ -53,194 +56,29 @@ import android.support.v4.content.ContextCompat;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.util.LruCache;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.ref.WeakReference;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Locale;
interface BrowserActionDelegate {
default GeckoSession toggleBrowserActionPopup(boolean force) {
return null;
}
default void onActionButton(ActionButton button) {}
default TabSession getSession(GeckoSession session) {
return null;
}
default TabSession getCurrentSession() {
return null;
}
}
class WebExtensionManager implements WebExtension.ActionDelegate, TabSessionManager.TabObserver {
public WebExtension extension;
private LruCache<WebExtension.ActionIcon, Bitmap> mBitmapCache = new LruCache<>(5);
private GeckoRuntime mRuntime;
private WebExtension.Action mDefaultAction;
private WeakReference<BrowserActionDelegate> mActionDelegate;
// We only support either one browserAction or one pageAction
private void onAction(final WebExtension extension, final GeckoSession session,
final WebExtension.Action action) {
BrowserActionDelegate delegate = mActionDelegate.get();
if (delegate == null) {
return;
}
WebExtension.Action resolved;
if (session == null) {
// This is the default action
mDefaultAction = action;
resolved = actionFor(delegate.getCurrentSession());
} else {
if (delegate.getSession(session) == null) {
return;
}
delegate.getSession(session).action = action;
if (delegate.getCurrentSession() != session) {
// This update is not for the session that we are currently displaying,
// no need to update the UI
return;
}
resolved = action.withDefault(mDefaultAction);
}
updateAction(resolved);
}
@Override
public void onPageAction(final WebExtension extension,
final GeckoSession session,
final WebExtension.Action action) {
onAction(extension, session, action);
}
@Override
public void onBrowserAction(final WebExtension extension,
final GeckoSession session,
final WebExtension.Action action) {
onAction(extension, session, action);
}
private GeckoResult<GeckoSession> togglePopup(boolean force) {
BrowserActionDelegate actionDelegate = mActionDelegate.get();
if (actionDelegate == null) {
return null;
}
GeckoSession session = actionDelegate.toggleBrowserActionPopup(false);
if (session == null) {
return null;
}
return GeckoResult.fromValue(session);
}
@Override
public GeckoResult<GeckoSession> onTogglePopup(final @NonNull WebExtension extension,
final @NonNull WebExtension.Action action) {
return togglePopup(false);
}
@Override
public GeckoResult<GeckoSession> onOpenPopup(final @NonNull WebExtension extension,
final @NonNull WebExtension.Action action) {
return togglePopup(true);
}
private WebExtension.Action actionFor(TabSession session) {
if (session.action == null) {
return mDefaultAction;
} else {
return session.action.withDefault(mDefaultAction);
}
}
private void updateAction(WebExtension.Action resolved) {
BrowserActionDelegate actionDelegate = mActionDelegate.get();
if (actionDelegate == null) {
return;
}
if (resolved.enabled == null || !resolved.enabled) {
actionDelegate.onActionButton(null);
return;
}
if (resolved.icon != null) {
if (mBitmapCache.get(resolved.icon) != null) {
actionDelegate.onActionButton(new ActionButton(
mBitmapCache.get(resolved.icon), resolved.badgeText,
resolved.badgeTextColor,
resolved.badgeBackgroundColor
));
} else {
resolved.icon.get(100).accept(bitmap -> {
mBitmapCache.put(resolved.icon, bitmap);
actionDelegate.onActionButton(new ActionButton(
bitmap, resolved.badgeText,
resolved.badgeTextColor,
resolved.badgeBackgroundColor));
});
}
} else {
actionDelegate.onActionButton(null);
}
}
public void onClicked(TabSession session) {
actionFor(session).click();
}
public void setActionDelegate(BrowserActionDelegate delegate) {
mActionDelegate = new WeakReference<>(delegate);
}
@Override
public void onCurrentSession(TabSession session) {
if (mDefaultAction == null) {
// No action was ever defined, so nothing to do
return;
}
if (session.action != null) {
updateAction(session.action.withDefault(mDefaultAction));
} else {
updateAction(mDefaultAction);
}
}
public WebExtensionManager(GeckoRuntime runtime) {
mRuntime = runtime;
// TODO: allow users to install an extension from file
// extension = new WebExtension("resource://android/assets/chill-out/");
// extension.setActionDelegate(this);
// mRuntime.registerWebExtension(extension);
}
}
public class GeckoViewActivity
extends AppCompatActivity
implements ToolbarLayout.TabListener, BrowserActionDelegate {
public class GeckoViewActivity extends AppCompatActivity {
private static final String LOGTAG = "GeckoViewActivity";
private static final String USE_MULTIPROCESS_EXTRA = "use_multiprocess";
private static final String FULL_ACCESSIBILITY_TREE_EXTRA = "full_accessibility_tree";
@ -252,9 +90,6 @@ public class GeckoViewActivity
private static final int REQUEST_WRITE_EXTERNAL_STORAGE = 3;
private static GeckoRuntime sGeckoRuntime;
private static WebExtensionManager sExtensionManager;
private TabSessionManager mTabSessionManager;
private GeckoView mGeckoView;
private boolean mUseMultiprocess;
@ -264,8 +99,6 @@ public class GeckoViewActivity
private boolean mEnableRemoteDebugging;
private boolean mKillProcessOnDestroy;
private boolean mDesktopMode;
private TabSession mPopupSession;
private View mPopupView;
private boolean mShowNotificationsRejected;
private ArrayList<String> mAcceptedPersistentStorage = new ArrayList<String>();
@ -311,7 +144,7 @@ public class GeckoViewActivity
mToolbarView = new ToolbarLayout(this, mTabSessionManager);
mToolbarView.setId(R.id.toolbar_layout);
mToolbarView.setTabListener(this);
mToolbarView.setTabListener(this::switchToSessionAtIndex);
getSupportActionBar().setCustomView(mToolbarView,
new ActionBar.LayoutParams(ActionBar.LayoutParams.MATCH_PARENT,
@ -369,9 +202,6 @@ public class GeckoViewActivity
}
});
sExtensionManager = new WebExtensionManager(sGeckoRuntime);
mTabSessionManager.setTabObserver(sExtensionManager);
// `getSystemService` call requires API level 23
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
sGeckoRuntime.setWebNotificationDelegate(new WebNotificationDelegate() {
@ -430,8 +260,6 @@ public class GeckoViewActivity
});
}
sExtensionManager.setActionDelegate(this);
if(savedInstanceState == null) {
TabSession session = getIntent().getParcelableExtra("session");
if (session != null) {
@ -459,69 +287,6 @@ public class GeckoViewActivity
mToolbarView.updateTabCount();
}
@Override
public TabSession getSession(GeckoSession session) {
return mTabSessionManager.getSession(session);
}
@Override
public TabSession getCurrentSession() {
return mTabSessionManager.getCurrentSession();
}
@Override
public void onActionButton(ActionButton button) {
mToolbarView.setBrowserActionButton(button);
}
@Override
public GeckoSession toggleBrowserActionPopup(boolean force) {
if (mPopupSession == null) {
openPopupSession();
}
ViewGroup.LayoutParams params = mPopupView.getLayoutParams();
boolean shouldShow = force || params.width == 0;
if (shouldShow) {
params.height = 1100;
params.width = 1200;
} else {
params.height = 0;
params.width = 0;
}
mPopupView.setLayoutParams(params);
return shouldShow ? mPopupSession : null;
}
private void openPopupSession() {
LayoutInflater inflater = (LayoutInflater)
getSystemService(LAYOUT_INFLATER_SERVICE);
mPopupView = inflater.inflate(R.layout.browser_action_popup, null);
GeckoView geckoView = mPopupView.findViewById(R.id.gecko_view_popup);
geckoView.setViewBackend(GeckoView.BACKEND_TEXTURE_VIEW);
mPopupSession = new TabSession();
mPopupSession.open(sGeckoRuntime);
geckoView.setSession(mPopupSession);
mPopupView.setOnFocusChangeListener(this::hideBrowserAction);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(0, 0);
params.addRule(RelativeLayout.ABOVE, R.id.toolbar);
mPopupView.setLayoutParams(params);
mPopupView.setFocusable(true);
((ViewGroup) findViewById(R.id.main)).addView(mPopupView);
}
private void hideBrowserAction(View view, boolean hasFocus) {
if (!hasFocus) {
ViewGroup.LayoutParams params = mPopupView.getLayoutParams();
params.height = 0;
params.width = 0;
mPopupView.setLayoutParams(params);
}
}
private void createNotificationChannel() {
// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
@ -575,9 +340,6 @@ public class GeckoViewActivity
session.setMediaDelegate(new ExampleMediaDelegate(this));
session.setSelectionActionDelegate(new BasicSelectionActionDelegate(this));
if (sExtensionManager.extension != null) {
session.setWebExtensionActionDelegate(sExtensionManager.extension, sExtensionManager);
}
updateTrackingProtection(session);
updateDesktopMode(session);
@ -736,11 +498,7 @@ public class GeckoViewActivity
}
}
public void onBrowserActionClick() {
sExtensionManager.onClicked(mTabSessionManager.getCurrentSession());
}
public void switchToTab(int index) {
private void switchToSessionAtIndex(int index) {
TabSession nextSession = mTabSessionManager.getSession(index);
TabSession currentSession = mTabSessionManager.getCurrentSession();
if(nextSession != currentSession) {

View File

@ -7,12 +7,10 @@ import android.support.annotation.UiThread;
import org.mozilla.geckoview.GeckoSession;
import org.mozilla.geckoview.GeckoSessionSettings;
import org.mozilla.geckoview.WebExtension;
public class TabSession extends GeckoSession {
private String mTitle;
private String mUri;
public WebExtension.Action action;
public TabSession() { super(); }

View File

@ -8,21 +8,12 @@ import org.mozilla.geckoview.GeckoSessionSettings;
import java.util.ArrayList;
public class TabSessionManager {
private static ArrayList<TabSession> mTabSessions = new ArrayList<>();
private static ArrayList<TabSession> mTabSessions = new ArrayList<TabSession>();
private int mCurrentSessionIndex = 0;
private TabObserver mTabObserver;
public interface TabObserver {
void onCurrentSession(TabSession session);
}
public TabSessionManager() {
}
public void setTabObserver(TabObserver observer) {
mTabObserver = observer;
}
public void addSession(TabSession session) {
mTabSessions.add(session);
}
@ -50,10 +41,6 @@ public class TabSessionManager {
index = mTabSessions.size() - 1;
}
mCurrentSessionIndex = index;
if (mTabObserver != null) {
mTabObserver.onCurrentSession(session);
}
}
private boolean isCurrentSession(TabSession session) {

View File

@ -1,30 +1,21 @@
package org.mozilla.geckoview_example;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.PorterDuff;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.support.v4.content.ContextCompat;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.TextView;
public class ToolbarLayout extends LinearLayout {
public interface TabListener {
void switchToTab(int tabId);
void onBrowserActionClick();
}
private LocationView mLocationView;
private Button mTabsCountButton;
private View mBrowserAction;
private TabListener mTabListener;
private TabSessionManager mSessionManager;
@ -44,9 +35,6 @@ public class ToolbarLayout extends LinearLayout {
mTabsCountButton = getTabsCountButton();
addView(mTabsCountButton);
mBrowserAction = getBrowserAction();
addView(mBrowserAction);
}
private Button getTabsCountButton() {
@ -59,47 +47,6 @@ public class ToolbarLayout extends LinearLayout {
return button;
}
private View getBrowserAction() {
View browserAction = ((LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE))
.inflate(R.layout.browser_action, this, false);
browserAction.setVisibility(GONE);
return browserAction;
}
public void setBrowserActionButton(ActionButton button) {
if (button == null) {
mBrowserAction.setVisibility(GONE);
return;
}
BitmapDrawable drawable = new BitmapDrawable(getContext().getResources(), button.icon);
ImageView view = mBrowserAction.findViewById(R.id.browser_action_icon);
view.setOnClickListener(this::onBrowserActionButtonClicked);
view.setBackground(drawable);
TextView badge = mBrowserAction.findViewById(R.id.browser_action_badge);
if (button.text != null && !button.text.equals("")) {
if (button.backgroundColor != null) {
GradientDrawable backgroundDrawable = ((GradientDrawable) badge.getBackground().mutate());
backgroundDrawable.setColor(button.backgroundColor);
backgroundDrawable.invalidateSelf();
}
if (button.textColor != null) {
badge.setTextColor(button.textColor);
}
badge.setText(button.text);
badge.setVisibility(VISIBLE);
} else {
badge.setVisibility(GONE);
}
mBrowserAction.setVisibility(VISIBLE);
}
public void onBrowserActionButtonClicked(View view) {
mTabListener.onBrowserActionClick();
}
public LocationView getLocationView() {
return mLocationView;
}

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid
android:id="@+id/browser_action_badge_background"
android:color="#176d7a"
/>
<corners android:radius="5dp" />
</shape>

View File

@ -1,32 +0,0 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="?android:actionBarSize"
android:layout_height="?android:actionBarSize"
android:gravity="center"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<ImageView
android:id="@+id/browser_action_icon"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_centerInParent="true"
/>
</RelativeLayout>
<TextView
android:id="@+id/browser_action_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/rounded_bg"
android:textColor="@color/colorPrimaryDark"
android:layout_alignParentRight="true"
android:paddingLeft="3dp"
android:paddingRight="3dp"
android:layout_marginTop="3dp"
android:layout_marginRight="3dp"
android:text="12"
/>
</RelativeLayout>

View File

@ -1,13 +0,0 @@
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<org.mozilla.geckoview.GeckoView
android:id="@+id/gecko_view_popup"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none"
/>
</RelativeLayout>

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorBackgroundDark">#3F51B5</color>
<color name="colorPrimaryDark">#FFFFFF</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
</resources>

View File

@ -2,6 +2,5 @@
<resources>
<item name="toolbar_layout" type="id"/>
<item name="url_bar" type="id"/>
<item name="browser_action" type="id"/>
<item name="tabs_button" type="id"/>
</resources>

View File

@ -0,0 +1,146 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { EventDispatcher } = ChromeUtils.import(
"resource://gre/modules/Messaging.jsm"
);
var EXPORTED_SYMBOLS = ["BrowserActions"];
var BrowserActions = {
_browserActions: {},
_browserActionTitles: {},
_initialized: false,
/**
* Registers the listeners only if they have not been initialized
* already and there is at least one browser action.
*/
_maybeRegisterListeners() {
if (!this._initialized && Object.keys(this._browserActions).length) {
this._initialized = true;
EventDispatcher.instance.registerListener(this, "Menu:Clicked");
}
},
/**
* Unregisters the listeners if they are already initizliaed and
* all of the browser actions have been removed.
*/
_maybeUnregisterListeners() {
if (this._initialized && !Object.keys(this._browserActions).length) {
this._initialized = false;
EventDispatcher.instance.unregisterListener(this, "Menu:Clicked");
}
},
/**
* Called when a browser action is clicked on.
* @param {string} event The name of the event, which should always
* be "Menu:Clicked".
* @param {Object} data An object containing information about the
* browser action, which in this case should contain an `item`
* property which is browser action's UUID.
*/
onEvent(event, data) {
if (event !== "Menu:Clicked") {
throw new Error(
`Expected "Menu:Clicked" event - received "${event}" instead`
);
}
let browserAction = this._browserActions[data.item];
if (!browserAction) {
// This was probably meant for the NativeWindow menu handler.
return;
}
browserAction.onClicked();
},
/**
* Registers a new browser action.
* @param {Object} browserAction The browser action to add.
*/
register(browserAction) {
EventDispatcher.instance.sendRequest({
type: "Menu:Add",
uuid: browserAction.uuid,
name: browserAction.defaults.name,
});
this._browserActions[browserAction.uuid] = browserAction;
this._browserActionTitles[browserAction.uuid] = browserAction.defaults.name;
this._maybeRegisterListeners();
},
/**
* Updates the browser action with the specified UUID.
* @param {string} uuid The UUID of the browser action.
* @param {Object} options The properties to update.
*/
update(uuid, options) {
if (options.name) {
EventDispatcher.instance.sendRequest({
type: "Menu:Update",
uuid,
options,
});
this._browserActionTitles[uuid] = options.name;
}
},
/**
* Retrieves the name currently used for the browser action with the
* specified UUID. Used for testing only.
* @param {string} uuid The UUID of the browser action.
* @returns {string} the name currently used for the browser action.
*/
getNameForActiveTab(uuid) {
return this._browserActionTitles[uuid];
},
/**
* Checks to see if the browser action is shown. Used for testing only.
* @param {string} uuid The UUID of the browser action.
* @returns {boolean} true if the browser action is shown; false otherwise.
*/
isShown(uuid) {
return !!this._browserActions[uuid];
},
/**
* Synthesizes a click on the browser action. Used for testing only.
* @param {string} uuid The UUID of the browser action.
*/
synthesizeClick(uuid) {
let browserAction = this._browserActions[uuid];
if (!browserAction) {
throw new Error(`No BrowserAction with UUID ${uuid} was found`);
}
browserAction.onClicked();
},
/**
* Unregisters the browser action with the specified UUID.
* @param {string} uuid The UUID of the browser action.
*/
unregister(uuid) {
let browserAction = this._browserActions[uuid];
if (!browserAction) {
throw new Error(`No BrowserAction with UUID ${uuid} was found`);
}
EventDispatcher.instance.sendRequest({
type: "Menu:Remove",
uuid,
});
delete this._browserActions[uuid];
delete this._browserActionTitles[uuid];
this._maybeUnregisterListeners();
},
};

View File

@ -0,0 +1,129 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { EventDispatcher } = ChromeUtils.import(
"resource://gre/modules/Messaging.jsm"
);
XPCOMUtils.defineLazyServiceGetter(
this,
"uuidgen",
"@mozilla.org/uuid-generator;1",
"nsIUUIDGenerator"
);
var EXPORTED_SYMBOLS = ["PageActions"];
// Copied from browser.js
// TODO: We should move this method to a common importable location
function resolveGeckoURI(aURI) {
if (!aURI) {
throw new Error("Can't resolve an empty uri");
}
if (aURI.startsWith("chrome://")) {
let registry = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(
Ci.nsIChromeRegistry
);
return registry.convertChromeURL(Services.io.newURI(aURI)).spec;
} else if (aURI.startsWith("resource://")) {
let handler = Services.io
.getProtocolHandler("resource")
.QueryInterface(Ci.nsIResProtocolHandler);
return handler.resolveURI(Services.io.newURI(aURI));
}
return aURI;
}
var PageActions = {
_items: {},
_initialized: false,
_maybeInitialize: function() {
if (!this._initialized && Object.keys(this._items).length) {
this._initialized = true;
EventDispatcher.instance.registerListener(this, [
"PageActions:Clicked",
"PageActions:LongClicked",
]);
}
},
_maybeUninitialize: function() {
if (this._initialized && !Object.keys(this._items).length) {
this._initialized = false;
EventDispatcher.instance.unregisterListener(this, [
"PageActions:Clicked",
"PageActions:LongClicked",
]);
}
},
onEvent: function(event, data, callback) {
let item = this._items[data.id];
if (event == "PageActions:Clicked") {
if (item.clickCallback) {
item.clickCallback();
}
} else if (event == "PageActions:LongClicked") {
if (item.longClickCallback) {
item.longClickCallback();
}
}
},
isShown: function(id) {
return !!this._items[id];
},
synthesizeClick: function(id) {
let item = this._items[id];
if (item && item.clickCallback) {
item.clickCallback();
}
},
add: function(aOptions) {
let id = aOptions.id || uuidgen.generateUUID().toString();
EventDispatcher.instance.sendRequest({
type: "PageActions:Add",
id: id,
title: aOptions.title,
icon: resolveGeckoURI(aOptions.icon),
important: "important" in aOptions ? aOptions.important : false,
useTint: "useTint" in aOptions ? aOptions.useTint : false,
});
this._items[id] = {};
if (aOptions.clickCallback) {
this._items[id].clickCallback = aOptions.clickCallback;
}
if (aOptions.longClickCallback) {
this._items[id].longClickCallback = aOptions.longClickCallback;
}
this._maybeInitialize();
return id;
},
remove: function(id) {
EventDispatcher.instance.sendRequest({
type: "PageActions:Remove",
id: id,
});
delete this._items[id];
this._maybeUninitialize();
},
};

View File

@ -4,11 +4,7 @@
"use strict";
var EXPORTED_SYMBOLS = [
"ExtensionActionHelper",
"GeckoViewConnection",
"GeckoViewWebExtension",
];
var EXPORTED_SYMBOLS = ["GeckoViewConnection", "GeckoViewWebExtension"];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
@ -22,76 +18,10 @@ 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);
@ -267,47 +197,10 @@ 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 (
@ -367,7 +260,3 @@ var GeckoViewWebExtension = {
};
GeckoViewWebExtension.extensionScopes = new Map();
// WeakMap[Extension -> BrowserAction]
GeckoViewWebExtension.browserActions = new WeakMap();
// WeakMap[Extension -> PageAction]
GeckoViewWebExtension.pageActions = new WeakMap();

View File

@ -22,6 +22,7 @@ DIRS += ['geckoview']
EXTRA_JS_MODULES += [
'Accounts.jsm',
'ActionBarHandler.jsm',
'BrowserActions.jsm',
'dbg-browser-actors.js',
'DownloadNotifications.jsm',
'FormAssistant.jsm',
@ -34,6 +35,7 @@ EXTRA_JS_MODULES += [
'MediaPlayerApp.jsm',
'NetErrorHelper.jsm',
'Notifications.jsm',
'PageActions.jsm',
'Prompt.jsm',
'RuntimePermissions.jsm',
'Sanitizer.jsm',

View File

@ -1,510 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var EXPORTED_SYMBOLS = ["BrowserActionBase", "PageActionBase"];
const { ExtensionUtils } = ChromeUtils.import(
"resource://gre/modules/ExtensionUtils.jsm"
);
const { ExtensionError } = ExtensionUtils;
const { ExtensionParent } = ChromeUtils.import(
"resource://gre/modules/ExtensionParent.jsm"
);
const { IconDetails, StartupCache } = ExtensionParent;
function parseColor(color, kind) {
if (typeof color == "string") {
let rgba = InspectorUtils.colorToRGBA(color);
if (!rgba) {
throw new ExtensionError(`Invalid badge ${kind} color: "${color}"`);
}
color = [rgba.r, rgba.g, rgba.b, Math.round(rgba.a * 255)];
}
return color;
}
/** Common base class for Page and Browser actions. */
class PanelActionBase {
constructor(options, tabContext, extension) {
this.tabContext = tabContext;
this.extension = extension;
// These are always defined on the action
this.defaults = {
enabled: true,
title: options.default_title || extension.name,
popup: options.default_popup || "",
};
this.globals = Object.create(this.defaults);
// eslint-disable-next-line mozilla/balanced-listeners
this.tabContext.on("location-change", this.handleLocationChange.bind(this));
// eslint-disable-next-line mozilla/balanced-listeners
this.tabContext.on("tab-select", (evt, tab) => {
this.updateOnChange(tab);
});
}
onShutdown() {
this.tabContext.shutdown();
}
setPropertyFromDetails(details, prop, value) {
return this.setProperty(this.getTargetFromDetails(details), prop, value);
}
/**
* Set a global, window specific or tab specific property.
*
* @param {XULElement|ChromeWindow|null} target
* A XULElement tab, a ChromeWindow, or null for the global data.
* @param {string} prop
* String property to set. Should should be one of "icon", "title", "badgeText",
* "popup", "badgeBackgroundColor", "badgeTextColor" or "enabled".
* @param {string} value
* Value for prop.
* @returns {Object}
* The object to which the property has been set.
*/
setProperty(target, prop, value) {
let values = this.getContextData(target);
if (value === null) {
delete values[prop];
} else {
values[prop] = value;
}
this.updateOnChange(target);
return values;
}
/**
* Gets the data associated with a tab, window, or the global one.
*
* @param {XULElement|ChromeWindow|null} target
* A XULElement tab, a ChromeWindow, or null for the global data.
* @returns {Object}
* The icon, title, badge, etc. associated with the target.
*/
getContextData(target) {
if (target) {
return this.tabContext.get(target);
}
return this.globals;
}
/**
* Retrieve the value of a global, window specific or tab specific property.
*
* @param {XULElement|ChromeWindow|null} target
* A XULElement tab, a ChromeWindow, or null for the global data.
* @param {string} prop
* String property to retrieve. Should should be one of "icon", "title",
* "badgeText", "popup", "badgeBackgroundColor" or "enabled".
* @returns {string} value
* Value of prop.
*/
getProperty(target, prop) {
return this.getContextData(target)[prop];
}
getPropertyFromDetails(details, prop) {
return this.getProperty(this.getTargetFromDetails(details), prop);
}
enable(tabId) {
this.setPropertyFromDetails({ tabId }, "enabled", true);
}
disable(tabId) {
this.setPropertyFromDetails({ tabId }, "enabled", false);
}
getIcon(details = {}) {
return this.getPropertyFromDetails(details, "icon");
}
normalizeIcon(details, extension, context) {
let icon = IconDetails.normalize(details, extension, context);
if (!Object.keys(icon).length) {
return null;
}
return icon;
}
/**
* Updates the `tabData` for any location change, however it only updates the button
* when the selected tab has a location change, or the selected tab has changed.
*
* @param {string} eventType
* The type of the event, should be "location-change".
* @param {XULElement} tab
* The tab whose location changed, or which has become selected.
* @param {boolean} [fromBrowse]
* - `true` if navigation occurred in `tab`.
* - `false` if the location changed but no navigation occurred, e.g. due to
a hash change or `history.pushState`.
* - Omitted if TabSelect has occurred, tabData does not need to be updated.
*/
handleLocationChange(eventType, tab, fromBrowse) {
if (fromBrowse) {
this.tabContext.clear(tab);
}
}
api(context) {
let { extension } = context;
return {
setTitle: details => {
this.setPropertyFromDetails(details, "title", details.title);
},
getTitle: details => {
return this.getPropertyFromDetails(details, "title");
},
setIcon: details => {
details.iconType = "browserAction";
this.setPropertyFromDetails(
details,
"icon",
this.normalizeIcon(details, extension, context)
);
},
setPopup: details => {
// Note: Chrome resolves arguments to setIcon relative to the calling
// context, but resolves arguments to setPopup relative to the extension
// root.
// For internal consistency, we currently resolve both relative to the
// calling context.
let url = details.popup && context.uri.resolve(details.popup);
if (url && !context.checkLoadURL(url)) {
return Promise.reject({ message: `Access denied for URL ${url}` });
}
this.setPropertyFromDetails(details, "popup", url);
},
getPopup: details => {
return this.getPropertyFromDetails(details, "popup");
},
};
}
// Override these
/**
* Update the toolbar button when the extension changes the icon, title, url, etc.
* If it only changes a parameter for a single tab, `target` will be that tab.
* If it only changes a parameter for a single window, `target` will be that window.
* Otherwise `target` will be null.
*
* @param {XULElement|ChromeWindow|null} target
* Browser tab or browser chrome window, may be null.
*/
updateOnChange(target) {}
/**
* Get tab object from tabId.
*
* @param {string} tabId
* Internal id of the tab to get.
*/
getTab(tabId) {}
/**
* Get window object from windowId
*
* @param {string} windowId
* Internal id of the window to get.
*/
getWindow(windowId) {}
/**
* Gets the target object corresponding to the `details` parameter of the various
* get* and set* API methods.
*
* @param {Object} details
* An object with optional `tabId` or `windowId` properties.
* @throws if both `tabId` and `windowId` are specified, or if they are invalid.
* @returns {XULElement|ChromeWindow|null}
* If a `tabId` was specified, the corresponding XULElement tab.
* If a `windowId` was specified, the corresponding ChromeWindow.
* Otherwise, `null`.
*/
getTargetFromDetails({ tabId, windowId }) {
return null;
}
}
class PageActionBase extends PanelActionBase {
constructor(tabContext, extension) {
const options = extension.manifest.page_action;
super(options, tabContext, extension);
// `enabled` can have three different values:
// - `false`. This means the page action is not shown.
// It's set as default if show_matches is empty. Can also be set in a tab via
// `pageAction.hide(tabId)`, e.g. in order to override show_matches.
// - `true`. This means the page action is shown.
// It's never set as default because <all_urls> doesn't really match all URLs
// (e.g. "about:" URLs). But can be set in a tab via `pageAction.show(tabId)`.
// - `undefined`.
// This is the default value when there are some patterns in show_matches.
// Can't be set as a tab-specific value.
let enabled, showMatches, hideMatches;
let show_matches = options.show_matches || [];
let hide_matches = options.hide_matches || [];
if (!show_matches.length) {
// Always hide by default. No need to do any pattern matching.
enabled = false;
} else {
// Might show or hide depending on the URL. Enable pattern matching.
const { restrictSchemes } = extension;
showMatches = new MatchPatternSet(show_matches, { restrictSchemes });
hideMatches = new MatchPatternSet(hide_matches, { restrictSchemes });
}
this.defaults = {
...this.defaults,
enabled,
showMatches,
hideMatches,
pinned: options.pinned,
};
this.globals = Object.create(this.defaults);
}
handleLocationChange(eventType, tab, fromBrowse) {
super.handleLocationChange(eventType, tab, fromBrowse);
if (fromBrowse === false) {
// Clear pattern matching cache when URL changes.
let tabData = this.tabContext.get(tab);
if (tabData.patternMatching !== undefined) {
tabData.patternMatching = undefined;
}
}
if (tab.selected) {
// isShownForTab will do pattern matching (if necessary) and store the result
// so that updateButton knows whether the page action should be shown.
this.isShownForTab(tab);
this.updateOnChange(tab);
}
}
// Checks whether the tab action is shown when the specified tab becomes active.
// Does pattern matching if necessary, and caches the result as a tab-specific value.
// @param {XULElement} tab
// The tab to be checked
// @return boolean
isShownForTab(tab) {
let tabData = this.getContextData(tab);
// If there is a "show" value, return it. Can be due to show(), hide() or empty show_matches.
if (tabData.enabled !== undefined) {
return tabData.enabled;
}
// Otherwise pattern matching must have been configured. Do it, caching the result.
if (tabData.patternMatching === undefined) {
let uri = tab.linkedBrowser.currentURI;
tabData.patternMatching =
tabData.showMatches.matches(uri) && !tabData.hideMatches.matches(uri);
}
return tabData.patternMatching;
}
async loadIconData() {
const { extension } = this;
const options = extension.manifest.page_action;
this.defaults.icon = await StartupCache.get(
extension,
["pageAction", "default_icon"],
() =>
this.normalizeIcon(
{ path: options.default_icon || "" },
extension,
null
)
);
}
getPinned() {
return this.globals.pinned;
}
getTargetFromDetails({ tabId, windowId }) {
// PageActionBase doesn't support |windowId|
if (tabId != null) {
return this.getTab(tabId);
}
return null;
}
api(context) {
return {
...super.api(context),
show: (...args) => this.enable(...args),
hide: (...args) => this.disable(...args),
isShown: ({ tabId }) => {
let tab = this.getTab(tabId);
return this.isShownForTab(tab);
},
};
}
}
class BrowserActionBase extends PanelActionBase {
constructor(tabContext, extension) {
const options = extension.manifest.browser_action;
super(options, tabContext, extension);
this.defaults = {
...this.defaults,
badgeText: "",
badgeBackgroundColor: [0xd9, 0, 0, 255],
badgeDefaultColor: [255, 255, 255, 255],
badgeTextColor: null,
default_area: options.default_area || "navbar",
};
this.globals = Object.create(this.defaults);
}
async loadIconData() {
const { extension } = this;
const options = extension.manifest.browser_action;
this.defaults.icon = await StartupCache.get(
extension,
["browserAction", "default_icon"],
() =>
IconDetails.normalize(
{
path: options.default_icon || extension.manifest.icons,
iconType: "browserAction",
themeIcons: options.theme_icons,
},
extension
)
);
}
handleLocationChange(eventType, tab, fromBrowse) {
super.handleLocationChange(eventType, tab, fromBrowse);
if (fromBrowse) {
this.updateOnChange(tab);
}
}
getTargetFromDetails({ tabId, windowId }) {
if (tabId != null && windowId != null) {
throw new ExtensionError(
"Only one of tabId and windowId can be specified."
);
}
if (tabId != null) {
return this.getTab(tabId);
} else if (windowId != null) {
return this.getWindow(windowId);
}
return null;
}
getDefaultArea() {
return this.globals.default_area;
}
/**
* Determines the text badge color to be used in a tab, window, or globally.
*
* @param {Object} values
* The values associated with the tab or window, or global values.
* @returns {ColorArray}
*/
getTextColor(values) {
// If a text color has been explicitly provided, use it.
let { badgeTextColor } = values;
if (badgeTextColor) {
return badgeTextColor;
}
// Otherwise, check if the default color to be used has been cached previously.
let { badgeDefaultColor } = values;
if (badgeDefaultColor) {
return badgeDefaultColor;
}
// Choose a color among white and black, maximizing contrast with background
// according to https://www.w3.org/TR/WCAG20-TECHS/G18.html#G18-procedure
let [r, g, b] = values.badgeBackgroundColor
.slice(0, 3)
.map(function(channel) {
channel /= 255;
if (channel <= 0.03928) {
return channel / 12.92;
}
return ((channel + 0.055) / 1.055) ** 2.4;
});
let lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
// The luminance is 0 for black, 1 for white, and `lum` for the background color.
// Since `0 <= lum`, the contrast ratio for black is `c0 = (lum + 0.05) / 0.05`.
// Since `lum <= 1`, the contrast ratio for white is `c1 = 1.05 / (lum + 0.05)`.
// We want to maximize contrast, so black is chosen if `c1 < c0`, that is, if
// `1.05 * 0.05 < (L + 0.05) ** 2`. Otherwise white is chosen.
let channel = 1.05 * 0.05 < (lum + 0.05) ** 2 ? 0 : 255;
let result = [channel, channel, channel, 255];
// Cache the result as high as possible in the prototype chain
while (!Object.getOwnPropertyDescriptor(values, "badgeDefaultColor")) {
values = Object.getPrototypeOf(values);
}
values.badgeDefaultColor = result;
return result;
}
api(context) {
return {
...super.api(context),
enable: (...args) => this.enable(...args),
disable: (...args) => this.disable(...args),
isEnabled: details => {
return this.getPropertyFromDetails(details, "enabled");
},
setBadgeText: details => {
this.setPropertyFromDetails(details, "badgeText", details.text);
},
getBadgeText: details => {
return this.getPropertyFromDetails(details, "badgeText");
},
setBadgeBackgroundColor: details => {
let color = parseColor(details.color, "background");
let values = this.setPropertyFromDetails(
details,
"badgeBackgroundColor",
color
);
if (color === null) {
// Let the default text color inherit after removing background color
delete values.badgeDefaultColor;
} else {
// Invalidate a cached default color calculated with the old background
values.badgeDefaultColor = null;
}
},
getBadgeBackgroundColor: details => {
return this.getPropertyFromDetails(details, "badgeBackgroundColor");
},
setBadgeTextColor: details => {
let color = parseColor(details.color, "text");
this.setPropertyFromDetails(details, "badgeTextColor", color);
},
getBadgeTextColor: details => {
let target = this.getTargetFromDetails(details);
let values = this.getContextData(target);
return this.getTextColor(values);
},
};
}
}

View File

@ -10,7 +10,6 @@ with Files('**'):
EXTRA_JS_MODULES += [
'Extension.jsm',
'ExtensionActions.jsm',
'ExtensionActivityLog.jsm',
'ExtensionChild.jsm',
'ExtensionChildDevToolsUtils.jsm',

View File

@ -6,7 +6,6 @@ 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
@ -35,7 +34,6 @@ 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

View File

@ -56,6 +56,7 @@ DIRS += [
'promiseworker',
'prompts',
'protobuf',
'reader',
'remotebrowserutils',
'remotepagemanager',
'reflect',
@ -84,10 +85,7 @@ DIRS += [
]
if CONFIG['MOZ_BUILD_APP'] != 'mobile/android':
DIRS += [
'narrate',
'reader',
];
DIRS += ['narrate'];
if CONFIG['NS_PRINTING']:
DIRS += ['printing']

View File

@ -1,172 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "ImageDecoderSupport.h"
#include "imgITools.h"
#include "gfxUtils.h"
#include "AndroidGraphics.h"
#include "JavaExceptions.h"
#include "mozilla/gfx/Point.h"
#include "mozilla/gfx/Swizzle.h"
namespace mozilla {
namespace widget {
namespace {
class ImageCallbackHelper;
HashSet<RefPtr<ImageCallbackHelper>,
PointerHasher<ImageCallbackHelper*>>
gDecodeRequests;
class ImageCallbackHelper : public imgIContainerCallback,
public imgINotificationObserver {
public:
NS_DECL_ISUPPORTS
void CompleteExceptionally(const char* aMessage) {
mResult->CompleteExceptionally(java::sdk::IllegalArgumentException::New(aMessage)
.Cast<jni::Throwable>());
gDecodeRequests.remove(this);
}
void Complete(DataSourceSurface::ScopedMap& aSourceSurface, int32_t width, int32_t height) {
auto pixels = mozilla::jni::ByteBuffer::New(
reinterpret_cast<int8_t*>(aSourceSurface.GetData()),
aSourceSurface.GetStride() * height);
auto bitmap = java::sdk::Bitmap::CreateBitmap(
width, height, java::sdk::Config::ARGB_8888());
bitmap->CopyPixelsFromBuffer(pixels);
mResult->Complete(bitmap);
gDecodeRequests.remove(this);
}
ImageCallbackHelper(java::GeckoResult::Param aResult, int32_t aDesiredLength)
: mResult(aResult), mDesiredLength(aDesiredLength), mImage(nullptr) {
MOZ_ASSERT(mResult);
}
NS_IMETHOD
OnImageReady(imgIContainer* aImage, nsresult aStatus) override {
// Let's make sure we are alive until the request completes
MOZ_ALWAYS_TRUE(gDecodeRequests.putNew(this));
if (NS_FAILED(aStatus)) {
CompleteExceptionally("Could not process image.");
return aStatus;
}
mImage = aImage;
return mImage->StartDecoding(
imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY,
imgIContainer::FRAME_FIRST);
}
// This method assumes that the image is ready to be processed
nsresult SendBitmap() {
RefPtr<gfx::SourceSurface> surface;
if (mDesiredLength > 0) {
surface = mImage->GetFrameAtSize(
gfx::IntSize(mDesiredLength, mDesiredLength),
imgIContainer::FRAME_FIRST,
imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
} else {
surface = mImage->GetFrame(
imgIContainer::FRAME_FIRST,
imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
}
RefPtr<DataSourceSurface> dataSurface = surface->GetDataSurface();
NS_ENSURE_TRUE(dataSurface, NS_ERROR_FAILURE);
int32_t width = dataSurface->GetSize().width;
int32_t height = dataSurface->GetSize().height;
DataSourceSurface::ScopedMap sourceMap(dataSurface,
DataSourceSurface::READ);
// Android's Bitmap only supports R8G8B8A8, so we need to convert the
// data to the right format
RefPtr<DataSourceSurface> destDataSurface =
Factory::CreateDataSourceSurfaceWithStride(dataSurface->GetSize(),
SurfaceFormat::R8G8B8A8,
sourceMap.GetStride());
NS_ENSURE_TRUE(destDataSurface, NS_ERROR_FAILURE);
DataSourceSurface::ScopedMap destMap(destDataSurface,
DataSourceSurface::READ_WRITE);
SwizzleData(sourceMap.GetData(), sourceMap.GetStride(),
surface->GetFormat(), destMap.GetData(), destMap.GetStride(),
SurfaceFormat::R8G8B8A8, destDataSurface->GetSize());
Complete(destMap, width, height);
return NS_OK;
}
NS_IMETHOD
Notify(imgIRequest* aRequest, int32_t aType,
const nsIntRect* aData) override {
if (aType == imgINotificationObserver::DECODE_COMPLETE) {
SendBitmap();
}
return NS_OK;
}
private:
const java::GeckoResult::GlobalRef mResult;
int32_t mDesiredLength;
nsCOMPtr<imgIContainer> mImage;
virtual ~ImageCallbackHelper() {}
};
NS_IMPL_ISUPPORTS(ImageCallbackHelper, imgIContainerCallback,
imgINotificationObserver)
} // namespace
/* static */ void ImageDecoderSupport::Decode(jni::String::Param aUri,
int32_t aDesiredLength,
jni::Object::Param aResult) {
auto result = java::GeckoResult::LocalRef(aResult);
RefPtr<ImageCallbackHelper> helper =
new ImageCallbackHelper(result, aDesiredLength);
nsresult rv = DecodeInternal(aUri->ToString(), helper, helper);
if (NS_FAILED(rv)) {
helper->OnImageReady(nullptr, rv);
}
}
/* static */ nsresult ImageDecoderSupport::DecodeInternal(
const nsAString& aUri, imgIContainerCallback* aCallback,
imgINotificationObserver* aObserver) {
nsCOMPtr<imgITools> imgTools = do_GetService("@mozilla.org/image/tools;1");
if (NS_WARN_IF(!imgTools)) {
return NS_ERROR_FAILURE;
}
nsCOMPtr<nsIURI> uri;
nsresult rv = NS_NewURI(getter_AddRefs(uri), aUri);
NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI);
nsCOMPtr<nsIChannel> channel;
rv = NS_NewChannel(getter_AddRefs(channel), uri,
nsContentUtils::GetSystemPrincipal(),
nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
nsIContentPolicy::TYPE_IMAGE);
NS_ENSURE_SUCCESS(rv, rv);
return imgTools->DecodeImageFromChannelAsync(uri, channel, aCallback,
aObserver);
}
} // namespace widget
} // namespace mozilla

View File

@ -1,30 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef ImageDecoderSupport_h__
#define ImageDecoderSupport_h__
#include "GeneratedJNINatives.h"
class imgIContainerCallback;
namespace mozilla {
namespace widget {
class ImageDecoderSupport final
: public java::ImageDecoder::Natives<ImageDecoderSupport> {
public:
static void Decode(jni::String::Param aUri, int32_t aDesiredLength,
jni::Object::Param aResult);
private:
static nsresult DecodeInternal(const nsAString& aUri,
imgIContainerCallback* aCallback,
imgINotificationObserver* aObserver);
};
} // namespace widget
} // namespace mozilla
#endif // ImageDecoderSupport_h__

View File

@ -1,5 +1,2 @@
[java.lang.IllegalStateException = skip:true]
<init>(Ljava/lang/String;)V =
[java.lang.IllegalArgumentException = skip:true]
<init>(Ljava/lang/String;)V =
<init>(Ljava/lang/String;)V =

View File

@ -51,7 +51,6 @@ UNIFIED_SOURCES += [
'EventDispatcher.cpp',
'GeckoEditableSupport.cpp',
'GfxInfo.cpp',
'ImageDecoderSupport.cpp',
'nsAndroidProtocolHandler.cpp',
'nsAppShell.cpp',
'nsClipboard.cpp',

View File

@ -66,7 +66,6 @@
#include "GeckoSystemStateListener.h"
#include "GeckoTelemetryDelegate.h"
#include "GeckoVRManager.h"
#include "ImageDecoderSupport.h"
#include "PrefsHelper.h"
#include "ScreenHelperAndroid.h"
#include "Telemetry.h"
@ -435,7 +434,6 @@ nsAppShell::nsAppShell()
mozilla::GeckoSystemStateListener::Init();
mozilla::PrefsHelper::Init();
mozilla::widget::Telemetry::Init();
mozilla::widget::ImageDecoderSupport::Init();
mozilla::widget::WebExecutorSupport::Init();
mozilla::widget::Base64UtilsSupport::Init();
nsWindow::InitNatives();