Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Mon, 17 Nov 2014 16:21:20 -0500
changeset 240346 2d8a4c388808f4175d835e645025355950fc1120
parent 240334 47f88e6ae34c7d62f30231ca5637c95360cb491a (current diff)
parent 240345 31722b673a74201a92605d9aee05ed8615901435 (diff)
child 240383 2d0a51ef828dc93013ae7a70bd8af8d9c2518f57
push id4311
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 19:37:41 +0000
treeherdermozilla-beta@150c9fed433b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone36.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
Merge fx-team to m-c. a=merge
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1314,17 +1314,17 @@ pref("browser.devedition.theme.enabled",
 pref("browser.devedition.theme.showCustomizeButton", false);
 #endif
 
 // Developer edition promo preferences
 pref("devtools.devedition.promo.shown", false);
 pref("devtools.devedition.promo.url", "https://mozilla.org/firefox/developer");
 
 // Only potentially show in beta release
-#ifdef MOZ_UPDATE_CHANNEL == beta
+#if MOZ_UPDATE_CHANNEL == beta
   pref("devtools.devedition.promo.enabled", true);
 #else
   pref("devtools.devedition.promo.enabled", false);
 #endif
 
 // Disable the error console
 pref("devtools.errorconsole.enabled", false);
 
--- a/browser/base/content/browser-devedition.js
+++ b/browser/base/content/browser-devedition.js
@@ -45,25 +45,36 @@ let DevEdition = {
       if (data == this._devtoolsThemePrefName) {
         this._updateDevtoolsThemeAttribute();
       } else {
         this._updateStyleSheetFromPrefs();
       }
     }
   },
 
