Bug 1333468 - Part 1 - Move WebRTC sharing indicator into the identity block and add a paused state. r=florian draft
authorJohann Hofmann <jhofmann@mozilla.com>
Thu, 16 Nov 2017 19:33:19 +0100
changeset 758931 cc7575236c84de23ff7e86182c81160306f096c6
parent 758927 6661c077325c35af028f1cdaa660f673cbea39be
child 758932 fcdfbf3d646ddf96a43b5facf69f0f7f8211dfa6
push id100224
push userjhofmann@mozilla.com
push dateFri, 23 Feb 2018 10:32:36 +0000
reviewersflorian
bugs1333468
milestone60.0a1
Bug 1333468 - Part 1 - Move WebRTC sharing indicator into the identity block and add a paused state. r=florian MozReview-Commit-ID: ETi6nX2Eapc
browser/base/content/browser.js
browser/base/content/tabbrowser.xml
browser/modules/ContentWebRTC.jsm
browser/themes/shared/identity-block/identity-block.inc.css
browser/themes/shared/tabs.inc.css
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -7421,24 +7421,27 @@ var gIdentityHandler = {
     if (!this._uri) {
       return;
     }
     this.refreshIdentityBlock();
   },
 
   updateSharingIndicator() {
     let tab = gBrowser.selectedTab;
-    let sharing = tab.getAttribute("sharing");
-    if (sharing)
-      this._identityBox.setAttribute("sharing", sharing);
-    else
-      this._identityBox.removeAttribute("sharing");
-
     this._sharingState = tab._sharingState;
 
+    this._identityBox.removeAttribute("paused");
+    this._identityBox.removeAttribute("sharing");
+    if (this._sharingState && this._sharingState.sharing) {
+      this._identityBox.setAttribute("sharing", this._sharingState.sharing);
+      if (this._sharingState.paused) {
+        this._identityBox.setAttribute("paused", "true");
+      }
+    }
+
     if (this._identityPopup.state == "open") {
       this.updateSitePermissions();
       PanelView.forNode(this._identityPopupMainView)
                .descriptionHeightWorkaround();
     }
   },
 
   /**
@@ -7925,36 +7928,36 @@ var gIdentityHandler = {
     while (this._permissionList.hasChildNodes())
       this._permissionList.removeChild(this._permissionList.lastChild);
 
     let permissions =
       SitePermissions.getAllPermissionDetailsForBrowser(gBrowser.selectedBrowser);
 
     if (this._sharingState) {
       // If WebRTC device or screen permissions are in use, we need to find
-      // the associated permission item to set the inUse field to true.
+      // the associated permission item to set the sharingState field.
       for (let id of ["camera", "microphone", "screen"]) {
         if (this._sharingState[id]) {
           let found = false;
           for (let permission of permissions) {
             if (permission.id != id)
               continue;
             found = true;
-            permission.inUse = true;
+            permission.sharingState = this._sharingState[id];
             break;
           }
           if (!found) {
             // If the permission item we were looking for doesn't exist,
             // the user has temporarily allowed sharing and we need to add
             // an item in the permissions array to reflect this.
             permissions.push({
               id,
               state: SitePermissions.ALLOW,
               scope: SitePermissions.SCOPE_REQUEST,
-              inUse: true,
+              sharingState: this._sharingState[id],
             });
           }
         }
       }
     }
 
     let hasBlockedPopupIndicator = false;
     for (let permission of permissions) {
@@ -7995,18 +7998,38 @@ var gIdentityHandler = {
     let container = document.createElement("hbox");
     container.setAttribute("class", "identity-popup-permission-item");
     container.setAttribute("align", "center");
 
     let img = document.createElement("image");
     let classes = "identity-popup-permission-icon " + aPermission.id + "-icon";
     if (aPermission.state == SitePermissions.BLOCK)
       classes += " blocked-permission-icon";
-    if (aPermission.inUse)
+
+    if (aPermission.sharingState == Ci.nsIMediaManagerService.STATE_CAPTURE_ENABLED ||
+       (aPermission.id == "screen" && aPermission.sharingState &&
+        !aPermission.sharingState.includes("Paused"))) {
       classes += " in-use";
+
+      // Synchronize control center and identity block blinking animations.
+      BrowserUtils.promiseLayoutFlushed(document, "style", () => {
+        let sharingIconBlink = document.getElementById("sharing-icon").getAnimations()[0];
+        if (sharingIconBlink) {
+          let startTime = sharingIconBlink.startTime;
+          window.requestAnimationFrame(() => {
+            // TODO(Bug 1440607): This could cause a style flush, but putting
+            // the getAnimations() call outside of rAF causes a leak.
+            let imgBlink = img.getAnimations()[0];
+            if (imgBlink) {
+              imgBlink.startTime = startTime;
+            }
+          });
+        }
+      });
+    }
     img.setAttribute("class", classes);
 
     let nameLabel = document.createElement("label");
     nameLabel.setAttribute("flex", "1");
     nameLabel.setAttribute("class", "identity-popup-permission-label");
     nameLabel.textContent = SitePermissions.getPermissionLabel(aPermission.id);
 
     let isPolicyPermission = aPermission.scope == SitePermissions.SCOPE_POLICY;
@@ -8055,17 +8078,17 @@ var gIdentityHandler = {
     }
 
     let stateLabel = document.createElement("label");
     stateLabel.setAttribute("flex", "1");
     stateLabel.setAttribute("class", "identity-popup-permission-state-label");
     let {state, scope} = aPermission;
     // If the user did not permanently allow this device but it is currently
     // used, set the variables to display a "temporarily allowed" info.
-    if (state != SitePermissions.ALLOW && aPermission.inUse) {
+    if (state != SitePermissions.ALLOW && aPermission.sharingState) {
       state = SitePermissions.ALLOW;
       scope = SitePermissions.SCOPE_REQUEST;
     }
     stateLabel.textContent = SitePermissions.getCurrentStateLabel(state, scope);
 
     container.appendChild(img);
     container.appendChild(nameLabel);
     container.appendChild(stateLabel);
@@ -8079,17 +8102,17 @@ var gIdentityHandler = {
 
     let button = document.createElement("button");
     button.setAttribute("class", "identity-popup-permission-remove-button");
     let tooltiptext = gNavigatorBundle.getString("permissions.remove.tooltip");
     button.setAttribute("tooltiptext", tooltiptext);
     button.addEventListener("command", () => {
       let browser = gBrowser.selectedBrowser;
       this._permissionList.removeChild(container);
-      if (aPermission.inUse &&
+      if (aPermission.sharingState &&
           ["camera", "microphone", "screen"].includes(aPermission.id)) {
         let windowId = this._sharingState.windowId;
         if (aPermission.id == "screen") {
           windowId = "screen:" + windowId;
         } else {
           // If we set persistent permissions or the sharing has
           // started due to existing persistent permissions, we need
           // to handle removing these even for frames with different hostnames.
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1555,48 +1555,44 @@
       <method name="setBrowserSharing">
         <parameter name="aBrowser"/>
         <parameter name="aState"/>
         <body><![CDATA[
           let tab = this.getTabForBrowser(aBrowser);
           if (!tab)
             return;
 
-          let sharing;
-          if (aState.screen) {
-            sharing = "screen";
-          } else if (aState.camera) {
-            sharing = "camera";
-          } else if (aState.microphone) {
-            sharing = "microphone";
-          }
-
-          if (sharing) {
-            tab.setAttribute("sharing", sharing);
+          if (aState.sharing) {
             tab._sharingState = aState;
+            if (aState.paused) {
+              tab.removeAttribute("sharing");
+            } else {
+              tab.setAttribute("sharing", aState.sharing);
+            }
           } else {
+            tab._sharingState = null;
             tab.removeAttribute("sharing");
-            tab._sharingState = null;
           }
           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;
+          return {
+            camera: !!state.camera,
+            microphone: !!state.microphone,
+            screen: state.screen && state.screen.replace("Paused", ""),
+          };
         ]]></body>
       </method>
 
       <!-- TODO: remove after 57, once we know add-ons can no longer use it. -->
       <method name="setTabTitleLoading">
         <parameter name="aTab"/>
         <body/>
       </method>
--- a/browser/modules/ContentWebRTC.jsm
+++ b/browser/modules/ContentWebRTC.jsm
@@ -308,38 +308,39 @@ function updateIndicators(aSubject, aTop
 
   for (let contentWindow of contentWindows) {
     if (contentWindow.document.documentURI == kBrowserURL) {
       // There may be a preview shown at the same time as other streams.
       continue;
     }
 
     let tabState = getTabStateForContentWindow(contentWindow);
-    if (tabState.camera == MediaManagerService.STATE_CAPTURE_ENABLED)
+    if (tabState.camera == MediaManagerService.STATE_CAPTURE_ENABLED ||
+        tabState.camera == MediaManagerService.STATE_CAPTURE_DISABLED) {
       state.showCameraIndicator = true;
-    if (tabState.camera == MediaManagerService.STATE_CAPTURE_DISABLED)
-      state.showCameraIndicator = true;
-    if (tabState.microphone == MediaManagerService.STATE_CAPTURE_ENABLED)
+    }
+    if (tabState.microphone == MediaManagerService.STATE_CAPTURE_ENABLED ||
+        tabState.microphone == MediaManagerService.STATE_CAPTURE_DISABLED) {
       state.showMicrophoneIndicator = true;
-    if (tabState.microphone == MediaManagerService.STATE_CAPTURE_DISABLED)
-      state.showMicrophoneIndicator = true;
+    }
     if (tabState.screen) {
-      if (tabState.screen == "Screen") {
+      if (tabState.screen.startsWith("Screen")) {
         state.showScreenSharingIndicator = "Screen";
-      } else if (tabState.screen == "Window") {
+      } else if (tabState.screen.startsWith("Window")) {
         if (state.showScreenSharingIndicator != "Screen")
           state.showScreenSharingIndicator = "Window";
-      } else if (tabState.screen == "Application") {
+      } else if (tabState.screen.startsWith("Application")) {
         if (!state.showScreenSharingIndicator)
           state.showScreenSharingIndicator = "Application";
-      } else if (tabState.screen == "Browser") {
+      } else if (tabState.screen.startsWith("Browser")) {
         if (!state.showScreenSharingIndicator)
           state.showScreenSharingIndicator = "Browser";
       }
     }
+
     let mm = getMessageManagerForWindow(contentWindow);
     mm.sendAsyncMessage("webrtc:UpdateBrowserIndicators", tabState);
   }
 
   Services.cpmm.sendAsyncMessage("webrtc:UpdateGlobalIndicators", state);
 }
 
 function removeBrowserSpecificIndicator(aSubject, aTopic, aData) {
@@ -361,24 +362,47 @@ function removeBrowserSpecificIndicator(
 }
 
 function getTabStateForContentWindow(aContentWindow) {
   let camera = {}, microphone = {}, screen = {}, window = {}, app = {}, browser = {};
   MediaManagerService.mediaCaptureWindowState(aContentWindow,
                                               camera, microphone,
                                               screen, window, app, browser);
   let tabState = {camera: camera.value, microphone: microphone.value};
-  if (screen.value != MediaManagerService.STATE_NOCAPTURE)
+  if (screen.value == MediaManagerService.STATE_CAPTURE_ENABLED)
     tabState.screen = "Screen";
-  else if (window.value != MediaManagerService.STATE_NOCAPTURE)
+  else if (window.value == MediaManagerService.STATE_CAPTURE_ENABLED)
     tabState.screen = "Window";
-  else if (app.value != MediaManagerService.STATE_NOCAPTURE)
+  else if (app.value == MediaManagerService.STATE_CAPTURE_ENABLED)
     tabState.screen = "Application";
-  else if (browser.value != MediaManagerService.STATE_NOCAPTURE)
+  else if (browser.value == MediaManagerService.STATE_CAPTURE_ENABLED)
     tabState.screen = "Browser";
+  else if (screen.value == MediaManagerService.STATE_CAPTURE_DISABLED)
+    tabState.screen = "ScreenPaused";
+  else if (window.value == MediaManagerService.STATE_CAPTURE_DISABLED)
+    tabState.screen = "WindowPaused";
+  else if (app.value == MediaManagerService.STATE_CAPTURE_DISABLED)
+    tabState.screen = "ApplicationPaused";
+  else if (browser.value == MediaManagerService.STATE_CAPTURE_DISABLED)
+    tabState.screen = "BrowserPaused";
+
+  if (tabState.screen) {
+    tabState.sharing = "screen";
+  } else if (tabState.camera) {
+    tabState.sharing = "camera";
+  } else if (tabState.microphone) {
+    tabState.sharing = "microphone";
+  }
+
+  // The stream is considered paused when we're sharing something
+  // but all devices are off or set to disabled.
+  tabState.paused = tabState.sharing &&
+    (!tabState.screen || tabState.screen.includes("Paused")) &&
+    tabState.camera != MediaManagerService.STATE_CAPTURE_ENABLED &&
+    tabState.microphone != MediaManagerService.STATE_CAPTURE_ENABLED;
 
   tabState.windowId = getInnerWindowIDForWindow(aContentWindow);
   tabState.documentURI = aContentWindow.document.documentURI;
 
   return tabState;
 }
 
 function getInnerWindowIDForWindow(aContentWindow) {
--- a/browser/themes/shared/identity-block/identity-block.inc.css
+++ b/browser/themes/shared/identity-block/identity-block.inc.css
@@ -71,16 +71,17 @@
 
 #urlbar-display-box {
   padding-inline-start: 4px;
   border-inline-start: 1px solid var(--urlbar-separator-color);
   border-image: linear-gradient(transparent 15%, var(--urlbar-separator-color) 15%, var(--urlbar-separator-color) 85%, transparent 85%);
   border-image-slice: 1;
 }
 
+#sharing-icon,
 #identity-icon,
 #tracking-protection-icon,
 #connection-icon,
 .notification-anchor-icon,
 #blocked-permissions-container > .blocked-permission-icon,
 #extension-icon {
   width: 16px;
   height: 16px;
@@ -108,56 +109,40 @@
 }
 
 #urlbar[actiontype="extension"] > #identity-box > #identity-icon {
   list-style-image: url(chrome://mozapps/skin/extensions/extensionGeneric-16.svg);
 }
 
 /* SHARING ICON */
 
