Bug 1423725 add event, query and details for hidden status, r?rpl draft
authorShane Caraveo <scaraveo@mozilla.com>
Thu, 18 Jan 2018 16:37:11 -0700
changeset 722403 ad8f7420a88fa2dbb70ca1688ca02ce6e68118ef
parent 721830 b7a651281314d6369658eeb58e3bb181cf95016f
child 722404 1a0a2f439964ea520af160bccd998c19e25c5d99
push id96149
push usermixedpuppy@gmail.com
push dateThu, 18 Jan 2018 23:37:40 +0000
reviewersrpl
bugs1423725
milestone59.0a1
Bug 1423725 add event, query and details for hidden status, r?rpl MozReview-Commit-ID: AMcmbh4m8lK
browser/base/content/tabbrowser.xml
browser/components/extensions/ext-browser.js
browser/components/extensions/ext-tabs.js
browser/components/extensions/schemas/tabs.json
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_tabs_sharingState.js
mobile/android/components/extensions/ext-utils.js
mobile/android/components/extensions/schemas/tabs.json
toolkit/components/extensions/ext-tabs-base.js
--- 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;
     }