+  _inferBrightness: function() {
+    ToolbarIconColor.inferFromText();
+    // Get an inverted full screen button if the dark theme is applied.
+    if (this.styleSheet &&
+        document.documentElement.getAttribute("devtoolstheme") == "dark") {
+      document.documentElement.setAttribute("brighttitlebarforeground", "true");
+    } else {
+      document.documentElement.removeAttribute("brighttitlebarforeground");
+    }
+  },
+
   _updateDevtoolsThemeAttribute: function() {
     // Set an attribute on root element to make it possible
     // to change colors based on the selected devtools theme.
     let devtoolsTheme = Services.prefs.getCharPref(this._devtoolsThemePrefName);
     if (devtoolsTheme != "dark") {
       devtoolsTheme = "light";
     }
     document.documentElement.setAttribute("devtoolstheme", devtoolsTheme);
-    ToolbarIconColor.inferFromText();
+    this._inferBrightness();
     this._updateStyleSheetFromPrefs();
   },
 
   _updateStyleSheetFromPrefs: function() {
     let lightweightThemeSelected = false;
     try {
       lightweightThemeSelected = Services.prefs.getBoolPref(this._lwThemePrefName);
     } catch(e) {}
@@ -78,17 +89,17 @@ let DevEdition = {
 
     this._toggleStyleSheet(deveditionThemeEnabled);
   },
 
   handleEvent: function(e) {
     if (e.type === "load") {
       this.styleSheet.removeEventListener("load", this);
       gBrowser.tabContainer._positionPinnedTabs();
-      ToolbarIconColor.inferFromText();
+      this._inferBrightness();
       Services.obs.notifyObservers(window, "devedition-theme-state-changed", true);
     }
   },
 
   _toggleStyleSheet: function(deveditionThemeEnabled) {
     if (deveditionThemeEnabled && !this.styleSheet) {
       let styleSheetAttr = `href="${this.styleSheetLocation}" type="text/css"`;
       this.styleSheet = document.createProcessingInstruction(
@@ -97,17 +108,17 @@ let DevEdition = {
       document.insertBefore(this.styleSheet, document.documentElement);
       // NB: we'll notify observers once the stylesheet has fully loaded, see
       // handleEvent above.
     } else if (!deveditionThemeEnabled && this.styleSheet) {
       this.styleSheet.removeEventListener("load", this);
       this.styleSheet.remove();
       this.styleSheet = null;
       gBrowser.tabContainer._positionPinnedTabs();
-      ToolbarIconColor.inferFromText();
+      this._inferBrightness();
       Services.obs.notifyObservers(window, "devedition-theme-state-changed", false);
     }
   },
 
   uninit: function () {
     Services.prefs.removeObserver(this._lwThemePrefName, this);
     Services.prefs.removeObserver(this._prefName, this);
     Services.prefs.removeObserver(this._devtoolsThemePrefName, this);
--- a/browser/base/content/browser-loop.js
+++ b/browser/base/content/browser-loop.js
@@ -6,17 +6,16 @@
 let LoopUI;
 
 XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI", "resource:///modules/loop/MozLoopAPI.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService", "resource:///modules/loop/MozLoopService.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/PanelFrame.jsm");
 
 
 (function() {
-
   LoopUI = {
     get toolbarButton() {
       delete this.toolbarButton;
       return this.toolbarButton = CustomizableUI.getWidget("loop-button").forWindow(window);
     },
 
     /**
      * Opens the panel for Loop and sizes it appropriately.
@@ -79,13 +78,33 @@ XPCOMUtils.defineLazyModuleGetter(this, 
       }
       let state = "";
       if (MozLoopService.errors.size) {
         state = "error";
       } else if (aReason == "login" && MozLoopService.userProfile) {
         state = "active";
       } else if (MozLoopService.doNotDisturb) {
         state = "disabled";
+      } else if (MozLoopService.roomsParticipantsCount > 0) {
+        state = "active";
       }
       this.toolbarButton.node.setAttribute("state", state);
     },
+
+    /**
+     * Play a sound in this window IF there's no sound playing yet.
+     *
+     * @param {String} name Name of the sound, like 'ringtone' or 'room-joined'
+     */
+    playSound: function(name) {
+      if (this.ActiveSound || MozLoopService.doNotDisturb) {
+        return;
+      }
+
+      this.activeSound = new window.Audio();
+      this.activeSound.src = `chrome://browser/content/loop/shared/sounds/${name}.ogg`;
+      this.activeSound.load();
+      this.activeSound.play();
+
+      this.activeSound.addEventListener("ended", () => this.activeSound = undefined, false);
+    },
   };
 })();
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -82,51 +82,71 @@ addEventListener("DOMFormHasPassword", f
 });
 addEventListener("DOMAutoComplete", function(event) {
   LoginManagerContent.onUsernameInput(event);
 });
 addEventListener("blur", function(event) {
   LoginManagerContent.onUsernameInput(event);
 });
 
-if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
-  let handleContentContextMenu = function (event) {
-    let defaultPrevented = event.defaultPrevented;
-    if (!Services.prefs.getBoolPref("dom.event.contextmenu.enabled")) {
-      let plugin = null;
-      try {
-        plugin = event.target.QueryInterface(Ci.nsIObjectLoadingContent);
-      } catch (e) {}
-      if (plugin && plugin.displayedType == Ci.nsIObjectLoadingContent.TYPE_PLUGIN) {
-        // Don't open a context menu for plugins.
-        return;
-      }
-
-      defaultPrevented = false;
+let handleContentContextMenu = function (event) {
+  let defaultPrevented = event.defaultPrevented;
+  if (!Services.prefs.getBoolPref("dom.event.contextmenu.enabled")) {
+    let plugin = null;
+    try {
+      plugin = event.target.QueryInterface(Ci.nsIObjectLoadingContent);
+    } catch (e) {}
+    if (plugin && plugin.displayedType == Ci.nsIObjectLoadingContent.TYPE_PLUGIN) {
+      // Don't open a context menu for plugins.
+      return;
     }
 
-    if (!defaultPrevented) {
-      let editFlags = SpellCheckHelper.isEditable(event.target, content);
-      let spellInfo;
-      if (editFlags &
-          (SpellCheckHelper.EDITABLE | SpellCheckHelper.CONTENTEDITABLE)) {
-        spellInfo =
-          InlineSpellCheckerContent.initContextMenu(event, editFlags, this);
-      }
-
-      sendSyncMessage("contextmenu", { editFlags, spellInfo }, { event });
-    }
+    defaultPrevented = false;
   }
 
-  Cc["@mozilla.org/eventlistenerservice;1"]
-    .getService(Ci.nsIEventListenerService)
-    .addSystemEventListener(global, "contextmenu", handleContentContextMenu, true);
+  if (defaultPrevented)
+    return;
+
+  let addonInfo = {};
+  let subject = {
+    event: event,
+    addonInfo: addonInfo,
+  };
+  subject.wrappedJSObject = subject;
+  Services.obs.notifyObservers(subject, "content-contextmenu", null);
+
+  if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+    let editFlags = SpellCheckHelper.isEditable(event.target, content);
+    let spellInfo;
+    if (editFlags &
+        (SpellCheckHelper.EDITABLE | SpellCheckHelper.CONTENTEDITABLE)) {
+      spellInfo =
+        InlineSpellCheckerContent.initContextMenu(event, editFlags, this);
+    }
 
+    sendSyncMessage("contextmenu", { editFlags, spellInfo, addonInfo }, { event, popupNode: event.target });
+  }
+  else {
+    // Break out to the parent window and pass the add-on info along
+    let browser = docShell.chromeEventHandler;
+    let mainWin = browser.ownerDocument.defaultView;
+    mainWin.gContextMenuContentData = {
+      isRemote: false,
+      event: event,
+      popupNode: event.target,
+      browser: browser,
+      addonInfo: addonInfo,
+    };
+  }
 }
 
+Cc["@mozilla.org/eventlistenerservice;1"]
+  .getService(Ci.nsIEventListenerService)
+  .addSystemEventListener(global, "contextmenu", handleContentContextMenu, false);
+
 let AboutNetErrorListener = {
   init: function(chromeGlobal) {
     chromeGlobal.addEventListener('AboutNetErrorLoad', this, false, true);
     chromeGlobal.addEventListener('AboutNetErrorSetAutomatic', this, false, true);
     chromeGlobal.addEventListener('AboutNetErrorSendReport', this, false, true);
   },
 
   get isAboutNetError() {
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -523,27 +523,26 @@ nsContextMenu.prototype = {
       } else {
         inspector.selection.setNode(this.target, "browser-context-menu");
       }
     }.bind(this));
   },
 
   // Set various context menu attributes based on the state of the world.
   setTarget: function (aNode, aRangeParent, aRangeOffset) {
-    // If gContextMenuContentData is not null, this event was forwarded from a
-    // child process, so use that information instead.
+    // gContextMenuContentData.isRemote tells us if the event came from a remote
+    // process. gContextMenuContentData can be null if something (like tests)
+    // opens the context menu directly.
     let editFlags;
-    if (gContextMenuContentData) {
-      this.isRemote = true;
+    this.isRemote = gContextMenuContentData && gContextMenuContentData.isRemote;
+    if (this.isRemote) {
       aNode = gContextMenuContentData.event.target;
       aRangeParent = gContextMenuContentData.event.rangeParent;
       aRangeOffset = gContextMenuContentData.event.rangeOffset;
       editFlags = gContextMenuContentData.editFlags;
-    } else {
-      this.isRemote = false;
     }
 
     const xulNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
     if (aNode.namespaceURI == xulNS ||
         aNode.nodeType == Node.DOCUMENT_NODE ||
         this.isDisabledForEvents(aNode)) {
       this.shouldDisplay = false;
       return;
@@ -642,17 +641,17 @@ nsContextMenu.prototype = {
       else if (this.target instanceof HTMLAudioElement) {
         this.onAudio = true;
         this.mediaURL = this.target.currentSrc || this.target.src;
       }
       else if (editFlags & (SpellCheckHelper.INPUT | SpellCheckHelper.TEXTAREA)) {
         this.onTextInput = (editFlags & SpellCheckHelper.TEXTINPUT) !== 0;
         this.onEditableArea = (editFlags & SpellCheckHelper.EDITABLE) !== 0;
         if (this.onEditableArea) {
-          if (gContextMenuContentData) {
+          if (this.isRemote) {
             InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
           }
           else {
             InlineSpellCheckerUI.init(this.target.QueryInterface(Ci.nsIDOMNSEditableElement).editor);
             InlineSpellCheckerUI.initFromEvent(aRangeParent, aRangeOffset);
           }
         }
         this.onKeywordField = (editFlags & SpellCheckHelper.KEYWORD);
@@ -767,17 +766,17 @@ nsContextMenu.prototype = {
         this.onLoadedImage     = false;
         this.onCompletedImage  = false;
         this.onMathML          = false;
         this.inFrame           = false;
         this.inSrcdocFrame     = false;
         this.hasBGImage        = false;
         this.isDesignMode      = true;
         this.onEditableArea = true;
-        if (gContextMenuContentData) {
+        if (this.isRemote) {
           InlineSpellCheckerUI.initFromRemote(gContextMenuContentData.spellInfo);
         }
         else {
           var targetWin = this.target.ownerDocument.defaultView;
           var editingSession = targetWin.QueryInterface(Ci.nsIInterfaceRequestor)
                                         .getInterface(Ci.nsIWebNavigation)
                                         .QueryInterface(Ci.nsIInterfaceRequestor)
                                         .getInterface(Ci.nsIEditingSession);
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -3077,20 +3077,23 @@
                 this.removeTab(tab);
               }
               break;
             }
             case "contextmenu": {
               let spellInfo = aMessage.data.spellInfo;
               if (spellInfo)
                 spellInfo.target = aMessage.target.messageManager;
-              gContextMenuContentData = { event: aMessage.objects.event,
+              gContextMenuContentData = { isRemote: true,
+                                          event: aMessage.objects.event,
+                                          popupNode: aMessage.objects.popupNode,
                                           browser: browser,
                                           editFlags: aMessage.data.editFlags,
-                                          spellInfo: spellInfo };
+                                          spellInfo: spellInfo,
+                                          addonInfo: aMessage.data.addonInfo };
               let popup = browser.ownerDocument.getElementById("contentAreaContextMenu");
               let event = gContextMenuContentData.event;
               let pos = browser.mapScreenCoordinatesFromContent(event.screenX, event.screenY);
               popup.openPopupAtScreen(pos.x, pos.y, true);
               break;
             }
             case "DOMWebNotificationClicked": {
               let tab = this.getTabForBrowser(browser);
--- a/browser/base/content/test/general/browser_devedition.js
+++ b/browser/base/content/test/general/browser_devedition.js
@@ -11,84 +11,93 @@ const PREF_DEVTOOLS_THEME = "devtools.th
 
 registerCleanupFunction(() => {
   // Set preferences back to their original values
   Services.prefs.clearUserPref(PREF_DEVEDITION_THEME);
   Services.prefs.clearUserPref(PREF_LWTHEME);
   Services.prefs.clearUserPref(PREF_DEVTOOLS_THEME);
 });
 
-function test() {
-  waitForExplicitFinish();
-  startTests();
-}
-
-function startTests() {
+add_task(function* startTests() {
   Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark");
 
   info ("Setting browser.devedition.theme.enabled to false.");
   Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, false);
   ok (!DevEdition.styleSheet, "There is no devedition style sheet when the pref is false.");
 
   info ("Setting browser.devedition.theme.enabled to true.");
   Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true);
   ok (DevEdition.styleSheet, "There is a devedition stylesheet when no themes are applied and pref is set.");
 
   info ("Adding a lightweight theme.");
   Services.prefs.setBoolPref(PREF_LWTHEME, true);
   ok (!DevEdition.styleSheet, "The devedition stylesheet has been removed when a lightweight theme is applied.");
 
   info ("Removing a lightweight theme.");
+  let onAttributeAdded = waitForBrightTitlebarAttribute();
   Services.prefs.setBoolPref(PREF_LWTHEME, false);
   ok (DevEdition.styleSheet, "The devedition stylesheet has been added when a lightweight theme is removed.");
+  yield onAttributeAdded;
+
+  is (document.documentElement.getAttribute("brighttitlebarforeground"), "true",
+     "The brighttitlebarforeground attribute is set on the window.");
 
   info ("Setting browser.devedition.theme.enabled to false.");
   Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, false);
   ok (!DevEdition.styleSheet, "The devedition stylesheet has been removed.");
 
-  testDevtoolsTheme();
-  testLightweightThemePreview();
-  finish();
-}
+  ok (!document.documentElement.hasAttribute("brighttitlebarforeground"),
+     "The brighttitlebarforeground attribute is not set on the window after devedition.theme is false.");
+});
 
-function testDevtoolsTheme() {
+add_task(function* testDevtoolsTheme() {
   info ("Checking that Australis is shown when the light devtools theme is applied.");
 
+  let onAttributeAdded = waitForBrightTitlebarAttribute();
   Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true);
   ok (DevEdition.styleSheet, "The devedition stylesheet exists.");
+  yield onAttributeAdded;
+  ok (document.documentElement.hasAttribute("brighttitlebarforeground"),
+     "The brighttitlebarforeground attribute is set on the window with dark devtools theme.");
 
   info ("Checking stylesheet and :root attributes based on devtools theme.");
   Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "light");
   is (document.documentElement.getAttribute("devtoolstheme"), "light",
     "The documentElement has an attribute based on devtools theme.");
   ok (DevEdition.styleSheet, "The devedition stylesheet is still there with the light devtools theme.");
+  ok (!document.documentElement.hasAttribute("brighttitlebarforeground"),
+     "The brighttitlebarforeground attribute is not set on the window with light devtools theme.");
 
   Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark");
   is (document.documentElement.getAttribute("devtoolstheme"), "dark",
     "The documentElement has an attribute based on devtools theme.");
   ok (DevEdition.styleSheet, "The devedition stylesheet is still there with the dark devtools theme.");
+  is (document.documentElement.getAttribute("brighttitlebarforeground"), "true",
+     "The brighttitlebarforeground attribute is set on the window with dark devtools theme.");
 
   Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "foobar");
   is (document.documentElement.getAttribute("devtoolstheme"), "light",
     "The documentElement has 'light' as a default for the devtoolstheme attribute");
   ok (DevEdition.styleSheet, "The devedition stylesheet is still there with the foobar devtools theme.");
-}
+  ok (!document.documentElement.hasAttribute("brighttitlebarforeground"),
+     "The brighttitlebarforeground attribute is not set on the window with light devtools theme.");
+});
 
 function dummyLightweightTheme(id) {
   return {
     id: id,
     name: id,
     headerURL: "http://lwttest.invalid/a.png",
     footerURL: "http://lwttest.invalid/b.png",
     textcolor: "red",
     accentcolor: "blue"
   };
 }
 
-function testLightweightThemePreview() {
+add_task(function* testLightweightThemePreview() {
   let {LightweightThemeManager} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {});
 
   info ("Turning the pref on, then previewing lightweight themes");
   Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true);
   ok (DevEdition.styleSheet, "The devedition stylesheet is enabled.");
   LightweightThemeManager.previewTheme(dummyLightweightTheme("preview0"));
   ok (!DevEdition.styleSheet, "The devedition stylesheet is not enabled after a lightweight theme preview.");
   LightweightThemeManager.resetPreview();
@@ -109,9 +118,26 @@ function testLightweightThemePreview() {
 
   info ("Turning the pref on, then previewing the default theme, turning it off and resetting the preview");
   Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true);
   ok (DevEdition.styleSheet, "The devedition stylesheet is enabled.");
   LightweightThemeManager.previewTheme(null);
   ok (DevEdition.styleSheet, "The devedition stylesheet is still enabled after the default theme is applied.");
   LightweightThemeManager.resetPreview();
   ok (DevEdition.styleSheet, "The devedition stylesheet is still enabled after resetting the preview.");
+});
+
+// Use a mutation observer to wait for the brighttitlebarforeground
+// attribute to change.  Using this instead of waiting for the load
+// event on the DevEdition styleSheet.
+function waitForBrightTitlebarAttribute() {
+  return new Promise((resolve, reject) => {
+    let mutationObserver = new MutationObserver(function (mutations) {
+      for (let mutation of mutations) {
+        if (mutation.attributeName == "brighttitlebarforeground") {
+          mutationObserver.disconnect();
+          resolve();
+        }
+      }
+    });
+    mutationObserver.observe(document.documentElement, { attributes: true });
+  });
 }
--- a/browser/components/loop/LoopRooms.jsm
+++ b/browser/components/loop/LoopRooms.jsm
@@ -51,17 +51,21 @@ const extend = function(target, source) 
  *
  * @param {Object} room        A room object that contains a list of current participants
  * @param {Object} participant Participant to check if it's already there
  * @returns {Boolean} TRUE when the participant is already a member of the room,
  *                    FALSE when it's not.
  */
 const containsParticipant = function(room, participant) {
   for (let user of room.participants) {
-    if (user.roomConnectionId == participant.roomConnectionId) {
+    // XXX until a bug 1100318 is implemented and deployed,
+    // we need to check the "id" field here as well - roomConnectionId is the
+    // official value for the interface.
+    if (user.roomConnectionId == participant.roomConnectionId &&
+        user.id == participant.id) {
       return true;
     }
   }
   return false;
 };
 
 /**
  * Compares the list of participants of the room currently in the cache and an
@@ -102,24 +106,45 @@ const checkForParticipantsUpdate = funct
 /**
  * The Rooms class.
  *
  * Each method that is a member of this class requires the last argument to be a
  * callback Function. MozLoopAPI will cause things to break if this invariant is
  * violated. You'll notice this as well in the documentation for each method.
  */
 let LoopRoomsInternal = {
+  /**
+   * @var {Map} rooms Collection of rooms currently in cache.
+   */
   rooms: new Map(),
 
+  /**
+   * @var {String} sessionType The type of user session. May be 'FXA' or 'GUEST'.
+   */
   get sessionType() {
     return MozLoopService.userProfile ? LOOP_SESSION_TYPE.FXA :
                                         LOOP_SESSION_TYPE.GUEST;
   },
 
   /**
+   * @var {Number} participantsCount The total amount of participants currently
+   *                                 inside all rooms.
+   */
+  get participantsCount() {
+    let count = 0;
+    for (let room of this.rooms.values()) {
+      if (!("participants" in room)) {
+        continue;
+      }
+      count += room.participants.length;
+    }
+    return count;
+  },
+
+  /**
    * Fetch a list of rooms that the currently registered user is a member of.
    *
    * @param {String}   [version] If set, we will fetch a list of changed rooms since
    *                             `version`. Optional.
    * @param {Function} callback  Function that will be invoked once the operation
    *                             finished. The first argument passed will be an
    *                             `Error` object or `null`. The second argument will
    *                             be the list of rooms, if it was fetched successfully.
@@ -149,16 +174,20 @@ let LoopRoomsInternal = {
       }
 
       for (let room of roomsList) {
         // See if we already have this room in our cache.
         let orig = this.rooms.get(room.roomToken);
         if (orig) {
           checkForParticipantsUpdate(orig, room);
         }
+        // Remove the `currSize` for posterity.
+        if ("currSize" in room) {
+          delete room.currSize;
+        }
         this.rooms.set(room.roomToken, room);
         // When a version is specified, all the data is already provided by this
         // request.
         if (version) {
           eventEmitter.emit("update", room);
           eventEmitter.emit("update" + ":" + room.roomToken, room);
         } else {
           // Next, request the detailed information for each room. If the request
@@ -198,21 +227,16 @@ let LoopRoomsInternal = {
 
     MozLoopService.hawkRequest(this.sessionType, "/rooms/" + encodeURIComponent(roomToken), "GET")
       .then(response => {
         let data = JSON.parse(response.body);
 
         room.roomToken = roomToken;
         checkForParticipantsUpdate(room, data);
         extend(room, data);
-
-        // Remove the `currSize` for posterity.
-        if ("currSize" in room) {
-          delete room.currSize;
-        }
         this.rooms.set(roomToken, room);
 
         let eventName = !needsUpdate ? "update" : "add";
         eventEmitter.emit(eventName, room);
         eventEmitter.emit(eventName + ":" + roomToken, room);
         callback(null, room);
       }, err => callback(err)).catch(err => callback(err));
   },
@@ -377,16 +401,20 @@ Object.freeze(LoopRoomsInternal);
  *  - 'update[:{room-id}]': A room object was successfully updated with changed
  *                          properties in the data store.
  *  - 'joined[:{room-id}]': A participant joined a room.
  *  - 'left[:{room-id}]':   A participant left a room.
  *
  * See the internal code for the API documentation.
  */
 this.LoopRooms = {
+  get participantsCount() {
+    return LoopRoomsInternal.participantsCount;
+  },
+
   getAll: function(version, callback) {
     return LoopRoomsInternal.getAll(version, callback);
   },
 
   get: function(roomToken, callback) {
     return LoopRoomsInternal.get(roomToken, callback);
   },
 
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -76,16 +76,20 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gDNSService",
                                    "@mozilla.org/network/dns-service;1",
                                    "nsIDNSService");
 
+XPCOMUtils.defineLazyServiceGetter(this, "gWM",
+                                   "@mozilla.org/appshell/window-mediator;1",
+                                   "nsIWindowMediator");
+
 // Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
 XPCOMUtils.defineLazyGetter(this, "log", () => {
   let ConsoleAPI = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).ConsoleAPI;
   let consoleOptions = {
     maxLogLevel: Services.prefs.getCharPref(PREF_LOG_LEVEL).toLowerCase(),
     prefix: "Loop",
   };
   return new ConsoleAPI(consoleOptions);
@@ -960,16 +964,20 @@ this.MozLoopService = {
       roomsGuest: "19d3f799-a8f3-4328-9822-b7cd02765832",
     };
   },
 
   set initializeTimerFunc(value) {
     gInitializeTimerFunc = value;
   },
 
+  get roomsParticipantsCount() {
+    return LoopRooms.participantsCount;
+  },
+
   /**
    * Initialized the loop service, and starts registration with the
    * push and loop servers.
    *
    * @return {Promise}
    */
   initialize: Task.async(function*() {
     // Do this here, rather than immediately after definition, so that we can
@@ -990,16 +998,36 @@ this.MozLoopService = {
 
     if (Services.prefs.getPrefType("loop.fxa.enabled") == Services.prefs.PREF_BOOL) {
       gFxAEnabled = Services.prefs.getBoolPref("loop.fxa.enabled");
       if (!gFxAEnabled) {
         yield this.logOutFromFxA();
       }
     }
 
+    // The Loop toolbar button should change icon when the room participant count
+    // changes from 0 to something.
+    const onRoomsChange = () => {
+      MozLoopServiceInternal.notifyStatusChanged();
+    };
+    LoopRooms.on("add", onRoomsChange);
+    LoopRooms.on("update", onRoomsChange);
+    LoopRooms.on("joined", (e, roomToken, participant) => {
+      // Don't alert if we're in the doNotDisturb mode, or the participant
+      // is the owner - the content code deals with the rest of the sounds.
+      if (MozLoopServiceInternal.doNotDisturb || participant.owner) {
+        return;
+      }
+
+      let window = gWM.getMostRecentWindow("navigator:browser");
+      if (window) {
+        window.LoopUI.playSound("room-joined");
+      }
+    });
+
     // If expiresTime is not in the future and the user hasn't
     // previously authenticated then skip registration.
     if (!MozLoopServiceInternal.urlExpiryTimeIsInFuture() &&
         !MozLoopServiceInternal.fxAOAuthTokenData) {
       return Promise.resolve("registration not needed");
     }
 
     let deferredInitialization = Promise.defer();
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -26,23 +26,29 @@ loop.panel = (function(_, mozL10n) {
     propTypes: {
       buttonsHidden: React.PropTypes.bool,
       // The selectedTab prop is used by the UI showcase.
       selectedTab: React.PropTypes.string
     },
 
     getDefaultProps: function() {
       return {
-        buttonsHidden: false,
-        selectedTab: "call"
+        buttonsHidden: false
       };
     },
 
     getInitialState: function() {
-      return {selectedTab: this.props.selectedTab};
+      // XXX Work around props.selectedTab being undefined initially.
+      // When we don't need to rely on the pref, this can move back to
+      // getDefaultProps (bug 1100258).
+      return {
+        selectedTab: this.props.selectedTab ||
+          (navigator.mozLoop.getLoopBoolPref("rooms.enabled") ?
+            "rooms" : "call")
+      };
     },
 
     handleSelectTab: function(event) {
       var tabName = event.target.dataset.tabName;
       this.setState({selectedTab: tabName});
     },
 
     render: function() {
@@ -673,41 +679,56 @@ loop.panel = (function(_, mozL10n) {
           detailsButtonLabel: serviceError.error.friendlyDetailsButtonLabel,
           detailsButtonCallback: serviceError.error.friendlyDetailsButtonCallback,
         });
       } else {
         this.props.notifications.remove(this.props.notifications.get("service-error"));
       }
     },
 
+    _roomsEnabled: function() {
+      return navigator.mozLoop.getLoopBoolPref("rooms.enabled");
+    },
+
     _onStatusChanged: function() {
       var profile = navigator.mozLoop.userProfile;
       var currUid = this.state.userProfile ? this.state.userProfile.uid : null;
       var newUid = profile ? profile.uid : null;
       if (currUid != newUid) {
         // On profile change (login, logout), switch back to the default tab.
-        this.selectTab("call");
+        this.selectTab(this._roomsEnabled() ? "rooms" : "call");
         this.setState({userProfile: profile});
       }
       this.updateServiceErrors();
     },
 
     /**
      * The rooms feature is hidden by default for now. Once it gets mainstream,
-     * this method can be safely removed.
+     * this method can be simplified.
      */
-    _renderRoomsTab: function() {
-      if (!navigator.mozLoop.getLoopBoolPref("rooms.enabled")) {
-        return null;
+    _renderRoomsOrCallTab: function() {
+      if (!this._roomsEnabled()) {
+        return (
+          Tab({name: "call"}, 
+            React.DOM.div({className: "content-area"}, 
+              CallUrlResult({client: this.props.client, 
+                             notifications: this.props.notifications, 
+                             callUrl: this.props.callUrl}), 
+              ToSView(null)
+            )
+          )
+        );
       }
+
       return (
         Tab({name: "rooms"}, 
           RoomList({dispatcher: this.props.dispatcher, 
                     store: this.props.roomStore, 
-                    userDisplayName: this._getUserDisplayName()})
+                    userDisplayName: this._getUserDisplayName()}), 
+          ToSView(null)
         )
       );
     },
 
     startForm: function(name, contact) {
       this.refs[name].initForm(contact);
       this.selectTab(name);
     },
@@ -730,31 +751,24 @@ loop.panel = (function(_, mozL10n) {
 
     _getUserDisplayName: function() {
       return this.state.userProfile && this.state.userProfile.email ||
              __("display_name_guest");
     },
 
     render: function() {
       var NotificationListView = sharedViews.NotificationListView;
+
       return (
         React.DOM.div(null, 
           NotificationListView({notifications: this.props.notifications, 
                                 clearOnDocumentHidden: true}), 
           TabView({ref: "tabView", selectedTab: this.props.selectedTab, 
             buttonsHidden: !this.state.userProfile && !this.props.showTabButtons}, 
-            Tab({name: "call"}, 
-              React.DOM.div({className: "content-area"}, 
-                CallUrlResult({client: this.props.client, 
-                               notifications: this.props.notifications, 
-                               callUrl: this.props.callUrl}), 
-                ToSView(null)
-              )
-            ), 
-            this._renderRoomsTab(), 
+            this._renderRoomsOrCallTab(), 
             Tab({name: "contacts"}, 
               ContactsList({selectTab: this.selectTab, 
                             startForm: this.startForm})
             ), 
             Tab({name: "contacts_add", hidden: true}, 
               ContactDetailsForm({ref: "contacts_add", mode: "add", 
                                   selectTab: this.selectTab})
             ), 
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -26,23 +26,29 @@ loop.panel = (function(_, mozL10n) {
     propTypes: {
       buttonsHidden: React.PropTypes.bool,
       // The selectedTab prop is used by the UI showcase.
       selectedTab: React.PropTypes.string
     },
 
     getDefaultProps: function() {
       return {
-        buttonsHidden: false,
-        selectedTab: "call"
+        buttonsHidden: false
       };
     },
 
     getInitialState: function() {
-      return {selectedTab: this.props.selectedTab};
+      // XXX Work around props.selectedTab being undefined initially.
+      // When we don't need to rely on the pref, this can move back to
+      // getDefaultProps (bug 1100258).
+      return {
+        selectedTab: this.props.selectedTab ||
+          (navigator.mozLoop.getLoopBoolPref("rooms.enabled") ?
+            "rooms" : "call")
+      };
     },
 
     handleSelectTab: function(event) {
       var tabName = event.target.dataset.tabName;
       this.setState({selectedTab: tabName});
     },
 
     render: function() {
@@ -673,41 +679,56 @@ loop.panel = (function(_, mozL10n) {
           detailsButtonLabel: serviceError.error.friendlyDetailsButtonLabel,
           detailsButtonCallback: serviceError.error.friendlyDetailsButtonCallback,
         });
       } else {
         this.props.notifications.remove(this.props.notifications.get("service-error"));
       }
     },
 
+    _roomsEnabled: function() {
+      return navigator.mozLoop.getLoopBoolPref("rooms.enabled");
+    },
+
     _onStatusChanged: function() {
       var profile = navigator.mozLoop.userProfile;
       var currUid = this.state.userProfile ? this.state.userProfile.uid : null;
       var newUid = profile ? profile.uid : null;
       if (currUid != newUid) {
         // On profile change (login, logout), switch back to the default tab.
-        this.selectTab("call");
+        this.selectTab(this._roomsEnabled() ? "rooms" : "call");
         this.setState({userProfile: profile});
       }
       this.updateServiceErrors();
     },
 
     /**
      * The rooms feature is hidden by default for now. Once it gets mainstream,
-     * this method can be safely removed.
+     * this method can be simplified.
      */
-    _renderRoomsTab: function() {
-      if (!navigator.mozLoop.getLoopBoolPref("rooms.enabled")) {
-        return null;
+    _renderRoomsOrCallTab: function() {
+      if (!this._roomsEnabled()) {
+        return (
+          <Tab name="call">
+            <div className="content-area">
+              <CallUrlResult client={this.props.client}
+                             notifications={this.props.notifications}
+                             callUrl={this.props.callUrl} />
+              <ToSView />
+            </div>
+          </Tab>
+        );
       }
+
       return (
         <Tab name="rooms">
           <RoomList dispatcher={this.props.dispatcher}
                     store={this.props.roomStore}
                     userDisplayName={this._getUserDisplayName()}/>
+          <ToSView />
         </Tab>
       );
     },
 
     startForm: function(name, contact) {
       this.refs[name].initForm(contact);
       this.selectTab(name);
     },
@@ -730,31 +751,24 @@ loop.panel = (function(_, mozL10n) {
 
     _getUserDisplayName: function() {
       return this.state.userProfile && this.state.userProfile.email ||
              __("display_name_guest");
     },
 
     render: function() {
       var NotificationListView = sharedViews.NotificationListView;
+
       return (
         <div>
           <NotificationListView notifications={this.props.notifications}
                                 clearOnDocumentHidden={true} />
           <TabView ref="tabView" selectedTab={this.props.selectedTab}
             buttonsHidden={!this.state.userProfile && !this.props.showTabButtons}>
-            <Tab name="call">
-              <div className="content-area">
-                <CallUrlResult client={this.props.client}
-                               notifications={this.props.notifications}
-                               callUrl={this.props.callUrl} />
-                <ToSView />
-              </div>
-            </Tab>
-            {this._renderRoomsTab()}
+            {this._renderRoomsOrCallTab()}
             <Tab name="contacts">
               <ContactsList selectTab={this.selectTab}
                             startForm={this.startForm} />
             </Tab>
             <Tab name="contacts_add" hidden={true}>
               <ContactDetailsForm ref="contacts_add" mode="add"
                                   selectTab={this.selectTab} />
             </Tab>
--- a/browser/components/loop/content/shared/css/panel.css
+++ b/browser/components/loop/content/shared/css/panel.css
@@ -135,17 +135,16 @@ body {
 
 .content-area input:not(.pristine):invalid {
   border-color: #d74345;
   box-shadow: 0 0 4px #c43c3e;
 }
 
 /* Rooms */
 .rooms {
-  background: #f5f5f5;
   min-height: 100px;
 }
 
 .rooms > h1 {
   font-weight: bold;
   color: #999;
   padding: .5rem 1rem;
   border-bottom: 1px solid #ddd;
--- a/browser/components/loop/content/shared/js/mixins.js
+++ b/browser/components/loop/content/shared/js/mixins.js
@@ -149,16 +149,20 @@ loop.shared.mixins = (function() {
     },
 
     /**
      * Starts playing an audio file, stopping any audio that is already in progress.
      *
      * @param {String} name The filename to play (excluding the extension).
      */
     play: function(name, options) {
+      if (this._isLoopDesktop() && rootObject.navigator.mozLoop.doNotDisturb) {
+        return;
+      }
+
       options = options || {};
       options.loop = options.loop || false;
 
       this._ensureAudioStopped();
       this._getAudioBlob(name, function(error, blob) {
         if (error) {
           console.error(error);
           return;
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..5e952291f167014246bbc28270c6897db39fe004
GIT binary patch
literal 16312
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
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -81,16 +81,17 @@ browser.jar:
   content/browser/loop/shared/libs/jquery-2.1.0.js    (content/shared/libs/jquery-2.1.0.js)
   content/browser/loop/shared/libs/backbone-1.1.2.js  (content/shared/libs/backbone-1.1.2.js)
 
   # Shared sounds
   content/browser/loop/shared/sounds/ringtone.ogg     (content/shared/sounds/ringtone.ogg)
   content/browser/loop/shared/sounds/connecting.ogg   (content/shared/sounds/connecting.ogg)
   content/browser/loop/shared/sounds/connected.ogg    (content/shared/sounds/connected.ogg)
   content/browser/loop/shared/sounds/terminated.ogg   (content/shared/sounds/terminated.ogg)
+  content/browser/loop/shared/sounds/room-joined.ogg  (content/shared/sounds/room-joined.ogg)
   content/browser/loop/shared/sounds/failure.ogg      (content/shared/sounds/failure.ogg)
 
   # Partner SDK assets
   content/browser/loop/libs/sdk.js                                                    (content/shared/libs/sdk.js)
   content/browser/loop/sdk-content/css/ot.css                                 (content/shared/libs/sdk-content/css/ot.css)
   content/browser/loop/sdk-content/js/dynamic_config.min.js                   (content/shared/libs/sdk-content/js/dynamic_config.min.js)
   content/browser/loop/sdk-content/images/rtc/access-denied-chrome.png        (content/shared/libs/sdk-content/images/rtc/access-denied-chrome.png)
   content/browser/loop/sdk-content/images/rtc/access-denied-copy-firefox.png  (content/shared/libs/sdk-content/images/rtc/access-denied-copy-firefox.png)
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -912,16 +912,17 @@ describe("loop.conversation", function()
     var view, fakeAudio;
 
     beforeEach(function() {
       fakeAudio = {
         play: sinon.spy(),
         pause: sinon.spy(),
         removeAttribute: sinon.spy()
       };
+      navigator.mozLoop.doNotDisturb = false;
       sandbox.stub(window, "Audio").returns(fakeAudio);
 
       view = TestUtils.renderIntoDocument(
         loop.conversation.GenericFailureView({
           cancelCall: function() {}
         })
       );
     });
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -179,17 +179,17 @@ describe("loop.panel", function() {
           navigator.mozLoop.getLoopBoolPref = function(pref) {
             if (pref === "rooms.enabled") {
               return true;
             }
           };
 
           view = createTestPanelView();
 
-          [callTab, roomsTab, contactsTab] =
+          [roomsTab, contactsTab] =
             TestUtils.scryRenderedDOMComponentsWithClass(view, "tab");
         });
 
         it("should select contacts tab when clicking tab button", function() {
           TestUtils.Simulate.click(
             view.getDOMNode().querySelector("li[data-tab-name=\"contacts\"]"));
 
           expect(contactsTab.getDOMNode().classList.contains("selected"))
@@ -198,24 +198,16 @@ describe("loop.panel", function() {
 
         it("should select rooms tab when clicking tab button", function() {
           TestUtils.Simulate.click(
             view.getDOMNode().querySelector("li[data-tab-name=\"rooms\"]"));
 
           expect(roomsTab.getDOMNode().classList.contains("selected"))
             .to.be.true;
         });
-
-        it("should select call tab when clicking tab button", function() {
-          TestUtils.Simulate.click(
-            view.getDOMNode().querySelector("li[data-tab-name=\"call\"]"));
-
-          expect(callTab.getDOMNode().classList.contains("selected"))
-            .to.be.true;
-        });
       });
 
       describe("loop.rooms.enabled off", function() {
         beforeEach(function() {
           navigator.mozLoop.getLoopBoolPref = function(pref) {
             if (pref === "rooms.enabled") {
               return false;
             }
--- a/browser/components/loop/test/mochitest/browser_toolbarbutton.js
+++ b/browser/components/loop/test/mochitest/browser_toolbarbutton.js
@@ -3,16 +3,17 @@
 
 /**
  * Test the toolbar button states.
  */
 
 "use strict";
 
 Components.utils.import("resource://gre/modules/Promise.jsm", this);
+const {LoopRoomsInternal} = Components.utils.import("resource:///modules/loop/LoopRooms.jsm", {});
 
 registerCleanupFunction(function*() {
   MozLoopService.doNotDisturb = false;
   MozLoopServiceInternal.fxAOAuthProfile = null;
   yield MozLoopServiceInternal.clearError("testing");
 });
 
 add_task(function* test_doNotDisturb() {
@@ -74,8 +75,19 @@ add_task(function* test_active() {
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state after opening panel");
   let loopPanel = document.getElementById("loop-notification-panel");
   loopPanel.hidePopup();
   MozLoopServiceInternal.fxAOAuthTokenData = null;
   MozLoopServiceInternal.notifyStatusChanged();
   Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
 });
 
+add_task(function* test_room_participants() {
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
+  LoopRoomsInternal.rooms.set("test_room", {participants: [{displayName: "hugh", id: "008"}]});
+  MozLoopServiceInternal.notifyStatusChanged();
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "active", "Check button is in active state");
+  LoopRoomsInternal.rooms.set("test_room", {participants: []});
+  MozLoopServiceInternal.notifyStatusChanged();
+  Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
+  LoopRoomsInternal.rooms.delete("test_room");
+});
+
--- a/browser/components/loop/test/shared/mixins_test.js
+++ b/browser/components/loop/test/shared/mixins_test.js
@@ -161,9 +161,57 @@ describe("loop.shared.mixins", function(
       function() {
         setupFakeVisibilityEventDispatcher({target: {hidden: true}});
 
         comp = TestUtils.renderIntoDocument(TestComp());
 
         sinon.assert.calledOnce(onDocumentHiddenStub);
       });
   });
+
+  describe("loop.shared.mixins.AudioMixin", function() {
+    var view, fakeAudio, TestComp;
+
+    beforeEach(function() {
+      navigator.mozLoop = {
+        doNotDisturb: true,
+        getAudioBlob: sinon.spy(function(name, callback) {
+          callback(null, new Blob([new ArrayBuffer(10)], {type: 'audio/ogg'}));
+        })
+      };
+
+      fakeAudio = {
+        play: sinon.spy(),
+        pause: sinon.spy(),
+        removeAttribute: sinon.spy()
+      };
+      sandbox.stub(window, "Audio").returns(fakeAudio);
+
+      TestComp = React.createClass({
+        mixins: [loop.shared.mixins.AudioMixin],
+        componentDidMount: function() {
+          this.play("failure");
+        },
+        render: function() {
+          return React.DOM.div();
+        }
+      });
+
+    });
+
+    it("should not play a failure sound when doNotDisturb true", function() {
+      view = TestUtils.renderIntoDocument(TestComp());
+      sinon.assert.notCalled(navigator.mozLoop.getAudioBlob);
+      sinon.assert.notCalled(fakeAudio.play);
+    });
+
+    it("should play a failure sound, once", function() {
+      navigator.mozLoop.doNotDisturb = false;
+      view = TestUtils.renderIntoDocument(TestComp());
+      sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
+      sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
+                                     "failure", sinon.match.func);
+      sinon.assert.calledOnce(fakeAudio.play);
+      expect(fakeAudio.loop).to.equal(false);
+    });
+  });
+
 });
--- a/mobile/android/base/tabs/TabsLayoutItemView.java
+++ b/mobile/android/base/tabs/TabsLayoutItemView.java
@@ -7,16 +7,17 @@ package org.mozilla.gecko.tabs;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.widget.TabThumbnailWrapper;
 
 import android.content.Context;
 import android.graphics.Rect;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
+import android.util.TypedValue;
 import android.view.TouchDelegate;
 import android.view.View;
 import android.view.ViewTreeObserver;
 import android.widget.Checkable;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
@@ -88,22 +89,31 @@ public class TabsLayoutItemView extends 
         mCloseButton = (ImageButton) findViewById(R.id.close);
         mThumbnailWrapper = (TabThumbnailWrapper) findViewById(R.id.wrapper);
 
         getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
             @Override
             public boolean onPreDraw() {
                 getViewTreeObserver().removeOnPreDrawListener(this);
 
-                final Rect r = new Rect();
-                mCloseButton.getHitRect(r);
-                r.left -= 25;
-                r.bottom += 25;
+                final Rect hitRect = new Rect();
+                mCloseButton.getHitRect(hitRect);
+
+                // Ideally we want the close button hit area to be 40x40dp but we are constrained by the height of the parent, so
+                // we make it as tall as the parent view and 40dp across.
+                final int targetHitArea = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 40, getResources().getDisplayMetrics());;
+                final View parent = ((View) mCloseButton.getParent());
 
-                setTouchDelegate(new TouchDelegate(r, mCloseButton));
+
+                hitRect.top = 0;
+                hitRect.right = getWidth();
+                hitRect.left = getWidth() - targetHitArea;
+                hitRect.bottom = parent.getHeight();
+
+                setTouchDelegate(new TouchDelegate(hitRect, mCloseButton));
 
                 return true;
             }
         });
     }
 
     protected void assignValues(Tab tab)  {
         if (tab == null) {
--- a/testing/mochitest/jetpack-addon-harness.js
+++ b/testing/mochitest/jetpack-addon-harness.js
@@ -42,16 +42,19 @@ function installAddon(url) {
             profile: {
               memory: false,
               leaks: false,
             },
             output: {
               logLevel: "verbose",
               format: "tbpl",
             },
+            console: {
+              logLevel: "info",
+            },
           }
           setPrefs("extensions." + install.addon.id + ".sdk", options);
 
           // If necessary override the add-ons module paths to point somewhere
           // else
           if (sdkpath) {
             let paths = {}
             for (let path of ["dev", "diffpatcher", "framescript", "method", "node", "sdk", "toolkit"]) {
--- a/testing/mochitest/jetpack-package-harness.js
+++ b/testing/mochitest/jetpack-package-harness.js
@@ -151,16 +151,19 @@ function testInit() {
         profile: {
           memory: false,
           leaks: false,
         },
         output: {
           logLevel: "verbose",
           format: "tbpl",
         },
+        console: {
+          logLevel: "info",
+        },
       }
       setPrefs("extensions." + TEST_ID + ".sdk", options);
 
       // Override the SDK modules if necessary
       let sdkpath = "resource://gre/modules/commonjs/";
       try {
         let sdklibs = Services.prefs.getCharPref("extensions.sdk.path");
         // sdkpath is a file path, make it a URI and map a resource URI to it
--- a/toolkit/devtools/server/actors/highlighter.css
+++ b/toolkit/devtools/server/actors/highlighter.css
@@ -98,17 +98,17 @@
   content: "";
   display: none;
 
   position: absolute;
   left: calc(50% - 14px);
 
   height: 0;
   width: 0;
-  border: 14px solid hsl(210,2%,22%);
+  border: 14px solid hsl(214,13%,24%);
   border-left-color: transparent;
   border-right-color: transparent;
 }
 
 :-moz-native-anonymous .box-model-nodeinfobar-container[position="top"]:not([hide-arrow]) > .box-model-nodeinfobar:before {
   border-bottom: 0;
   top: 100%;
   display: block;
--- a/toolkit/themes/osx/global/toolbar.css
+++ b/toolkit/themes/osx/global/toolbar.css
@@ -9,17 +9,17 @@ toolbar {
   min-height: 20px;
   -moz-appearance: toolbar;
 }
 
 menubar:-moz-lwtheme,
 toolbar:-moz-lwtheme {
   -moz-appearance: none;
   background: none;
-  border-style: none;
+  border-color: transparent;
 }
 
 menubar {
   -moz-appearance: dialog; /* For content menubars, "toolbar" is too dark, so we use "dialog". */
   min-width: 1px;
 }
 
 .toolbar-primary {
--- a/toolkit/themes/windows/global/menu.css
+++ b/toolkit/themes/windows/global/menu.css
@@ -122,17 +122,17 @@ menubar > menu[_moz-menuactive="true"]:n
 }
 
 menubar > menu[_moz-menuactive="true"][open="true"] {
   border-width: 3px 1px 1px 3px;
 }
 
 menubar > menu:-moz-lwtheme {
   -moz-appearance: none;
-  border-style: none;
+  border-color: transparent;
 }
 
 menubar > menu:-moz-lwtheme:not([disabled="true"]) {
   color: inherit !important;
 }
 
 menubar > menu:-moz-lwtheme[_moz-menuactive="true"]:not([disabled="true"]) {
   background-color: Highlight;
--- a/toolkit/themes/windows/global/toolbar.css
+++ b/toolkit/themes/windows/global/toolbar.css
@@ -38,17 +38,17 @@ toolbar:first-child, menubar {
 
 /* ::::: lightweight theme ::::: */
  
 menubar:-moz-lwtheme,
 toolbox:-moz-lwtheme,
 toolbar:-moz-lwtheme {
   -moz-appearance: none;
   background: none;
-  border-style: none;
+  border-color: transparent;
 }
 
 /* ::::: toolbar decorations ::::: */
 
 toolbarseparator {
   -moz-appearance: separator;
   border-top: 2px solid transparent;
   border-bottom: 2px solid transparent;