-#sharing-icon {
-  width: 16px;
-  height: 16px;
-  margin-inline-start: -16px;
-  position: relative;
-  -moz-context-properties: fill;
-  fill: rgb(224, 41, 29);
-  display: none;
-}
-
 #identity-box[sharing="camera"] > #sharing-icon {
   list-style-image: url("chrome://browser/skin/notification-icons/camera.svg");
 }
 
 #identity-box[sharing="microphone"] > #sharing-icon {
   list-style-image: url("chrome://browser/skin/notification-icons/microphone.svg");
 }
 
 #identity-box[sharing="screen"] > #sharing-icon {
   list-style-image: url("chrome://browser/skin/notification-icons/screen.svg");
 }
 
-#identity-box[sharing] > #sharing-icon {
-  display: -moz-box;
-  animation-delay: -1.5s;
+#identity-box:not([sharing]) > #sharing-icon {
+  display: none;
 }
 
-#identity-box[sharing] > #identity-icon,
-#sharing-icon {
-  animation: 3s linear identity-box-sharing-icon-pulse infinite;
+#identity-box[sharing]:not([paused]) > #sharing-icon {
+  animation: 1.5s ease in-use-blink infinite;
+  -moz-context-properties: fill;
+  fill: rgb(224, 41, 29);
 }
 
