Bug 1423725 add event, query and details for hidden status, r=Gijs,rpl
authorShane Caraveo <scaraveo@mozilla.com>
Thu, 18 Jan 2018 16:37:11 -0700
changeset 454276 c7cc4bef1e90774a25df76e6336886b25bc38c9d
parent 454275 0c7a34d13dc36c736eebd6e80aee00cb2d4d697d
child 454277 ea7705cc940fd908cfec76cf535629b1fbd78ab8
push id1648
push usermtabara@mozilla.com
push dateThu, 01 Mar 2018 12:45:47 +0000
treeherdermozilla-release@cbb9688c2eeb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersGijs, rpl
bugs1423725
milestone59.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1423725 add event, query and details for hidden status, r=Gijs,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;
     }