--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1569,16 +1569,27 @@
}
this._tabAttrModified(tab, ["sharing"]);
if (aBrowser == this.mCurrentBrowser)
gIdentityHandler.updateSharingIndicator();
]]></body>
</method>
+ <method name="getTabSharingState">
+ <parameter name="aTab"/>
+ <body><![CDATA[
+ // Normalize the state object for consumers (ie.extensions).
+ let state = Object.assign({}, aTab._sharingState);
+ // ensure bool if undefined
+ state.camera = !!state.camera;
+ state.microphone = !!state.microphone;
+ return state;
+ ]]></body>
+ </method>
<!-- TODO: remove after 57, once we know add-ons can no longer use it. -->
<method name="setTabTitleLoading">
<parameter name="aTab"/>
<body/>
</method>
<method name="setInitialTabTitle">
@@ -3878,17 +3889,17 @@
</body>
</method>
<method name="hideTab">
<parameter name="aTab"/>
<body>
<![CDATA[
if (!aTab.hidden && !aTab.pinned && !aTab.selected &&
- !aTab.closing) {
+ !aTab.closing && !aTab._sharingState) {
aTab.setAttribute("hidden", "true");
this._visibleTabs = null; // invalidate cache
this.tabContainer._updateCloseButtons();
this.tabContainer._setPositionalAttributes();
let event = document.createEvent("Events");
--- a/browser/components/extensions/ext-browser.js
+++ b/browser/components/extensions/ext-browser.js
@@ -611,16 +611,24 @@ class Tab extends TabBase {
}
get frameLoader() {
// If we don't have a frameLoader yet, just return a dummy with no width and
// height.
return super.frameLoader || {lazyWidth: 0, lazyHeight: 0};
}
+ get hidden() {
+ return this.nativeTab.hidden;
+ }
+
+ get sharingState() {
+ return this.window.gBrowser.getTabSharingState(this.nativeTab);
+ }
+
get cookieStoreId() {
return getCookieStoreIdForTab(this, this.nativeTab);
}
get openerTabId() {
let opener = this.nativeTab.openerTab;
if (opener && opener.parentNode && opener.ownerDocument == this.nativeTab.ownerDocument) {
return tabTracker.getId(opener);
@@ -712,16 +720,17 @@ class Tab extends TabBase {
static convertFromSessionStoreClosedData(extension, tabData, window = null) {
let result = {
sessionId: String(tabData.closedId),
index: tabData.pos ? tabData.pos : 0,
windowId: window && windowTracker.getId(window),
highlighted: false,
active: false,
pinned: false,
+ hidden: tabData.state ? tabData.state.hidden : tabData.hidden,
incognito: Boolean(tabData.state && tabData.state.isPrivate),
lastAccessed: tabData.state ? tabData.state.lastAccessed : tabData.lastAccessed,
};
if (extension.tabManager.hasTabPermission(tabData)) {
let entries = tabData.state ? tabData.state.entries : tabData.entries;
let lastTabIndex = tabData.state ? tabData.state.index : tabData.index;
// We need to take lastTabIndex - 1 because the index in the tab data is
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -256,25 +256,32 @@ this.tabs = class extends ExtensionAPI {
needed.push("mutedInfo");
}
if (changed.includes("soundplaying")) {
needed.push("audible");
}
if (changed.includes("label")) {
needed.push("title");
}
+ if (changed.includes("sharing")) {
+ needed.push("sharingState");
+ }
} else if (event.type == "TabPinned") {
needed.push("pinned");
} else if (event.type == "TabUnpinned") {
needed.push("pinned");
} else if (event.type == "TabBrowserInserted" &&
!event.detail.insertedOnTabCreation) {
needed.push("discarded");
} else if (event.type == "TabBrowserDiscarded") {
needed.push("discarded");
+ } else if (event.type == "TabShow") {
+ needed.push("hidden");
+ } else if (event.type == "TabHide") {
+ needed.push("hidden");
}
let tab = tabManager.getWrapper(event.originalTarget);
let changeInfo = {};
for (let prop of needed) {
changeInfo[prop] = tab[prop];
}
@@ -305,26 +312,30 @@ this.tabs = class extends ExtensionAPI {
};
windowTracker.addListener("status", statusListener);
windowTracker.addListener("TabAttrModified", listener);
windowTracker.addListener("TabPinned", listener);
windowTracker.addListener("TabUnpinned", listener);
windowTracker.addListener("TabBrowserInserted", listener);
windowTracker.addListener("TabBrowserDiscarded", listener);
+ windowTracker.addListener("TabShow", listener);
+ windowTracker.addListener("TabHide", listener);
tabTracker.on("tab-isarticle", isArticleChangeListener);
return () => {
windowTracker.removeListener("status", statusListener);
windowTracker.removeListener("TabAttrModified", listener);
windowTracker.removeListener("TabPinned", listener);
windowTracker.removeListener("TabUnpinned", listener);
windowTracker.removeListener("TabBrowserInserted", listener);
windowTracker.removeListener("TabBrowserDiscarded", listener);
+ windowTracker.removeListener("TabShow", listener);
+ windowTracker.removeListener("TabHide", listener);
tabTracker.off("tab-isarticle", isArticleChangeListener);
};
}).api(),
create(createProperties) {
return new Promise((resolve, reject) => {
let window = createProperties.windowId !== null ?
windowTracker.getWindow(createProperties.windowId, context) :
--- a/browser/components/extensions/schemas/tabs.json
+++ b/browser/components/extensions/schemas/tabs.json
@@ -48,16 +48,36 @@
"extensionId": {
"type": "string",
"optional": true,
"description": "The ID of the extension that changed the muted state. Not set if an extension was not the reason the muted state last changed."
}
}
},
{
+ "id": "SharingState",
+ "type": "object",
+ "description": "Tab sharing state for screen, microphone and camera.",
+ "properties": {
+ "screen": {
+ "type": "string",
+ "optional": true,
+ "description": "If the tab is sharing the screen the value will be one of \"Screen\", \"Window\", or \"Application\", or undefined if not screen sharing."
+ },
+ "camera": {
+ "type": "boolean",
+ "description": "True if the tab is using the camera."
+ },
+ "microphone": {
+ "type": "boolean",
+ "description": "True if the tab is using the microphone."
+ }
+ }
+ },
+ {
"id": "Tab",
"type": "object",
"properties": {
"id": {"type": "integer", "minimum": -1, "optional": true, "description": "The ID of the tab. Tab IDs are unique within a browser session. Under some circumstances a Tab may not be assigned an ID, for example when querying foreign tabs using the $(ref:sessions) API, in which case a session ID may be present. Tab ID can also be set to $(ref:tabs.TAB_ID_NONE) for apps and devtools windows."},
"index": {"type": "integer", "minimum": -1, "description": "The zero-based index of the tab within its window."},
"windowId": {"type": "integer", "optional": true, "minimum": 0, "description": "The ID of the window the tab is contained within."},
"openerTabId": {"type": "integer", "minimum": 0, "optional": true, "description": "The ID of the tab that opened this tab, if any. This property is only present if the opener tab still exists."},
"selected": {"type": "boolean", "description": "Whether the tab is selected.", "deprecated": "Please use $(ref:tabs.Tab.highlighted).", "unsupported": true},
@@ -70,20 +90,22 @@
"url": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL the tab is displaying. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
"title": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The title of the tab. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
"favIconUrl": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL of the tab's favicon. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission. It may also be an empty string if the tab is loading."},
"status": {"type": "string", "optional": true, "description": "Either <em>loading</em> or <em>complete</em>."},
"discarded": {"type": "boolean", "optional": true, "description": "True while the tab is not loaded with content."},
"incognito": {"type": "boolean", "description": "Whether the tab is in an incognito window."},
"width": {"type": "integer", "optional": true, "description": "The width of the tab in pixels."},
"height": {"type": "integer", "optional": true, "description": "The height of the tab in pixels."},
+ "hidden": {"type": "boolean", "optional": true, "description": "True if the tab is hidden."},
"sessionId": {"type": "string", "optional": true, "description": "The session ID used to uniquely identify a Tab obtained from the $(ref:sessions) API."},
"cookieStoreId": {"type": "string", "optional": true, "description": "The CookieStoreId used for the tab."},
"isArticle": {"type": "boolean", "optional": true, "description": "Whether the document in the tab can be rendered in reader mode."},
- "isInReaderMode": {"type": "boolean", "optional": true, "description": "Whether the document in the tab is being rendered in reader mode."}
+ "isInReaderMode": {"type": "boolean", "optional": true, "description": "Whether the document in the tab is being rendered in reader mode."},
+ "sharingState": {"$ref": "SharingState", "optional": true, "description": "Current tab sharing state for screen, microphone and camera."}
}
},
{
"id": "ZoomSettingsMode",
"type": "string",
"description": "Defines how zoom changes are handled, i.e. which entity is responsible for the actual scaling of the page; defaults to <code>automatic</code>.",
"enum": [
{
@@ -595,16 +617,21 @@
"optional": true,
"description": "Whether the tabs have completed loading."
},
"discarded": {
"type": "boolean",
"optional": true,
"description": "True while the tabs are not loaded with content."
},
+ "hidden": {
+ "type": "boolean",
+ "optional": true,
+ "description": "True while the tabs are hidden."
+ },
"title": {
"type": "string",
"optional": true,
"description": "Match page titles against a pattern."
},
"url": {
"choices": [
{"type": "string"},
@@ -635,16 +662,34 @@
"optional": true,
"description": "The CookieStoreId used for the tab."
},
"openerTabId": {
"type": "integer",
"minimum": 0,
"optional": true,
"description": "The ID of the tab that opened this tab. If specified, the opener tab must be in the same window as this tab."
+ },
+ "screen": {
+ "choices": [
+ {"type": "string", "enum": ["Screen", "Window", "Application"]},
+ {"type": "boolean"}
+ ],
+ "optional": true,
+ "description": "True for any screen sharing, or a string to specify type of screen sharing."
+ },
+ "camera": {
+ "type": "boolean",
+ "optional": true,
+ "description": "True if the tab is using the camera."
+ },
+ "microphone": {
+ "type": "boolean",
+ "optional": true,
+ "description": "True if the tab is using the microphone."
}
}
},
{
"type": "function",
"name": "callback",
"parameters": [
{
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -165,16 +165,17 @@ skip-if = !e10s
[browser_ext_tabs_onUpdated.js]
[browser_ext_tabs_opener.js]
[browser_ext_tabs_printPreview.js]
[browser_ext_tabs_query.js]
[browser_ext_tabs_readerMode.js]
[browser_ext_tabs_reload.js]
[browser_ext_tabs_reload_bypass_cache.js]
[browser_ext_tabs_sendMessage.js]
+[browser_ext_tabs_sharingState.js]
[browser_ext_tabs_cookieStoreId.js]
[browser_ext_tabs_update.js]
[browser_ext_tabs_zoom.js]
[browser_ext_tabs_update_url.js]
[browser_ext_themes_icons.js]
[browser_ext_themes_validation.js]
[browser_ext_url_overrides_newtab.js]
[browser_ext_user_events.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_sharingState.js
@@ -0,0 +1,58 @@
+"use strict";
+
+add_task(async function test_tabs_mediaIndicators() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+ // setBrowserSharing is called when a request for media icons occurs. We're
+ // just testing that extension tabs get the info and are updated when it is
+ // called.
+ gBrowser.setBrowserSharing(tab.linkedBrowser, {screen: "Window", microphone: true, camera: true});
+
+ async function background() {
+ let tabs = await browser.tabs.query({microphone: true});
+ let testTab = tabs[0];
+
+ let state = testTab.sharingState;
+ browser.test.assertTrue(state.camera, "sharing camera was turned on");
+ browser.test.assertTrue(state.microphone, "sharing mic was turned on");
+ browser.test.assertEq(state.screen, "Window", "sharing screen is window");
+
+ tabs = await browser.tabs.query({screen: true});
+ browser.test.assertEq(tabs.length, 1, "screen sharing tab was found");
+
+ tabs = await browser.tabs.query({screen: "Window"});
+ browser.test.assertEq(tabs.length, 1, "screen sharing (window) tab was found");
+
+ tabs = await browser.tabs.query({screen: "Screen"});
+ browser.test.assertEq(tabs.length, 0, "screen sharing tab was not found");
+
+ browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+ if (testTab.id !== tabId) {
+ return;
+ }
+ let state = tab.sharingState;
+ browser.test.assertFalse(state.camera, "sharing camera was turned off");
+ browser.test.assertFalse(state.microphone, "sharing mic was turned off");
+ browser.test.assertFalse(state.screen, "sharing screen was turned off");
+ browser.test.notifyPass("done");
+ });
+ browser.test.sendMessage("ready");
+ }
+
+ let extdata = {
+ manifest: {permissions: ["tabs"]},
+ useAddonManager: "temporary",
+ background,
+ };
+ let extension = ExtensionTestUtils.loadExtension(extdata);
+ await extension.startup();
+
+ // Test that onUpdated is called after the sharing state is changed from
+ // chrome code.
+ await extension.awaitMessage("ready");
+ gBrowser.setBrowserSharing(tab.linkedBrowser, {});
+
+ await extension.awaitFinish("done");
+ await extension.unload();
+
+ await BrowserTestUtils.removeTab(tab);
+});
--- a/mobile/android/components/extensions/ext-utils.js
+++ b/mobile/android/components/extensions/ext-utils.js
@@ -571,16 +571,28 @@ class Tab extends TabBase {
// https://bugzilla.mozilla.org/show_bug.cgi?id=1402924
get isArticle() {
return false;
}
get isInReaderMode() {
return false;
}
+
+ get hidden() {
+ return false;
+ }
+
+ get sharingState() {
+ return {
+ screen: undefined,
+ microphone: false,
+ camera: false,
+ };
+ }
}
// Manages tab-specific context data and dispatches tab select and close events.
class TabContext extends EventEmitter {
constructor(getDefaults, extension) {
super();
this.extension = extension;
--- a/mobile/android/components/extensions/schemas/tabs.json
+++ b/mobile/android/components/extensions/schemas/tabs.json
@@ -48,16 +48,36 @@
"extensionId": {
"type": "string",
"optional": true,
"description": "The ID of the extension that changed the muted state. Not set if an extension was not the reason the muted state last changed."
}
}
},
{
+ "id": "SharingState",
+ "type": "object",
+ "description": "Tab sharing state for screen, microphone and camera. Currently unsupported on Android.",
+ "properties": {
+ "screen": {
+ "type": "string",
+ "optional": true,
+ "description": "If the tab is sharing the screen the value will be one of \"Screen\", \"Window\", or \"Application\", or undefined if not screen sharing."
+ },
+ "camera": {
+ "type": "boolean",
+ "description": "True if the tab is using the camera."
+ },
+ "microphone": {
+ "type": "boolean",
+ "description": "True if the tab is using the microphone."
+ }
+ }
+ },
+ {
"id": "Tab",
"type": "object",
"properties": {
"id": {"type": "integer", "minimum": -1, "optional": true, "description": "The ID of the tab. Tab IDs are unique within a browser session. Under some circumstances a Tab may not be assigned an ID, for example when querying foreign tabs using the $(ref:sessions) API, in which case a session ID may be present. Tab ID can also be set to $(ref:tabs.TAB_ID_NONE) for apps and devtools windows."},
"index": {"type": "integer", "minimum": -1, "description": "The zero-based index of the tab within its window."},
"windowId": {"type": "integer", "optional": true, "minimum": 0, "description": "The ID of the window the tab is contained within."},
"openerTabId": {"unsupported": true, "type": "integer", "minimum": 0, "optional": true, "description": "The ID of the tab that opened this tab, if any. This property is only present if the opener tab still exists."},
"selected": {"type": "boolean", "description": "Whether the tab is selected.", "deprecated": "Please use $(ref:tabs.Tab.highlighted).", "unsupported": true},
@@ -70,20 +90,22 @@
"url": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL the tab is displaying. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
"title": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The title of the tab. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission."},
"favIconUrl": {"type": "string", "optional": true, "permissions": ["tabs"], "description": "The URL of the tab's favicon. This property is only present if the extension's manifest includes the <code>\"tabs\"</code> permission. It may also be an empty string if the tab is loading."},
"status": {"type": "string", "optional": true, "description": "Either <em>loading</em> or <em>complete</em>."},
"discarded": {"type": "boolean", "optional": true, "description": "True while the tab is not loaded with content."},
"incognito": {"type": "boolean", "description": "Whether the tab is in an incognito window."},
"width": {"type": "integer", "optional": true, "description": "The width of the tab in pixels."},
"height": {"type": "integer", "optional": true, "description": "The height of the tab in pixels."},
+ "hidden": {"type": "boolean", "optional": true, "description": "True if the tab is hidden."},
"sessionId": {"type": "string", "optional": true, "description": "The session ID used to uniquely identify a Tab obtained from the $(ref:sessions) API."},
"cookieStoreId": {"type": "string", "optional": true, "description": "The CookieStoreId used for the tab."},
"isArticle": {"type": "boolean", "optional": true, "description": "Whether the document in the tab can be rendered in reader mode."},
- "isInReaderMode": {"type": "boolean", "optional": true, "description": "Whether the document in the tab is being rendered in reader mode."}
+ "isInReaderMode": {"type": "boolean", "optional": true, "description": "Whether the document in the tab is being rendered in reader mode."},
+ "sharingState": {"$ref": "SharingState", "optional": true, "description": "Current tab sharing state for screen, microphone and camera."}
}
},
{
"id": "ZoomSettingsMode",
"type": "string",
"description": "Defines how zoom changes are handled, i.e. which entity is responsible for the actual scaling of the page; defaults to <code>automatic</code>.",
"enum": [
{
--- a/toolkit/components/extensions/ext-tabs-base.js
+++ b/toolkit/components/extensions/ext-tabs-base.js
@@ -313,26 +313,46 @@ class TabBase {
* Returns the ID of the tab which opened this one.
* @readonly
*/
get openerTabId() {
return null;
}
/**
+ * @property {integer} discarded
+ * Returns true if the tab is discarded.
+ * @readonly
+ * @abstract
+ */
+ get discarded() {
+ throw new Error("Not implemented");
+ }
+
+ /**
* @property {integer} height
* Returns the pixel height of the visible area of the tab.
* @readonly
* @abstract
*/
get height() {
throw new Error("Not implemented");
}
/**
+ * @property {integer} hidden
+ * Returns true if the tab is hidden.
+ * @readonly
+ * @abstract
+ */
+ get hidden() {
+ throw new Error("Not implemented");
+ }
+
+ /**
* @property {integer} index
* Returns the index of the tab in its window's tab list.
* @readonly
* @abstract
*/
get index() {
throw new Error("Not implemented");
}
@@ -343,16 +363,26 @@ class TabBase {
* @readonly
* @abstract
*/
get mutedInfo() {
throw new Error("Not implemented");
}
/**
+ * @property {SharingState} sharingState
+ * Returns object with tab sharingState.
+ * @readonly
+ * @abstract
+ */
+ get sharingState() {
+ throw new Error("Not implemented");
+ }
+
+ /**
* @property {boolean} pinned
* Returns true if the tab is pinned, false otherwise.
* @readonly
* @abstract
*/
get pinned() {
throw new Error("Not implemented");
}
@@ -448,46 +478,73 @@ class TabBase {
* @param {object} queryInfo
* The query info against which to match.
* @param {boolean} [queryInfo.active]
* Matches against the exact value of the tab's `active` attribute.
* @param {boolean} [queryInfo.audible]
* Matches against the exact value of the tab's `audible` attribute.
* @param {string} [queryInfo.cookieStoreId]
* Matches against the exact value of the tab's `cookieStoreId` attribute.
+ * @param {boolean} [queryInfo.discarded]
+ * Matches against the exact value of the tab's `discarded` attribute.
+ * @param {boolean} [queryInfo.hidden]
+ * Matches against the exact value of the tab's `hidden` attribute.
* @param {boolean} [queryInfo.highlighted]
* Matches against the exact value of the tab's `highlighted` attribute.
* @param {integer} [queryInfo.index]
* Matches against the exact value of the tab's `index` attribute.
* @param {boolean} [queryInfo.muted]
* Matches against the exact value of the tab's `mutedInfo.muted` attribute.
* @param {boolean} [queryInfo.pinned]
* Matches against the exact value of the tab's `pinned` attribute.
* @param {string} [queryInfo.status]
* Matches against the exact value of the tab's `status` attribute.
* @param {string} [queryInfo.title]
* Matches against the exact value of the tab's `title` attribute.
+ * @param {string|boolean } [queryInfo.screen]
+ * Matches against the exact value of the tab's `sharingState.screen` attribute, or use true to match any screen sharing tab.
+ * @param {boolean} [queryInfo.camera]
+ * Matches against the exact value of the tab's `sharingState.camera` attribute.
+ * @param {boolean} [queryInfo.microphone]
+ * Matches against the exact value of the tab's `sharingState.microphone` attribute.
*
* Note: Per specification, this should perform a pattern match, rather
* than an exact value match, and will do so in the future.
* @param {MatchPattern} [queryInfo.url]
* Requires the tab's URL to match the given MatchPattern object.
*
* @returns {boolean}
* True if the tab matches the query.
*/
matches(queryInfo) {
- const PROPS = ["active", "audible", "cookieStoreId", "highlighted", "index", "openerTabId", "pinned", "status"];
+ const PROPS = ["active", "audible", "cookieStoreId", "discarded", "hidden",
+ "highlighted", "index", "openerTabId", "pinned", "status"];
- if (PROPS.some(prop => queryInfo[prop] != null && queryInfo[prop] !== this[prop])) {
+ function checkProperty(prop, obj) {
+ return queryInfo[prop] != null && queryInfo[prop] !== obj[prop];
+ }
+
+ if (PROPS.some(prop => checkProperty(prop, this))) {
return false;
}
- if (queryInfo.muted !== null) {
- if (queryInfo.muted !== this.mutedInfo.muted) {
+ if (checkProperty("muted", this.mutedInfo)) {
+ return false;
+ }
+
+ let state = this.sharingState;
+ if (["camera", "microphone"].some(prop => checkProperty(prop, state))) {
+ return false;
+ }
+ // query for screen can be boolean (ie. any) or string (ie. specific).
+ if (queryInfo.screen !== null) {
+ let match = typeof queryInfo.screen == "boolean" ?
+ queryInfo.screen === !!state.screen :
+ queryInfo.screen === state.screen;
+ if (!match) {
return false;
}
}
if (queryInfo.url && !queryInfo.url.matches(this.uri)) {
return false;
}
if (queryInfo.title && !queryInfo.title.matches(this.title)) {
@@ -511,25 +568,27 @@ class TabBase {
let result = {
id: this.id,
index: this.index,
windowId: this.windowId,
highlighted: this.selected,
active: this.selected,
pinned: this.pinned,
status: this.status,
+ hidden: this.hidden,
discarded: this.discarded,
incognito: this.incognito,
width: this.width,
height: this.height,
lastAccessed: this.lastAccessed,
audible: this.audible,
mutedInfo: this.mutedInfo,
isArticle: this.isArticle,
isInReaderMode: this.isInReaderMode,
+ sharingState: this.sharingState,
};
// If the tab has not been fully layed-out yet, fallback to the geometry
// from a different tab (usually the currently active tab).
if (fallbackTab && (!result.width || !result.height)) {
result.width = fallbackTab.width;
result.height = fallbackTab.height;
}