-/* This should remain identical to tab-sharing-icon-pulse in tabs.inc.css */
-@keyframes identity-box-sharing-icon-pulse {
-  0%, 16.66%, 83.33%, 100% {
-    opacity: 0;
-  }
-  33.33%, 66.66% {
-    opacity: 1;
-  }
+@keyframes in-use-blink {
+  50% { opacity: 0; }
 }
 
 /* TRACKING PROTECTION ICON */
 
 #tracking-protection-icon {
   list-style-image: url(chrome://browser/skin/tracking-protection-16.svg#enabled);
   margin-inline-end: 0;
 }
--- a/browser/themes/shared/tabs.inc.css
+++ b/browser/themes/shared/tabs.inc.css
@@ -244,17 +244,16 @@ tabbrowser {
   fill: currentColor;
 }
 
 .tab-icon-image[sharing]:not([selected]),
 .tab-sharing-icon-overlay {
   animation: 3s linear tab-sharing-icon-pulse infinite;
 }
 
-/* This should remain identical to identity-box-sharing-icon-pulse in identity-block.inc.css */
 @keyframes tab-sharing-icon-pulse {
   0%, 16.66%, 83.33%, 100% {
     opacity: 0;
   }
   33.33%, 66.66% {
     opacity: 1;
   }
 }