--- a/mobile/android/components/extensions/.eslintrc.js
+++ b/mobile/android/components/extensions/.eslintrc.js
@@ -1,5 +1,14 @@
"use strict";
module.exports = {
"extends": "../../../../toolkit/components/extensions/.eslintrc.js",
+
+ "globals": {
+ "getCookieStoreIdForTab": false,
+ "GlobalEventDispatcher": true,
+ "GlobalEventManager": true,
+ "tabTracker": true,
+ "WindowEventManager": true,
+ "windowTracker": true,
+ },
};
copy from browser/components/extensions/ext-c-tabs.js
copy to mobile/android/components/extensions/ext-c-tabs.js
copy from browser/components/extensions/ext-tabs.js
copy to mobile/android/components/extensions/ext-tabs.js
--- a/browser/components/extensions/ext-tabs.js
+++ b/mobile/android/components/extensions/ext-tabs.js
@@ -51,44 +51,50 @@ global.tabGetSender = getSender;
extensions.on("page-shutdown", (type, context) => {
if (context.viewType == "tab") {
if (context.extension.id !== context.xulBrowser.contentPrincipal.addonId) {
// Only close extension tabs.
// This check prevents about:addons from closing when it contains a
// WebExtension as an embedded inline options page.
return;
}
- let {gBrowser} = context.xulBrowser.ownerGlobal;
- if (gBrowser) {
- let tab = gBrowser.getTabForBrowser(context.xulBrowser);
+ let {BrowserApp} = context.xulBrowser.ownerGlobal;
+ if (BrowserApp) {
+ let tab = BrowserApp.getTabForBrowser(context.xulBrowser);
if (tab) {
- gBrowser.removeTab(tab);
+ BrowserApp.closeTab(tab);
}
}
}
});
/* eslint-enable mozilla/balanced-listeners */
+function getBrowserWindow(window) {
+ return window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIDocShellTreeItem).rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+}
+
let tabListener = {
tabReadyInitialized: false,
tabReadyPromises: new WeakMap(),
initializingTabs: new WeakSet(),
initTabReady() {
if (!this.tabReadyInitialized) {
windowTracker.addListener("progress", this);
this.tabReadyInitialized = true;
}
},
onLocationChange(browser, webProgress, request, locationURI, flags) {
if (webProgress.isTopLevel) {
- let {gBrowser} = browser.ownerGlobal;
- let tab = gBrowser.getTabForBrowser(browser);
+ let {BrowserApp} = browser.ownerGlobal;
+ let tab = BrowserApp.getTabForBrowser(browser);
// Now we are certain that the first page in the tab was loaded.
this.initializingTabs.delete(tab);
// browser.innerWindowID is now set, resolve the promises if any.
let deferred = this.tabReadyPromises.get(tab);
if (deferred) {
deferred.resolve(tab);
@@ -105,18 +111,18 @@ let tabListener = {
*
* @param {XULElement} tab The <tab> element.
* @returns {Promise} Resolves with the given tab once ready.
*/
awaitTabReady(tab) {
let deferred = this.tabReadyPromises.get(tab);
if (!deferred) {
deferred = PromiseUtils.defer();
- if (!this.initializingTabs.has(tab) && (tab.linkedBrowser.innerWindowID ||
- tab.linkedBrowser.currentURI.spec === "about:blank")) {
+ if (!this.initializingTabs.has(tab) && (tab.browser.innerWindowID ||
+ tab.browser.currentURI.spec === "about:blank")) {
deferred.resolve(tab);
} else {
this.initTabReady();
this.tabReadyPromises.set(tab, deferred);
}
}
return deferred.promise;
},
@@ -144,21 +150,20 @@ extensions.registerSchemaAPI("tabs", "ad
await tabListener.awaitTabReady(tab.tab);
return tab;
}
let self = {
tabs: {
- onActivated: new WindowEventManager(context, "tabs.onActivated", "TabSelect", (fire, event) => {
- let tab = event.originalTarget;
- let tabId = tabTracker.getId(tab);
- let windowId = windowTracker.getId(tab.ownerGlobal);
- fire({tabId, windowId});
+ onActivated: new GlobalEventManager(context, "tabs.onActivated", "Tab:Selected", (fire, data) => {
+ let tab = tabManager.get(data.id);
+
+ fire({tabId: tab.id, windowId: tab.windowId});
}).api(),
onCreated: new EventManager(context, "tabs.onCreated", fire => {
let listener = (eventName, event) => {
fire(tabManager.convert(event.tab));
};
tabTracker.on("tab-created", listener);
@@ -168,99 +173,45 @@ extensions.registerSchemaAPI("tabs", "ad
}).api(),
/**
* Since multiple tabs currently can't be highlighted, onHighlighted
* essentially acts an alias for self.tabs.onActivated but returns
* the tabId in an array to match the API.
* @see https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/Tabs/onHighlighted
*/
- onHighlighted: new WindowEventManager(context, "tabs.onHighlighted", "TabSelect", (fire, event) => {
- let tab = event.originalTarget;
- let tabIds = [tabTracker.getId(tab)];
- let windowId = windowTracker.getId(tab.ownerGlobal);
- fire({tabIds, windowId});
+ onHighlighted: new GlobalEventManager(context, "tabs.onHighlighted", "Tab:Selected", (fire, data) => {
+ let tab = tabManager.get(data.id);
+
+ fire({tabIds: [tab.id], windowId: tab.windowId});
}).api(),
onAttached: new EventManager(context, "tabs.onAttached", fire => {
- let listener = (eventName, event) => {
- fire(event.tabId, {newWindowId: event.newWindowId, newPosition: event.newPosition});
- };
-
- tabTracker.on("tab-attached", listener);
- return () => {
- tabTracker.off("tab-attached", listener);
- };
+ return () => {};
}).api(),
onDetached: new EventManager(context, "tabs.onDetached", fire => {
- let listener = (eventName, event) => {
- fire(event.tabId, {oldWindowId: event.oldWindowId, oldPosition: event.oldPosition});
- };
-
- tabTracker.on("tab-detached", listener);
- return () => {
- tabTracker.off("tab-detached", listener);
- };
+ return () => {};
}).api(),
onRemoved: new EventManager(context, "tabs.onRemoved", fire => {
let listener = (eventName, event) => {
fire(event.tabId, {windowId: event.windowId, isWindowClosing: event.isWindowClosing});
};
tabTracker.on("tab-removed", listener);
return () => {
tabTracker.off("tab-removed", listener);
};
}).api(),
onReplaced: ignoreEvent(context, "tabs.onReplaced"),
onMoved: new EventManager(context, "tabs.onMoved", fire => {
- // There are certain circumstances where we need to ignore a move event.
- //
- // Namely, the first time the tab is moved after it's created, we need
- // to report the final position as the initial position in the tab's
- // onAttached or onCreated event. This is because most tabs are inserted
- // in a temporary location and then moved after the TabOpen event fires,
- // which generates a TabOpen event followed by a TabMove event, which
- // does not match the contract of our API.
- let ignoreNextMove = new WeakSet();
-
- let openListener = event => {
- ignoreNextMove.add(event.target);
- // Remove the tab from the set on the next tick, since it will already
- // have been moved by then.
- Promise.resolve().then(() => {
- ignoreNextMove.delete(event.target);
- });
- };
-
- let moveListener = event => {
- let tab = event.originalTarget;
-
- if (ignoreNextMove.has(tab)) {
- ignoreNextMove.delete(tab);
- return;
- }
-
- fire(tabTracker.getId(tab), {
- windowId: windowTracker.getId(tab.ownerGlobal),
- fromIndex: event.detail,
- toIndex: tab._tPos,
- });
- };
-
- windowTracker.addListener("TabMove", moveListener);
- windowTracker.addListener("TabOpen", openListener);
- return () => {
- windowTracker.removeListener("TabMove", moveListener);
- windowTracker.removeListener("TabOpen", openListener);
- };
+ return () => {};
}).api(),
onUpdated: new EventManager(context, "tabs.onUpdated", fire => {
const restricted = ["url", "favIconUrl", "title"];
function sanitize(extension, changeInfo) {
let result = {};
let nonempty = false;
@@ -277,240 +228,169 @@ extensions.registerSchemaAPI("tabs", "ad
let [needed, changeInfo] = sanitize(extension, changed);
if (needed) {
fire(tab.id, changeInfo, tab.convert());
}
};
let listener = event => {
let needed = [];
- if (event.type == "TabAttrModified") {
- let changed = event.detail.changed;
- if (changed.includes("image")) {
- needed.push("favIconUrl");
- }
- if (changed.includes("muted")) {
- needed.push("mutedInfo");
+ let tab;
+ switch (event.type) {
+ case "DOMTitleChanged": {
+ let {BrowserApp} = getBrowserWindow(event.target.ownerGlobal);
+
+ tab = BrowserApp.getTabForWindow(event.target.ownerGlobal);
+ needed.push("title");
+ break;
}
- if (changed.includes("soundplaying")) {
+
+ case "DOMAudioPlaybackStarted":
+ case "DOMAudioPlaybackStopped": {
+ let {BrowserApp} = event.target.ownerGlobal;
+ tab = BrowserApp.getTabForBrowser(event.originalTarget);
needed.push("audible");
+ break;
}
- if (changed.includes("label")) {
- needed.push("title");
- }
- } else if (event.type == "TabPinned") {
- needed.push("pinned");
- } else if (event.type == "TabUnpinned") {
- needed.push("pinned");
}
- let tab = tabManager.getWrapper(event.originalTarget);
+ if (!tab) {
+ return;
+ }
+
+ tab = tabManager.getWrapper(tab);
let changeInfo = {};
for (let prop of needed) {
changeInfo[prop] = tab[prop];
}
fireForTab(tab, changeInfo);
};
let statusListener = ({browser, status, url}) => {
- let {gBrowser} = browser.ownerGlobal;
- let tabElem = gBrowser.getTabForBrowser(browser);
- if (tabElem) {
+ let {BrowserApp} = browser.ownerGlobal;
+ let tab = BrowserApp.getTabForBrowser(browser);
+ if (tab) {
let changed = {status};
if (url) {
changed.url = url;
}
- fireForTab(tabManager.wrapTab(tabElem), changed);
+ fireForTab(tabManager.wrapTab(tab), changed);
}
};
windowTracker.addListener("status", statusListener);
- windowTracker.addListener("TabAttrModified", listener);
- windowTracker.addListener("TabPinned", listener);
- windowTracker.addListener("TabUnpinned", listener);
-
+ windowTracker.addListener("DOMTitleChanged", listener);
return () => {
windowTracker.removeListener("status", statusListener);
- windowTracker.removeListener("TabAttrModified", listener);
- windowTracker.removeListener("TabPinned", listener);
- windowTracker.removeListener("TabUnpinned", listener);
+ windowTracker.removeListener("DOMTitleChanged", listener);
};
}).api(),
- create(createProperties) {
- return new Promise((resolve, reject) => {
- let window = createProperties.windowId !== null ?
- windowTracker.getWindow(createProperties.windowId, context) :
- windowTracker.topWindow;
+ async create(createProperties) {
+ let window = createProperties.windowId !== null ?
+ windowTracker.getWindow(createProperties.windowId, context) :
+ windowTracker.topWindow;
- if (!window.gBrowser) {
- let obs = (finishedWindow, topic, data) => {
- if (finishedWindow != window) {
- return;
- }
- Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
- resolve(window);
- };
- Services.obs.addObserver(obs, "browser-delayed-startup-finished", false);
- } else {
- resolve(window);
- }
- }).then(window => {
- let url;
-
- if (createProperties.url !== null) {
- url = context.uri.resolve(createProperties.url);
+ let {BrowserApp} = window;
+ let url;
- if (!context.checkLoadURL(url, {dontReportErrors: true})) {
- return Promise.reject({message: `Illegal URL: ${url}`});
- }
- }
-
- if (createProperties.cookieStoreId && !extension.hasPermission("cookies")) {
- return Promise.reject({message: `No permission for cookieStoreId: ${createProperties.cookieStoreId}`});
- }
+ if (createProperties.url !== null) {
+ url = context.uri.resolve(createProperties.url);
- let options = {};
- if (createProperties.cookieStoreId) {
- if (!global.isValidCookieStoreId(createProperties.cookieStoreId)) {
- return Promise.reject({message: `Illegal cookieStoreId: ${createProperties.cookieStoreId}`});
- }
+ if (!context.checkLoadURL(url, {dontReportErrors: true})) {
+ return Promise.reject({message: `Illegal URL: ${url}`});
+ }
+ }
- let privateWindow = PrivateBrowsingUtils.isBrowserPrivate(window.gBrowser);
- if (privateWindow && !global.isPrivateCookieStoreId(createProperties.cookieStoreId)) {
- return Promise.reject({message: `Illegal to set non-private cookieStoreId in a private window`});
- }
-
- if (!privateWindow && global.isPrivateCookieStoreId(createProperties.cookieStoreId)) {
- return Promise.reject({message: `Illegal to set private cookieStoreId in a non-private window`});
- }
+ let options = {};
- if (global.isContainerCookieStoreId(createProperties.cookieStoreId)) {
- let containerId = global.getContainerForCookieStoreId(createProperties.cookieStoreId);
- if (!containerId) {
- return Promise.reject({message: `No cookie store exists with ID ${createProperties.cookieStoreId}`});
- }
+ let active = true;
+ if (createProperties.active !== null) {
+ active = createProperties.active;
+ }
+ options.selected = active;
- options.userContextId = containerId;
- }
- }
-
- // Make sure things like about:blank and data: URIs never inherit,
- // and instead always get a NullPrincipal.
- options.disallowInheritPrincipal = true;
-
- tabListener.initTabReady();
- let tab = window.gBrowser.addTab(url || window.BROWSER_NEW_TAB_URL, options);
+ if (createProperties.index !== null) {
+ options.tabIndex = createProperties.index;
+ }
- let active = true;
- if (createProperties.active !== null) {
- active = createProperties.active;
- }
- if (active) {
- window.gBrowser.selectedTab = tab;
- }
-
- if (createProperties.index !== null) {
- window.gBrowser.moveTabTo(tab, createProperties.index);
- }
+ // Make sure things like about:blank and data: URIs never inherit,
+ // and instead always get a NullPrincipal.
+ options.disallowInheritPrincipal = true;
- if (createProperties.pinned) {
- window.gBrowser.pinTab(tab);
- }
-
- if (createProperties.url && createProperties.url !== window.BROWSER_NEW_TAB_URL) {
- // We can't wait for a location change event for about:newtab,
- // since it may be pre-rendered, in which case its initial
- // location change event has already fired.
+ tabListener.initTabReady();
+ let tab = BrowserApp.addTab(url, options);
- // Mark the tab as initializing, so that operations like
- // `executeScript` wait until the requested URL is loaded in
- // the tab before dispatching messages to the inner window
- // that contains the URL we're attempting to load.
- tabListener.initializingTabs.add(tab);
- }
+ if (createProperties.url) {
+ tabListener.initializingTabs.add(tab);
+ }
- return tabManager.convert(tab);
- });
+ return tabManager.convert(tab);
},
- async remove(tabs) {
+ remove(tabs) {
if (!Array.isArray(tabs)) {
tabs = [tabs];
}
for (let tabId of tabs) {
let tab = tabTracker.getTab(tabId);
- tab.ownerGlobal.gBrowser.removeTab(tab);
+ tab.browser.ownerGlobal.BrowserApp.closeTab(tab);
}
+
+ return Promise.resolve();
},
async update(tabId, updateProperties) {
let tab = getTabOrActive(tabId);
- let tabbrowser = tab.ownerGlobal.gBrowser;
+ let {BrowserApp} = tab.browser.ownerGlobal;
if (updateProperties.url !== null) {
let url = context.uri.resolve(updateProperties.url);
if (!context.checkLoadURL(url, {dontReportErrors: true})) {
return Promise.reject({message: `Illegal URL: ${url}`});
}
- tab.linkedBrowser.loadURI(url);
+ tab.browser.loadURI(url);
}
if (updateProperties.active !== null) {
if (updateProperties.active) {
- tabbrowser.selectedTab = tab;
+ BrowserApp.selectTab(tab);
} else {
// Not sure what to do here? Which tab should we select?
}
}
- if (updateProperties.muted !== null) {
- if (tab.muted != updateProperties.muted) {
- tab.toggleMuteAudio(extension.uuid);
- }
- }
- if (updateProperties.pinned !== null) {
- if (updateProperties.pinned) {
- tabbrowser.pinTab(tab);
- } else {
- tabbrowser.unpinTab(tab);
- }
- }
- // FIXME: highlighted/selected, openerTabId
+ // FIXME: highlighted/selected, muted, pinned, openerTabId
- return tabManager.convert(tab);
+ return Promise.resolve(tabManager.convert(tab));
},
async reload(tabId, reloadProperties) {
let tab = getTabOrActive(tabId);
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
if (reloadProperties && reloadProperties.bypassCache) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
}
- tab.linkedBrowser.reloadWithFlags(flags);
+ tab.browser.reloadWithFlags(flags);
},
async get(tabId) {
- let tab = tabTracker.getTab(tabId);
-
- return tabManager.convert(tab);
+ return tabManager.get(tabId).convert();
},
- getCurrent() {
- let tab;
+ async getCurrent() {
if (context.tabId) {
- tab = tabManager.get(context.tabId).convert();
+ return tabManager.get(context.tabId).convert();
}
- return Promise.resolve(tab);
},
async query(queryInfo) {
if (queryInfo.url !== null) {
if (!extension.hasPermission("tabs")) {
return Promise.reject({message: 'The "tabs" permission is required to use the query API with the "url" parameter'});
}
@@ -526,19 +406,19 @@ extensions.registerSchemaAPI("tabs", "ad
if (!extension.hasPermission("<all_urls>")) {
return Promise.reject({message: "The <all_urls> permission is required to use the captureVisibleTab API"});
}
let window = windowId == null ?
windowTracker.topWindow :
windowTracker.getWindow(windowId, context);
- let tab = window.gBrowser.selectedTab;
+ let tab = window.BrowserApp.selectedTab;
return tabListener.awaitTabReady(tab).then(() => {
- let browser = tab.linkedBrowser;
+ let {browser} = tab;
let recipient = {
innerWindowID: browser.innerWindowID,
};
if (!options) {
options = {};
}
if (options.format == null) {
@@ -554,28 +434,16 @@ extensions.registerSchemaAPI("tabs", "ad
height: browser.clientHeight,
};
return context.sendMessage(browser.messageManager, "Extension:Capture",
message, {recipient});
});
},
- async detectLanguage(tabId) {
- let tab = getTabOrActive(tabId);
-
- return tabListener.awaitTabReady(tab).then(() => {
- let browser = tab.linkedBrowser;
- let recipient = {innerWindowID: browser.innerWindowID};
-
- return context.sendMessage(browser.messageManager, "Extension:DetectLanguage",
- {}, {recipient});
- });
- },
-
async executeScript(tabId, details) {
let tab = await promiseTabOrActive(tabId);
return tab.executeScript(context, details);
},
async insertCSS(tabId, details) {
let tab = await promiseTabOrActive(tabId);
@@ -583,233 +451,12 @@ extensions.registerSchemaAPI("tabs", "ad
return tab.insertCSS(context, details);
},
async removeCSS(tabId, details) {
let tab = await promiseTabOrActive(tabId);
return tab.removeCSS(context, details);
},
-
- async move(tabIds, moveProperties) {
- let index = moveProperties.index;
- let tabsMoved = [];
- if (!Array.isArray(tabIds)) {
- tabIds = [tabIds];
- }
-
- let destinationWindow = null;
- if (moveProperties.windowId !== null) {
- destinationWindow = windowTracker.getWindow(moveProperties.windowId);
- // Fail on an invalid window.
- if (!destinationWindow) {
- return Promise.reject({message: `Invalid window ID: ${moveProperties.windowId}`});
- }
- }
-
- /*
- Indexes are maintained on a per window basis so that a call to
- move([tabA, tabB], {index: 0})
- -> tabA to 0, tabB to 1 if tabA and tabB are in the same window
- move([tabA, tabB], {index: 0})
- -> tabA to 0, tabB to 0 if tabA and tabB are in different windows
- */
- let indexMap = new Map();
-
- let tabs = tabIds.map(tabId => tabTracker.getTab(tabId));
- for (let tab of tabs) {
- // If the window is not specified, use the window from the tab.
- let window = destinationWindow || tab.ownerGlobal;
- let gBrowser = window.gBrowser;
-
- let insertionPoint = indexMap.get(window) || index;
- // If the index is -1 it should go to the end of the tabs.
- if (insertionPoint == -1) {
- insertionPoint = gBrowser.tabs.length;
- }
-
- // We can only move pinned tabs to a point within, or just after,
- // the current set of pinned tabs. Unpinned tabs, likewise, can only
- // be moved to a position after the current set of pinned tabs.
- // Attempts to move a tab to an illegal position are ignored.
- let numPinned = gBrowser._numPinnedTabs;
- let ok = tab.pinned ? insertionPoint <= numPinned : insertionPoint >= numPinned;
- if (!ok) {
- continue;
- }
-
- indexMap.set(window, insertionPoint + 1);
-
- if (tab.ownerGlobal != window) {
- // If the window we are moving the tab in is different, then move the tab
- // to the new window.
- tab = gBrowser.adoptTab(tab, insertionPoint, false);
- } else {
- // If the window we are moving is the same, just move the tab.
- gBrowser.moveTabTo(tab, insertionPoint);
- }
- tabsMoved.push(tab);
- }
-
- return tabsMoved.map(tab => tabManager.convert(tab));
- },
-
- duplicate(tabId) {
- let tab = tabTracker.getTab(tabId);
-
- let gBrowser = tab.ownerGlobal.gBrowser;
- let newTab = gBrowser.duplicateTab(tab);
-
- return new Promise(resolve => {
- // We need to use SSTabRestoring because any attributes set before
- // are ignored. SSTabRestored is too late and results in a jump in
- // the UI. See http://bit.ly/session-store-api for more information.
- newTab.addEventListener("SSTabRestoring", function listener() {
- // As the tab is restoring, move it to the correct position.
-
- // Pinned tabs that are duplicated are inserted
- // after the existing pinned tab and pinned.
- if (tab.pinned) {
- gBrowser.pinTab(newTab);
- }
- gBrowser.moveTabTo(newTab, tab._tPos + 1);
- }, {once: true});
-
- newTab.addEventListener("SSTabRestored", function listener() {
- // Once it has been restored, select it and return the promise.
- gBrowser.selectedTab = newTab;
-
- return resolve(tabManager.convert(newTab));
- }, {once: true});
- });
- },
-
- getZoom(tabId) {
- let tab = getTabOrActive(tabId);
-
- let {ZoomManager} = tab.ownerGlobal;
- let zoom = ZoomManager.getZoomForBrowser(tab.linkedBrowser);
-
- return Promise.resolve(zoom);
- },
-
- setZoom(tabId, zoom) {
- let tab = getTabOrActive(tabId);
-
- let {FullZoom, ZoomManager} = tab.ownerGlobal;
-
- if (zoom === 0) {
- // A value of zero means use the default zoom factor.
- return FullZoom.reset(tab.linkedBrowser);
- } else if (zoom >= ZoomManager.MIN && zoom <= ZoomManager.MAX) {
- FullZoom.setZoom(zoom, tab.linkedBrowser);
- } else {
- return Promise.reject({
- message: `Zoom value ${zoom} out of range (must be between ${ZoomManager.MIN} and ${ZoomManager.MAX})`,
- });
- }
-
- return Promise.resolve();
- },
-
- _getZoomSettings(tabId) {
- let tab = getTabOrActive(tabId);
-
- let {FullZoom} = tab.ownerGlobal;
-
- return {
- mode: "automatic",
- scope: FullZoom.siteSpecific ? "per-origin" : "per-tab",
- defaultZoomFactor: 1,
- };
- },
-
- getZoomSettings(tabId) {
- return Promise.resolve(this._getZoomSettings(tabId));
- },
-
- setZoomSettings(tabId, settings) {
- let tab = getTabOrActive(tabId);
-
- let currentSettings = this._getZoomSettings(tab.id);
-
- if (!Object.keys(settings).every(key => settings[key] === currentSettings[key])) {
- return Promise.reject(`Unsupported zoom settings: ${JSON.stringify(settings)}`);
- }
- return Promise.resolve();
- },
-
- onZoomChange: new EventManager(context, "tabs.onZoomChange", fire => {
- let getZoomLevel = browser => {
- let {ZoomManager} = browser.ownerGlobal;
-
- return ZoomManager.getZoomForBrowser(browser);
- };
-
- // Stores the last known zoom level for each tab's browser.
- // WeakMap[<browser> -> number]
- let zoomLevels = new WeakMap();
-
- // Store the zoom level for all existing tabs.
- for (let window of windowTracker.browserWindows()) {
- for (let tab of window.gBrowser.tabs) {
- let browser = tab.linkedBrowser;
- zoomLevels.set(browser, getZoomLevel(browser));
- }
- }
-
- let tabCreated = (eventName, event) => {
- let browser = event.tab.linkedBrowser;
- zoomLevels.set(browser, getZoomLevel(browser));
- };
-
-
- let zoomListener = event => {
- let browser = event.originalTarget;
-
- // For non-remote browsers, this event is dispatched on the document
- // rather than on the <browser>.
- if (browser instanceof Ci.nsIDOMDocument) {
- browser = browser.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDocShell)
- .chromeEventHandler;
- }
-
- let {gBrowser} = browser.ownerGlobal;
- let tab = gBrowser.getTabForBrowser(browser);
- if (!tab) {
- // We only care about zoom events in the top-level browser of a tab.
- return;
- }
-
- let oldZoomFactor = zoomLevels.get(browser);
- let newZoomFactor = getZoomLevel(browser);
-
- if (oldZoomFactor != newZoomFactor) {
- zoomLevels.set(browser, newZoomFactor);
-
- let tabId = tabTracker.getId(tab);
- fire({
- tabId,
- oldZoomFactor,
- newZoomFactor,
- zoomSettings: self.tabs._getZoomSettings(tabId),
- });
- }
- };
-
- tabTracker.on("tab-attached", tabCreated);
- tabTracker.on("tab-created", tabCreated);
-
- windowTracker.addListener("FullZoomChange", zoomListener);
- windowTracker.addListener("TextZoomChange", zoomListener);
- return () => {
- tabTracker.off("tab-attached", tabCreated);
- tabTracker.off("tab-created", tabCreated);
-
- windowTracker.removeListener("FullZoomChange", zoomListener);
- windowTracker.removeListener("TextZoomChange", zoomListener);
- };
- }).api(),
},
};
return self;
});
copy from browser/components/extensions/ext-utils.js
copy to mobile/android/components/extensions/ext-utils.js
--- a/browser/components/extensions/ext-utils.js
+++ b/mobile/android/components/extensions/ext-utils.js
@@ -2,499 +2,326 @@
/* vim: set sts=2 sw=2 et tw=80: */
"use strict";
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
/* globals TabBase, WindowBase, TabTrackerBase, WindowTrackerBase, TabManagerBase, WindowManagerBase */
Cu.import("resource://gre/modules/ExtensionTabs.jsm");
-
-XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
- "@mozilla.org/content/style-sheet-service;1",
- "nsIStyleSheetService");
+/* globals EventDispatcher */
+Cu.import("resource://gre/modules/Messaging.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
EventManager,
ExtensionError,
+ DefaultWeakMap,
defineLazyGetter,
} = ExtensionUtils;
+global.GlobalEventDispatcher = EventDispatcher.instance;
+
+const BrowserStatusFilter = Components.Constructor(
+ "@mozilla.org/appshell/component/browser-status-filter;1", "nsIWebProgress",
+ "addProgressListener");
+
let tabTracker;
let windowTracker;
-// This file provides some useful code for the |tabs| and |windows|
-// modules. All of the code is installed on |global|, which is a scope
-// shared among the different ext-*.js scripts.
+class BrowserProgressListener {
+ constructor(browser, listener, flags) {
+ this.listener = listener;
+ this.browser = browser;
+ this.filter = new BrowserStatusFilter(this, flags);
+ }
-global.makeWidgetId = id => {
- id = id.toLowerCase();
- // FIXME: This allows for collisions.
- return id.replace(/[^a-z0-9_-]/g, "_");
-};
+ destroy() {
+ this.filter.removeProgressListener(this);
+ }
+
+ delegate(method, ...args) {
+ if (this.listener[method]) {
+ this.listener[method](this.browser, ...args);
+ }
+ }
-// Manages tab-specific context data, and dispatching tab select events
-// across all windows.
-global.TabContext = function TabContext(getDefaults, extension) {
- this.extension = extension;
- this.getDefaults = getDefaults;
+ onLocationChange(webProgress, request, locationURI, flags) {
+ this.delegate("onLocationChange", webProgress, request, locationURI, flags);
+ }
+ onStateChange(webProgress, request, stateFlags, status) {
+ this.delegate("onStateChange", webProgress, request, stateFlags, status);
+ }
- this.tabData = new WeakMap();
- this.lastLocation = new WeakMap();
+ onProgressChange(webProgress, request, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress) {}
+ onStatusChange(webProgress, request, status, message) {}
+ onSecurityChange(webProgress, request, state) {}
+}
- windowTracker.addListener("progress", this);
- windowTracker.addListener("TabSelect", this);
+class ProgressListenerWrapper {
+ constructor(window, listener) {
+ this.window = window;
+ this.listener = listener;
+ this.listeners = new WeakMap();
- EventEmitter.decorate(this);
-};
+ this.flags = Ci.nsIWebProgress.NOTIFY_STATE_ALL |
+ Ci.nsIWebProgress.NOTIFY_LOCATION;
-TabContext.prototype = {
- get(tab) {
- if (!this.tabData.has(tab)) {
- this.tabData.set(tab, this.getDefaults(tab));
+ for (let tab of this.window.BrowserApp.tabs) {
+ this.addBrowserProgressListener(tab.browser);
}
- return this.tabData.get(tab);
- },
+ this.window.BrowserApp.deck.addEventListener("TabOpen", this);
+ }
+
+ destroy() {
+ this.window.BrowserApp.deck.removeEventListener("TabOpen", this);
+
+ for (let tab of this.window.BrowserApp.tabs) {
+ this.removeProgressListener(tab.browser);
+ }
+ }
+
+ addBrowserProgressListener(browser) {
+ this.removeProgressListener(browser);
- clear(tab) {
- this.tabData.delete(tab);
- },
+ let listener = new BrowserProgressListener(browser, this.listener, this.flags);
+ this.listeners.set(browser, listener);
+
+ browser.addProgressListener(listener.filter, this.flags);
+ }
+
+ removeProgressListener(browser) {
+ let listener = this.listeners.get(browser);
+ if (listener) {
+ browser.removeProgressListener(listener.filter);
+ listener.destroy();
+ this.listeners.delete(browser);
+ }
+ }
handleEvent(event) {
- if (event.type == "TabSelect") {
- let tab = event.target;
- this.emit("tab-select", tab);
- this.emit("location-change", tab);
+ if (event.type === "TabOpen") {
+ this.addBrowserProgressListener(event.originalTarget);
}
- },
-
- onStateChange(browser, webProgress, request, stateFlags, statusCode) {
- let flags = Ci.nsIWebProgressListener;
-
- if (!(~stateFlags & (flags.STATE_IS_WINDOW | flags.STATE_START) ||
- this.lastLocation.has(browser))) {
- this.lastLocation.set(browser, request.URI);
- }
- },
+ }
- onLocationChange(browser, webProgress, request, locationURI, flags) {
- let gBrowser = browser.ownerGlobal.gBrowser;
- let lastLocation = this.lastLocation.get(browser);
- if (browser === gBrowser.selectedBrowser &&
- !(lastLocation && lastLocation.equalsExceptRef(browser.currentURI))) {
- let tab = gBrowser.getTabForBrowser(browser);
- this.emit("location-change", tab, true);
- }
- this.lastLocation.set(browser, browser.currentURI);
- },
-
- shutdown() {
- windowTracker.removeListener("progress", this);
- windowTracker.removeListener("TabSelect", this);
- },
-};
+}
class WindowTracker extends WindowTrackerBase {
+ constructor(...args) {
+ super(...args);
+
+ this.progressListeners = new DefaultWeakMap(() => new WeakMap());
+ }
+
addProgressListener(window, listener) {
- window.gBrowser.addTabsProgressListener(listener);
+ let listeners = this.progressListeners.get(window);
+ if (!listeners.has(listener)) {
+ let wrapper = new ProgressListenerWrapper(window, listener);
+ listeners.set(listener, wrapper);
+ }
}
removeProgressListener(window, listener) {
- window.gBrowser.removeTabsProgressListener(listener);
+ let listeners = this.progressListeners.get(window);
+ let wrapper = listeners.get(listener);
+ if (wrapper) {
+ wrapper.destroy();
+ listeners.delete(listener);
+ }
}
}
+global.GlobalEventManager = class extends EventManager {
+ constructor(context, name, event, listener) {
+ super(context, name, fire => {
+ let listener2 = {
+ onEvent(event, data, callback) {
+ listener(fire, data);
+ },
+ };
+
+ GlobalEventDispatcher.registerListener(listener2, [event]);
+ return () => {
+ GlobalEventDispatcher.unregisterListener(listener2, [event]);
+ };
+ });
+ }
+};
global.WindowEventManager = class extends EventManager {
constructor(context, name, event, listener) {
super(context, name, fire => {
let listener2 = listener.bind(null, fire);
windowTracker.addListener(event, listener2);
return () => {
windowTracker.removeListener(event, listener2);
};
});
}
};
class TabTracker extends TabTrackerBase {
- constructor() {
- super();
-
- this._tabs = new WeakMap();
- this._tabIds = new Map();
- this._nextId = 1;
-
- this.handleTabDestroyed = this.handleTabDestroyed.bind(this);
- }
-
init() {
if (this.initialized) {
return;
}
this.initialized = true;
- this.adoptedTabs = new WeakMap();
-
- this.handleWindowOpen = this.handleWindowOpen.bind(this);
- this.handleWindowClose = this.handleWindowClose.bind(this);
-
windowTracker.addListener("TabClose", this);
windowTracker.addListener("TabOpen", this);
- windowTracker.addOpenListener(this.handleWindowOpen);
- windowTracker.addCloseListener(this.handleWindowClose);
- /* eslint-disable mozilla/balanced-listeners */
- this.on("tab-detached", this.handleTabDestroyed);
- this.on("tab-removed", this.handleTabDestroyed);
- /* eslint-enable mozilla/balanced-listeners */
+ this.initialized = true;
}
getId(tab) {
- if (this._tabs.has(tab)) {
- return this._tabs.get(tab);
- }
-
- this.init();
-
- let id = this._nextId++;
- this.setId(tab, id);
- return id;
- }
-
- setId(tab, id) {
- this._tabs.set(tab, id);
- this._tabIds.set(id, tab);
+ return tab.id;
}
- handleTabDestroyed(event, {tab}) {
- let id = this._tabs.get(tab);
- if (id) {
- this._tabs.delete(tab);
- if (this._tabIds.get(id) === tab) {
- this._tabIds.delete(tab);
- }
- }
- }
-
- /**
- * Returns the XUL <tab> element associated with the given tab ID. If no tab
- * with the given ID exists, and no default value is provided, an error is
- * raised, belonging to the scope of the given context.
- *
- * @param {integer} tabId
- * The ID of the tab to retrieve.
- * @param {*} default_
- * The value to return if no tab exists with the given ID.
- * @returns {Element<tab>}
- * A XUL <tab> element.
- */
- getTab(tabId, default_ = undefined) {
- let tab = this._tabIds.get(tabId);
+ getTab(id, default_ = undefined) {
+ let tab = windowTracker.topWindow.BrowserApp.getTabForId(id);
if (tab) {
return tab;
}
if (default_ !== undefined) {
return default_;
}
- throw new ExtensionError(`Invalid tab ID: ${tabId}`);
+ throw new ExtensionError(`Invalid tab ID: ${id}`);
}
handleEvent(event) {
- let tab = event.target;
+ const {BrowserApp} = event.target.ownerGlobal;
+ let tab = BrowserApp.getTabForBrowser(event.target);
switch (event.type) {
case "TabOpen":
- let {adoptedTab} = event.detail;
- if (adoptedTab) {
- this.adoptedTabs.set(adoptedTab, event.target);
-
- // This tab is being created to adopt a tab from a different window.
- // Copy the ID from the old tab to the new.
- this.setId(tab, this.getId(adoptedTab));
-
- adoptedTab.linkedBrowser.messageManager.sendAsyncMessage("Extension:SetTabAndWindowId", {
- windowId: windowTracker.getId(tab.ownerGlobal),
- });
- }
-
- // We need to delay sending this event until the next tick, since the
- // tab does not have its final index when the TabOpen event is dispatched.
- Promise.resolve().then(() => {
- if (event.detail.adoptedTab) {
- this.emitAttached(event.originalTarget);
- } else {
- this.emitCreated(event.originalTarget);
- }
- });
+ this.emitCreated(tab);
break;
case "TabClose":
- let {adoptedBy} = event.detail;
- if (adoptedBy) {
- // This tab is being closed because it was adopted by a new window.
- // Copy its ID to the new tab, in case it was created as the first tab
- // of a new window, and did not have an `adoptedTab` detail when it was
- // opened.
- this.setId(adoptedBy, this.getId(tab));
-
- this.emitDetached(tab, adoptedBy);
- } else {
- this.emitRemoved(tab, false);
- }
+ this.emitRemoved(tab, false);
break;
}
}
- handleWindowOpen(window) {
- if (window.arguments && window.arguments[0] instanceof window.XULElement) {
- // If the first window argument is a XUL element, it means the
- // window is about to adopt a tab from another window to replace its
- // initial tab.
- //
- // Note that this event handler depends on running before the
- // delayed startup code in browser.js, which is currently triggered
- // by the first MozAfterPaint event. That code handles finally
- // adopting the tab, and clears it from the arguments list in the
- // process, so if we run later than it, we're too late.
- let tab = window.arguments[0];
- let adoptedBy = window.gBrowser.tabs[0];
-
- this.adoptedTabs.set(tab, adoptedBy);
- this.setId(adoptedBy, this.getId(tab));
-
- // We need to be sure to fire this event after the onDetached event
- // for the original tab.
- let listener = (event, details) => {
- if (details.tab === tab) {
- this.off("tab-detached", listener);
-
- Promise.resolve().then(() => {
- this.emitAttached(details.adoptedBy);
- });
- }
- };
-
- this.on("tab-detached", listener);
- } else {
- for (let tab of window.gBrowser.tabs) {
- this.emitCreated(tab);
- }
- }
- }
-
- handleWindowClose(window) {
- for (let tab of window.gBrowser.tabs) {
- if (this.adoptedTabs.has(tab)) {
- this.emitDetached(tab, this.adoptedTabs.get(tab));
- } else {
- this.emitRemoved(tab, true);
- }
- }
- }
-
- emitAttached(tab) {
- let newWindowId = windowTracker.getId(tab.ownerGlobal);
- let tabId = this.getId(tab);
-
- this.emit("tab-attached", {tab, tabId, newWindowId, newPosition: tab._tPos});
- }
-
- emitDetached(tab, adoptedBy) {
- let oldWindowId = windowTracker.getId(tab.ownerGlobal);
- let tabId = this.getId(tab);
-
- this.emit("tab-detached", {tab, adoptedBy, tabId, oldWindowId, oldPosition: tab._tPos});
- }
-
emitCreated(tab) {
this.emit("tab-created", {tab});
}
emitRemoved(tab, isWindowClosing) {
- let windowId = windowTracker.getId(tab.ownerGlobal);
+ let windowId = windowTracker.getId(tab.browser.ownerGlobal);
let tabId = this.getId(tab);
- // When addons run in-process, `window.close()` is synchronous. Most other
- // addon-invoked calls are asynchronous since they go through a proxy
- // context via the message manager. This includes event registrations such
- // as `tabs.onRemoved.addListener`.
- //
- // So, even if `window.close()` were to be called (in-process) after calling
- // `tabs.onRemoved.addListener`, then the tab would be closed before the
- // event listener is registered. To make sure that the event listener is
- // notified, we dispatch `tabs.onRemoved` asynchronously.
Services.tm.mainThread.dispatch(() => {
this.emit("tab-removed", {tab, tabId, windowId, isWindowClosing});
}, Ci.nsIThread.DISPATCH_NORMAL);
}
getBrowserData(browser) {
- if (!browser.ownerGlobal.location.href === "about:addons") {
- // When we're loaded into a <browser> inside about:addons, we need to go up
- // one more level.
- browser = browser.ownerGlobal.QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIDocShell)
- .chromeEventHandler;
- }
-
let result = {
tabId: -1,
windowId: -1,
};
- let {gBrowser} = browser.ownerGlobal;
- // Some non-browser windows have gBrowser but not
- // getTabForBrowser!
- if (gBrowser && gBrowser.getTabForBrowser) {
+ let {BrowserApp} = browser.ownerGlobal;
+ if (BrowserApp) {
result.windowId = windowTracker.getId(browser.ownerGlobal);
- let tab = gBrowser.getTabForBrowser(browser);
+ let tab = BrowserApp.getTabForBrowser(browser);
if (tab) {
result.tabId = this.getId(tab);
}
}
return result;
}
get activeTab() {
let window = windowTracker.topWindow;
- if (window && window.gBrowser) {
- return window.gBrowser.selectedTab;
+ if (window && window.BrowserApp) {
+ return window.BrowserApp.selectedTab;
}
return null;
}
}
windowTracker = new WindowTracker();
tabTracker = new TabTracker();
Object.assign(global, {tabTracker, windowTracker});
class Tab extends TabBase {
get _favIconUrl() {
- return this.window.gBrowser.getIcon(this.tab);
+ return undefined;
}
get audible() {
- return this.tab.soundPlaying;
+ return this.tab.playingAudio;
}
get browser() {
- return this.tab.linkedBrowser;
+ return this.tab.browser;
}
get cookieStoreId() {
return getCookieStoreIdForTab(this, this.tab);
}
get height() {
return this.browser.clientHeight;
}
- get index() {
- return this.tab._tPos;
+ get incognito() {
+ return PrivateBrowsingUtils.isBrowserPrivate(this.browser);
}
- get innerWindowID() {
- return this.browser.innerWindowID;
+ get index() {
+ return this.window.BrowserApp.tabs.indexOf(this.tab);
}
get mutedInfo() {
- let tab = this.tab;
-
- let mutedInfo = {muted: tab.muted};
- if (tab.muteReason === null) {
- mutedInfo.reason = "user";
- } else if (tab.muteReason) {
- mutedInfo.reason = "extension";
- mutedInfo.extensionId = tab.muteReason;
- }
-
- return mutedInfo;
+ return {muted: false};
}
get pinned() {
- return this.tab.pinned;
+ return false;
}
get active() {
- return this.tab.selected;
+ return this.tab.getActive();
}
get selected() {
- return this.tab.selected;
+ return this.tab.getActive();
}
get status() {
- if (this.tab.getAttribute("busy") === "true") {
+ if (this.browser.webProgress.isLoadingDocument) {
return "loading";
}
return "complete";
}
get width() {
return this.browser.clientWidth;
}
get window() {
- return this.tab.ownerGlobal;
+ return this.browser.ownerGlobal;
}
get windowId() {
return windowTracker.getId(this.window);
}
-
- static convertFromSessionStoreClosedData(extension, tab, window = null) {
- let result = {
- sessionId: String(tab.closedId),
- index: tab.pos ? tab.pos : 0,
- windowId: window && windowTracker.getId(window),
- selected: false,
- highlighted: false,
- active: false,
- pinned: false,
- incognito: Boolean(tab.state && tab.state.isPrivate),
- };
-
- if (extension.tabManager.hasTabPermission(tab)) {
- let entries = tab.state ? tab.state.entries : tab.entries;
- result.url = entries[0].url;
- result.title = entries[0].title;
- if (tab.image) {
- result.favIconUrl = tab.image;
- }
- }
-
- return result;
- }
}
class Window extends WindowBase {
- updateGeometry(options) {
- let {window} = this;
-
- if (options.left !== null || options.top !== null) {
- let left = options.left !== null ? options.left : window.screenX;
- let top = options.top !== null ? options.top : window.screenY;
- window.moveTo(left, top);
- }
-
- if (options.width !== null || options.height !== null) {
- let width = options.width !== null ? options.width : window.outerWidth;
- let height = options.height !== null ? options.height : window.outerHeight;
- window.resizeTo(width, height);
- }
- }
-
get focused() {
return this.window.document.hasFocus();
}
get top() {
return this.window.screenY;
}
@@ -510,107 +337,34 @@ class Window extends WindowBase {
return this.window.outerHeight;
}
get incognito() {
return PrivateBrowsingUtils.isWindowPrivate(this.window);
}
get alwaysOnTop() {
- return this.xulWindow.zLevel >= Ci.nsIXULWindow.raisedZ;
+ return false;
}
get isLastFocused() {
return this.window === windowTracker.topWindow;
}
- static getState(window) {
- const STATES = {
- [window.STATE_MAXIMIZED]: "maximized",
- [window.STATE_MINIMIZED]: "minimized",
- [window.STATE_NORMAL]: "normal",
- };
- let state = STATES[window.windowState];
- if (window.fullScreen) {
- state = "fullscreen";
- }
- return state;
- }
-
get state() {
- return Window.getState(this.window);
- }
-
- set state(state) {
- let {window} = this;
- if (state !== "fullscreen" && window.fullScreen) {
- window.fullScreen = false;
- }
-
- switch (state) {
- case "maximized":
- window.maximize();
- break;
-
- case "minimized":
- case "docked":
- window.minimize();
- break;
-
- case "normal":
- // Restore sometimes returns the window to its previous state, rather
- // than to the "normal" state, so it may need to be called anywhere from
- // zero to two times.
- window.restore();
- if (window.windowState !== window.STATE_NORMAL) {
- window.restore();
- }
- if (window.windowState !== window.STATE_NORMAL) {
- // And on OS-X, where normal vs. maximized is basically a heuristic,
- // we need to cheat.
- window.sizeToContent();
- }
- break;
-
- case "fullscreen":
- window.fullScreen = true;
- break;
-
- default:
- throw new Error(`Unexpected window state: ${state}`);
- }
+ return "fullscreen";
}
* getTabs() {
let {tabManager} = this.extension;
- for (let tab of this.window.gBrowser.tabs) {
+ for (let tab of this.window.BrowserApp.tabs) {
yield tabManager.getWrapper(tab);
}
}
-
- static convertFromSessionStoreClosedData(extension, window) {
- let result = {
- sessionId: String(window.closedId),
- focused: false,
- incognito: false,
- type: "normal", // this is always "normal" for a closed window
- // Surely this does not actually work?
- state: this.getState(window),
- alwaysOnTop: false,
- };
-
- if (window.tabs.length) {
- result.tabs = window.tabs.map(tab => {
- return Tab.convertFromSessionStoreClosedData(extension, tab);
- });
- }
-
- return result;
- }
}
Object.assign(global, {Tab, Window});
class TabManager extends TabManagerBase {
get(tabId, default_ = undefined) {
let tab = tabTracker.getTab(tabId, default_);
@@ -624,17 +378,17 @@ class TabManager extends TabManagerBase
return super.addActiveTabPermission(tab);
}
revokeActiveTabPermission(tab = tabTracker.activeTab) {
return super.revokeActiveTabPermission(tab);
}
wrapTab(tab) {
- return new Tab(this.extension, tab, tabTracker.getId(tab));
+ return new Tab(this.extension, tab, tab.id);
}
}
class WindowManager extends WindowManagerBase {
get(windowId, context) {
let window = windowTracker.getWindow(windowId, context);
return this.getWrapper(window);
--- a/mobile/android/components/extensions/extensions-mobile.manifest
+++ b/mobile/android/components/extensions/extensions-mobile.manifest
@@ -1,5 +1,9 @@
# scripts
category webextension-scripts pageAction chrome://browser/content/ext-pageAction.js
+category webextension-scripts tabs chrome://browser/content/ext-tabs.js
+category webextension-scripts utils chrome://browser/content/ext-utils.js
+category webextension-scripts-addon tabs chrome://browser/content/ext-c-tabs.js
# schemas
-category webextension-schemas page_action chrome://browser/content/schemas/page_action.json
\ No newline at end of file
+category webextension-schemas page_action chrome://browser/content/schemas/page_action.json
+category webextension-schemas tabs chrome://browser/content/schemas/tabs.json
--- a/mobile/android/components/extensions/jar.mn
+++ b/mobile/android/components/extensions/jar.mn
@@ -1,6 +1,9 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
chrome.jar:
- content/ext-pageAction.js
\ No newline at end of file
+ content/ext-c-tabs.js
+ content/ext-pageAction.js
+ content/ext-tabs.js
+ content/ext-utils.js
--- a/mobile/android/components/extensions/schemas/jar.mn
+++ b/mobile/android/components/extensions/schemas/jar.mn
@@ -1,6 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
chrome.jar:
- content/schemas/page_action.json
\ No newline at end of file
+ content/schemas/page_action.json
+ content/schemas/tabs.json
copy from browser/components/extensions/schemas/tabs.json
copy to mobile/android/components/extensions/schemas/tabs.json
--- a/browser/components/extensions/schemas/tabs.json
+++ b/mobile/android/components/extensions/schemas/tabs.json
@@ -411,16 +411,17 @@
"description": "Details about the created tab. Will contain the ID of the new tab."
}
]
}
]
},
{
"name": "duplicate",
+ "unsupported": true,
"type": "function",
"description": "Duplicates a tab.",
"async": "callback",
"parameters": [
{
"type": "integer",
"name": "tabId",
"minimum": 0,
@@ -619,21 +620,23 @@
"unsupported": true,
"deprecated": "Please use <em>highlighted</em>.",
"type": "boolean",
"optional": true,
"description": "Whether the tab should be selected."
},
"pinned": {
"type": "boolean",
+ "unsupported": true,
"optional": true,
"description": "Whether the tab should be pinned."
},
"muted": {
"type": "boolean",
+ "unsupported": true,
"optional": true,
"description": "Whether the tab should be muted."
},
"openerTabId": {
"unsupported": true,
"type": "integer",
"minimum": 0,
"optional": true,
@@ -653,16 +656,17 @@
"description": "Details about the updated tab. The $(ref:tabs.Tab) object doesn't contain <code>url</code>, <code>title</code> and <code>favIconUrl</code> if the <code>\"tabs\"</code> permission has not been requested."
}
]
}
]
},
{
"name": "move",
+ "unsupported": true,
"type": "function",
"description": "Moves one or more tabs to a new position within its window, or to a new window. Note that tabs can only be moved to and from normal (window.type === \"normal\") windows.",
"async": "callback",
"parameters": [
{
"name": "tabIds",
"description": "The tab or list of tabs to move.",
"choices": [
@@ -756,16 +760,17 @@
"name": "callback",
"optional": true,
"parameters": []
}
]
},
{
"name": "detectLanguage",
+ "unsupported": true,
"type": "function",
"description": "Detects the primary language of the content in a tab.",
"async": "callback",
"parameters": [
{
"type": "integer",
"name": "tabId",
"minimum": 0,
@@ -903,16 +908,17 @@
"optional": true,
"description": "Called when all the CSS has been removed.",
"parameters": []
}
]
},
{
"name": "setZoom",
+ "unsupported": true,
"type": "function",
"description": "Zooms a specified tab.",
"async": "callback",
"parameters": [
{
"type": "integer",
"name": "tabId",
"minimum": 0,
@@ -930,16 +936,17 @@
"optional": true,
"description": "Called after the zoom factor has been changed.",
"parameters": []
}
]
},
{
"name": "getZoom",
+ "unsupported": true,
"type": "function",
"description": "Gets the current zoom factor of a specified tab.",
"async": "callback",
"parameters": [
{
"type": "integer",
"name": "tabId",
"minimum": 0,
@@ -957,16 +964,17 @@
"description": "The tab's current zoom factor."
}
]
}
]
},
{
"name": "setZoomSettings",
+ "unsupported": true,
"type": "function",
"description": "Sets the zoom settings for a specified tab, which define how zoom changes are handled. These settings are reset to defaults upon navigating the tab.",
"async": "callback",
"parameters": [
{
"type": "integer",
"name": "tabId",
"optional": true,
@@ -984,16 +992,17 @@
"optional": true,
"description": "Called after the zoom settings have been changed.",
"parameters": []
}
]
},
{
"name": "getZoomSettings",
+ "unsupported": true,
"type": "function",
"description": "Gets the current zoom settings of a specified tab.",
"async": "callback",
"parameters": [
{
"type": "integer",
"name": "tabId",
"optional": true,
@@ -1272,16 +1281,17 @@
"description": "Fired when a tab is replaced with another tab due to prerendering or instant.",
"parameters": [
{"type": "integer", "name": "addedTabId", "minimum": 0},
{"type": "integer", "name": "removedTabId", "minimum": 0}
]
},
{
"name": "onZoomChange",
+ "unsupported": true,
"type": "function",
"description": "Fired when a tab is zoomed.",
"parameters": [{
"type": "object",
"name": "ZoomChangeInfo",
"properties": {
"tabId": {"type": "integer", "minimum": 0},
"oldZoomFactor": {"type": "number"},
--- a/toolkit/components/extensions/ExtensionParent.jsm
+++ b/toolkit/components/extensions/ExtensionParent.jsm
@@ -205,17 +205,17 @@ ProxyMessenger = {
*/
getMessageManagerForRecipient(recipient) {
let {tabId} = recipient;
// tabs.sendMessage / tabs.connect
if (tabId) {
// `tabId` being set implies that the tabs API is supported, so we don't
// need to check whether `tabTracker` exists.
let tab = apiManager.global.tabTracker.getTab(tabId, null);
- return tab && tab.linkedBrowser.messageManager;
+ return tab && (tab.linkedBrowser || tab.browser).messageManager;
}
// runtime.sendMessage / runtime.connect
let extension = GlobalManager.extensionMap.get(recipient.extensionId);
if (extension) {
return extension.parentMessageManager;
}
--- a/toolkit/components/extensions/test/mochitest/chrome.ini
+++ b/toolkit/components/extensions/test/mochitest/chrome.ini
@@ -12,28 +12,25 @@ support-files =
tags = webextensions
[test_chrome_ext_background_debug_global.html]
skip-if = (os == 'android') # android doesn't have devtools
[test_chrome_ext_background_page.html]
skip-if = (toolkit == 'android') # android doesn't have devtools
[test_chrome_ext_eventpage_warning.html]
[test_chrome_ext_contentscript_unrecognizedprop_warning.html]
-skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android.
[test_chrome_ext_hybrid_addons.html]
[test_chrome_ext_trustworthy_origin.html]
[test_chrome_ext_webnavigation_resolved_urls.html]
-skip-if = (os == 'android') # browser.tabs is undefined. Bug 1258975 on android.
[test_chrome_native_messaging_paths.html]
skip-if = os != "mac" && os != "linux"
[test_ext_cookies_expiry.html]
[test_ext_cookies_permissions_bad.html]
[test_ext_cookies_permissions_good.html]
[test_ext_cookies_containers.html]
[test_ext_jsversion.html]
[test_ext_schema.html]
[test_chrome_ext_storage_cleanup.html]
[test_chrome_ext_idle.html]
[test_chrome_ext_identity.html]
skip-if = os == 'android' # unsupported.
[test_chrome_ext_downloads_saveAs.html]
[test_chrome_ext_webrequest_background_events.html]
-skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
--- a/toolkit/components/extensions/test/mochitest/head_webrequest.js
+++ b/toolkit/components/extensions/test/mochitest/head_webrequest.js
@@ -7,19 +7,22 @@ let commonEvents = {
"onBeforeRedirect": [{urls: ["<all_urls>"]}],
"onHeadersReceived": [{urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]],
"onResponseStarted": [{urls: ["<all_urls>"]}],
"onCompleted": [{urls: ["<all_urls>"]}, ["responseHeaders"]],
"onErrorOccurred": [{urls: ["<all_urls>"]}],
};
function background(events) {
+ const IP_PATTERN = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
+
let expect;
let ignore;
let defaultOrigin;
+ let expectedIp = null;
browser.test.onMessage.addListener((msg, expected) => {
if (msg !== "set-expected") {
return;
}
expect = expected.expect;
defaultOrigin = expected.origin;
ignore = expected.ignore;
@@ -220,17 +223,22 @@ function background(events) {
if (name == "onCompleted") {
// If we have already completed a GET request for this url,
// and it was found, we expect for the response to come fromCache.
// expected.cached may be undefined, force boolean.
let expectCached = !!expected.cached && details.method === "GET" && details.statusCode != 404;
browser.test.assertEq(expectCached, details.fromCache, "fromCache is correct");
// We can only tell IPs for non-cached HTTP requests.
if (!details.fromCache && /^https?:/.test(details.url)) {
- browser.test.assertEq("127.0.0.1", details.ip, `correct ip for ${details.url}`);
+ browser.test.assertTrue(IP_PATTERN.test(details.ip), `IP for ${details.url} looks IP-ish: ${details.ip}`);
+
+ // We can't easily predict the IP ahead of time, so just make
+ // sure they're all consistent.
+ expectedIp = expectedIp || details.ip;
+ browser.test.assertEq(expectedIp, details.ip, `correct ip for ${details.url}`);
}
if (expected.headers && expected.headers.response) {
checkHeaders("response", expected, details);
}
}
if (expected.cancel && expected.cancel == name) {
browser.test.log(`${name} cancel request`);
--- a/toolkit/components/extensions/test/mochitest/mochitest-common.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini
@@ -37,82 +37,62 @@ support-files =
file_permission_xhr.html
file_teardown_test.js
return_headers.sjs
webrequest_worker.js
[test_clipboard.html]
# skip-if = # disabled test case with_permission_allow_copy, see inline comment.
[test_ext_inIncognitoContext_window.html]
-skip-if = os == 'android' # Android does not currently support windows.
+skip-if = os == 'android' # Android does not support multiple windows.
[test_ext_geturl.html]
[test_ext_background_canvas.html]
[test_ext_content_security_policy.html]
[test_ext_contentscript.html]
[test_ext_contentscript_api_injection.html]
[test_ext_contentscript_context.html]
[test_ext_contentscript_create_iframe.html]
[test_ext_contentscript_devtools_metadata.html]
[test_ext_contentscript_exporthelpers.html]
[test_ext_contentscript_incognito.html]
-skip-if = os == 'android' # Android does not multiple windows.
+skip-if = os == 'android' # Android does not support multiple windows.
[test_ext_contentscript_css.html]
[test_ext_contentscript_about_blank.html]
[test_ext_contentscript_permission.html]
-skip-if = os == 'android' # Android does not support tabs API. Bug 1260250
[test_ext_contentscript_teardown.html]
-skip-if = (os == 'android') # Android does not support tabs API. Bug 1260250
[test_ext_exclude_include_globs.html]
[test_ext_i18n_css.html]
[test_ext_generate.html]
[test_ext_notifications.html]
[test_ext_permission_xhr.html]
[test_ext_runtime_connect.html]
-skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
[test_ext_runtime_connect_twoway.html]
-skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
[test_ext_runtime_connect2.html]
-skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
[test_ext_runtime_disconnect.html]
[test_ext_runtime_id.html]
[test_ext_sandbox_var.html]
[test_ext_sendmessage_reply.html]
-skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
[test_ext_sendmessage_reply2.html]
-skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
[test_ext_sendmessage_doublereply.html]
-skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
[test_ext_sendmessage_no_receiver.html]
[test_ext_storage_content.html]
[test_ext_storage_tab.html]
-skip-if = os == 'android' # Android does not currently support tabs.
[test_ext_test.html]
[test_ext_cookies.html]
-skip-if = os == 'android' # Bug 1258975 on android.
[test_ext_background_api_injection.html]
[test_ext_background_generated_url.html]
[test_ext_background_teardown.html]
[test_ext_tab_teardown.html]
-skip-if = (os == 'android') # Android does not support tabs API. Bug 1260250
+skip-if = os == 'android' # Bug 1258975 on android.
[test_ext_unload_frame.html]
[test_ext_i18n.html]
-skip-if = (os == 'android') # Bug 1258975 on android.
[test_ext_listener_proxies.html]
[test_ext_web_accessible_resources.html]
-skip-if = (os == 'android') # Bug 1258975 on android.
[test_ext_webrequest_background_events.html]
-skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
[test_ext_webrequest_basic.html]
-skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
[test_ext_webrequest_filter.html]
-skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
[test_ext_webrequest_suspend.html]
-skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
[test_ext_webrequest_upload.html]
-skip-if = os == 'android' # webrequest api unsupported (bug 1258975).
[test_ext_webnavigation.html]
-skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
[test_ext_webnavigation_filters.html]
-skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
[test_ext_window_postMessage.html]
[test_ext_subframes_privileges.html]
-skip-if = os == 'android' # port.sender.tab is undefined on Android (bug 1258975).
[test_ext_xhr_capabilities.html]
--- a/toolkit/components/extensions/test/mochitest/test_ext_i18n.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_i18n.html
@@ -173,17 +173,17 @@ add_task(function* test_get_accept_langu
expected[index],
lang,
`got expected language in ${source}`);
});
}
let tabId;
- browser.tabs.query({currentWindow: true, active: true}, tabs => {
+ browser.tabs.query({currentWindow: true, active: true}).then(tabs => {
tabId = tabs[0].id;
browser.test.sendMessage("ready");
});
browser.test.onMessage.addListener(async ([msg, expected]) => {
let contentResults = await browser.tabs.sendMessage(tabId, "get-results");
let backgroundResults = await browser.i18n.getAcceptLanguages();
@@ -313,16 +313,21 @@ add_task(function* test_get_ui_language(
win.close();
yield extension.unload();
});
add_task(function* test_detect_language() {
+ if (AppConstants.MOZ_BUILD_APP !== "browser") {
+ // This is not supported on Android.
+ return;
+ }
+
const af_string = " aam skukuza die naam beteken hy wat skoonvee of hy wat alles onderstebo keer wysig " +
"bosveldkampe boskampe is kleiner afgeleë ruskampe wat oor min fasiliteite beskik daar is geen restaurante " +
"of winkels nie en slegs oornagbesoekers word toegelaat bateleur";
// String with intermixed French/English text
const fr_en_string = "France is the largest country in Western Europe and the third-largest in Europe as a whole. " +
"A accès aux chiens et aux frontaux qui lui ont été il peut consulter et modifier ses collections et exporter " +
"Cet article concerne le pays européen aujourd’hui appelé République française. Pour d’autres usages du nom France, " +
"Pour une aide rapide et effective, veuiller trouver votre aide dans le menu ci-dessus." +
--- a/toolkit/components/extensions/test/mochitest/test_ext_unload_frame.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_unload_frame.html
@@ -140,16 +140,21 @@ add_task(function* testSendMessage_and_r
window.open("file_sample.html");
yield extension.awaitFinish("Received sendMessage from closing frame");
yield extension.unload();
});
add_task(function* testConnect_and_remove_window() {
+ if (AppConstants.MOZ_BUILD_APP !== "browser") {
+ // We can't rely on this timing on Android.
+ return;
+ }
+
let extension = createTestExtension("window", connect_background, connect_contentScript);
yield extension.startup();
window.open("file_sample.html");
yield extension.awaitFinish("Received onDisconnect from closing frame");
yield extension.unload();
});
--- a/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webnavigation.html
@@ -327,18 +327,23 @@ add_task(function* webnav_transitions_pr
});
found = received.find((data) => (data.event == "onCommitted" &&
data.url == FRAME_MANUAL_PAGE2));
ok(found, "Got the onCommitted event");
if (found) {
- is(found.details.transitionType, "manual_subframe",
- "Got the expected 'manual_subframe' transitionType in the OnCommitted event");
+ if (AppConstants.MOZ_BUILD_APP === "browser") {
+ is(found.details.transitionType, "manual_subframe",
+ "Got the expected 'manual_subframe' transitionType in the OnCommitted event");
+ } else {
+ is(found.details.transitionType, "auto_subframe",
+ "Got the expected 'manual_subframe' transitionType in the OnCommitted event");
+ }
}
// cleanup phase
win.close();
yield extension.unload();
info("webnavigation extension unloaded");
});
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest_filter.html
@@ -21,29 +21,37 @@ add_task(function* setup() {
// Fetch the windowId and tabId we need to filter with WebRequest.
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: [
"tabs",
],
},
background() {
- browser.windows.getCurrent({populate: true}).then(window => {
- browser.test.log(`current window ${window.id} tabs: ${JSON.stringify(window.tabs.map(tab => [tab.id, tab.url]))}`);
- browser.test.sendMessage("windowData", {windowId: window.id, tabId: window.tabs[0].id});
+ browser.tabs.query({currentWindow: true}).then(tabs => {
+ let tab = tabs.find(tab => tab.active);
+ let {windowId} = tab;
+
+ browser.test.log(`current window ${windowId} tabs: ${JSON.stringify(tabs.map(tab => [tab.id, tab.url]))}`);
+ browser.test.sendMessage("windowData", {windowId, tabId: tab.id});
});
},
});
yield extension.startup();
windowData = yield extension.awaitMessage("windowData");
info(`window is ${JSON.stringify(windowData)}`);
yield extension.unload();
});
add_task(function* test_webRequest_filter_window() {
+ if (AppConstants.MOZ_BUILD_APP !== "browser") {
+ // Android does not support multiple windows.
+ return;
+ }
+
yield SpecialPowers.pushPrefEnv({
set: [["dom.serviceWorkers.testing.enabled", true]],
});
let events = {
"onBeforeRequest": [{urls: ["<all_urls>"], windowId: windowData.windowId}],
"onBeforeSendHeaders": [{urls: ["<all_urls>"], windowId: windowData.windowId}, ["requestHeaders"]],
"onSendHeaders": [{urls: ["<all_urls>"], windowId: windowData.windowId}, ["requestHeaders"]],
@@ -95,29 +103,31 @@ add_task(function* test_webRequest_filte
"onResponseStarted": [{urls: ["<all_urls>"], tabId: windowData.tabId}],
"onCompleted": [{urls: ["<all_urls>"], tabId: windowData.tabId}, ["responseHeaders"]],
"onErrorOccurred": [{urls: ["<all_urls>"], tabId: windowData.tabId}],
};
let expect = {
"file_image_good.png": {
optional_events: ["onBeforeRedirect", "onBeforeRequest", "onBeforeSendHeaders", "onSendHeaders"],
type: "main_frame",
- cached: true,
+ cached: AppConstants.MOZ_BUILD_APP === "browser",
},
};
let extension = makeExtension(events);
yield extension.startup();
extension.sendMessage("set-expected", {expect, origin: location.href});
yield extension.awaitMessage("continue");
- // We should not get events for a new window load.
- let newWindow = window.open("file_image_good.png", "_blank", "width=100,height=100");
- yield waitForLoad(newWindow);
- newWindow.close();
+ if (AppConstants.MOZ_BUILD_APP === "browser") {
+ // We should not get events for a new window load.
+ let newWindow = window.open("file_image_good.png", "_blank", "width=100,height=100");
+ yield waitForLoad(newWindow);
+ newWindow.close();
+ }
// We should not get background events.
let registration = yield navigator.serviceWorker.register("webrequest_worker.js?test1", {scope: "."});
// We should get events for the reload.
testWindow.location = "file_image_good.png";
yield extension.awaitMessage("done");