Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Fri, 31 Oct 2014 16:16:38 -0400
changeset 237747 04a87b6ff211eabb56fad2bba369176d4fa3c8be
parent 237732 12ac66e2c0161655f1728e675b3db9713501c3e5 (current diff)
parent 237746 6bcd67382275c4d8288d7df84e50e59900da861a (diff)
child 237748 23d70439fb8be661d83a02be40307fe57ef7e68e
child 237773 0510281753433218960218bf8e14770a22a21e8b
child 237833 539bb29a6a0faf44f1b050bda17b84ad0014029d
child 237854 cb1711e891162b7b7a6f6750fb3defbe0ea94441
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
browser/devtools/performance/performance.js
--- a/browser/base/content/browser-fullScreen.js
+++ b/browser/base/content/browser-fullScreen.js
@@ -87,16 +87,18 @@ var FullScreen = {
       // The user may quit fullscreen during an animation
       this._cancelAnimation();
       gNavToolbox.style.marginTop = "";
       if (this._isChromeCollapsed)
         this.mouseoverToggle(true);
       // This is needed if they use the context menu to quit fullscreen
       this._isPopupOpen = false;
 
+      document.documentElement.removeAttribute("inDOMFullscreen");
+
       this.cleanup();
     }
   },
 
   exitDomFullScreen : function() {
     document.mozCancelFullScreen();
   },
 
@@ -151,19 +153,17 @@ var FullScreen = {
     let focusManager = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
     if (focusManager.activeWindow != window) {
       // The top-level window has lost focus since the request to enter
       // full-screen was made. Cancel full-screen.
       document.mozCancelFullScreen();
       return;
     }
 
-    // Ensure the sidebar is hidden.
-    if (!document.getElementById("sidebar-box").hidden)
-      toggleSidebar();
+    document.documentElement.setAttribute("inDOMFullscreen", true);
 
     if (gFindBarInitialized)
       gFindBar.close();
 
     this.showWarning(aOrigin);
 
     // Exit DOM full-screen mode upon open, close, or change tab.
     gBrowser.tabContainer.addEventListener("TabOpen", this.exitDomFullScreen);
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -251,19 +251,24 @@ toolbar[customizing] > .overflow-button 
 %ifdef XP_WIN
 #main-window[sizemode="maximized"] #titlebar-buttonbox {
   -moz-appearance: -moz-window-button-box-maximized;
 }
 %endif
 
 %endif
 
-#main-window[inFullscreen] #global-notificationbox,
-#main-window[inFullscreen] #high-priority-global-notificationbox {
-  visibility: collapse;
+#main-window[inDOMFullscreen] #sidebar-box,
+#main-window[inDOMFullscreen] #sidebar-splitter {
+  visibility: collapse;
+}
+
+#main-window[inFullscreen] #global-notificationbox,
+#main-window[inFullscreen] #high-priority-global-notificationbox {
+  visibility: collapse;
 }
 
 /* Rules to help integrate SDK widgets */
 toolbaritem[sdkstylewidget="true"] > toolbarbutton,
 toolbarpaletteitem > toolbaritem[sdkstylewidget="true"] > iframe,
 toolbarpaletteitem > toolbaritem[sdkstylewidget="true"] > .toolbarbutton-text {
   display: none;
 }
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -673,17 +673,21 @@
            Should you need to add items to the toolbar here, make sure to also add them
            to the default placements of buttons in CustomizableUI.jsm, so the
            customization code doesn't get confused.
       -->
     <toolbar id="nav-bar" class="toolbar-primary chromeclass-toolbar"
              aria-label="&navbarCmd.label;"
              fullscreentoolbar="true" mode="icons" customizable="true"
              iconsize="small"
+#ifdef MOZ_DEV_EDITION
+             defaultset="urlbar-container,search-container,developer-button,bookmarks-menu-button,downloads-button,home-button,loop-call-button"
+#else
              defaultset="urlbar-container,search-container,bookmarks-menu-button,downloads-button,home-button,loop-call-button"
+#endif
              customizationtarget="nav-bar-customization-target"
              overflowable="true"
              overflowbutton="nav-bar-overflow-button"
              overflowtarget="widget-overflow-list"
              overflowpanel="widget-overflow"
              context="toolbar-context-menu">
 
       <hbox id="nav-bar-customization-target" flex="1">
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -170,17 +170,19 @@ let CustomizableUIInternal = {
       "privatebrowsing-button",
       "save-page-button",
       "print-button",
       "history-panelmenu",
       "fullscreen-button",
       "find-button",
       "preferences-button",
       "add-ons-button",
+#ifndef MOZ_DEV_EDITION
       "developer-button",
+#endif
     ];
 
     if (gPalette.has("switch-to-metro-button")) {
       panelPlacements.push("switch-to-metro-button");
     }
 
 #ifdef E10S_TESTING_ONLY
     if (gPalette.has("e10s-button")) {
@@ -204,16 +206,19 @@ let CustomizableUIInternal = {
       type: CustomizableUI.TYPE_MENU_PANEL,
       defaultPlacements: panelPlacements
     }, true);
     PanelWideWidgetTracker.init();
 
     let navbarPlacements = [
       "urlbar-container",
       "search-container",
+#ifdef MOZ_DEV_EDITION
+      "developer-button",
+#endif
       "bookmarks-menu-button",
       "downloads-button",
       "home-button",
       "loop-call-button",
     ];
 
     if (Services.prefs.getBoolPref(kPrefWebIDEInNavbar)) {
       navbarPlacements.push("webide-button");
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -338,17 +338,21 @@ const CustomizableWidgets = [
       }
     }
   }, {
     id: "developer-button",
     type: "view",
     viewId: "PanelUI-developer",
     shortcutId: "key_devToolboxMenuItem",
     tooltiptext: "developer-button.tooltiptext2",
+#ifdef MOZ_DEV_EDITION
+    defaultArea: CustomizableUI.AREA_NAVBAR,
+#else
     defaultArea: CustomizableUI.AREA_PANEL,
+#endif
     onViewShowing: function(aEvent) {
       // Populate the subview with whatever menuitems are in the developer
       // menu. We skip menu elements, because the menu panel has no way
       // of dealing with those right now.
       let doc = aEvent.target.ownerDocument;
       let win = doc.defaultView;
 
       let menu = doc.getElementById("menuWebDeveloperPopup");
--- a/browser/components/customizableui/CustomizeMode.jsm
+++ b/browser/components/customizableui/CustomizeMode.jsm
@@ -1063,16 +1063,17 @@ CustomizeMode.prototype = {
       for (let target of this.areas) {
         for (let toolbarItem of target.children) {
           if (this.isWrappedToolbarItem(toolbarItem)) {
             yield this.deferredUnwrapToolbarItem(toolbarItem);
           }
         }
         this._removeDragHandlers(target);
       }
+      this.areas.clear();
     }.bind(this)).then(null, ERROR);
   },
 
   _removeExtraToolbarsIfEmpty: function() {
     let toolbox = this.window.gNavToolbox;
     for (let child of toolbox.children) {
       if (child.hasAttribute("customindex")) {
         let placements = CustomizableUI.getWidgetIdsInArea(child.id);
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -143,10 +143,14 @@ skip-if = e10s # Bug 1088710
 [browser_995164_registerArea_during_customize_mode.js]
 [browser_996364_registerArea_different_properties.js]
 [browser_996635_remove_non_widgets.js]
 [browser_1003588_no_specials_in_panel.js]
 [browser_1007336_lwthemes_in_customize_mode.js]
 [browser_1008559_anchor_undo_restore.js]
 [browser_1042100_default_placements_update.js]
 [browser_1058573_showToolbarsDropdown.js]
+[browser_1087303_button_fullscreen.js]
+skip-if = os == "mac"
+[browser_1087303_button_preferences.js]
 [browser_bootstrapped_custom_toolbar.js]
 [browser_panel_toggle.js]
+[browser_1089591_still_customizable_after_reset.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_1087303_button_fullscreen.js
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+  * License, v. 2.0. If a copy of the MPL was not distributed with this
+  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_task(function() {
+  info("Check fullscreen button existence and functionality");
+
+  yield PanelUI.show();
+
+  let fullscreenButton = document.getElementById("fullscreen-button");
+  ok(fullscreenButton, "Fullscreen button appears in Panel Menu");
+
+  let fullscreenPromise = promiseFullscreenChange();
+  fullscreenButton.click();
+  yield fullscreenPromise;
+
+  ok(window.fullScreen, "Fullscreen mode was opened");
+
+  // exit full screen mode
+  fullscreenPromise = promiseFullscreenChange();
+  window.fullScreen = !window.fullScreen;
+  yield fullscreenPromise;
+
+  ok(!window.fullScreen, "Successfully exited fullscreen");
+});
+
+function promiseFullscreenChange() {
+  let deferred = Promise.defer();
+  info("Wait for fullscreen change");
+
+  let timeoutId = setTimeout(() => {
+    window.removeEventListener("fullscreen", onFullscreenChange, true);
+    deferred.reject("Fullscreen change did not happen within " + 20000 + "ms");
+  }, 20000);
+
+  function onFullscreenChange(event) {
+    clearTimeout(timeoutId);
+    window.removeEventListener("fullscreen", onFullscreenChange, true);
+    info("Fullscreen event received");
+    deferred.resolve();
+  }
+  window.addEventListener("fullscreen", onFullscreenChange, true);
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_1087303_button_preferences.js
@@ -0,0 +1,57 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+  * License, v. 2.0. If a copy of the MPL was not distributed with this
+  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const PREF_INCONTENT = "browser.preferences.inContent";
+
+let newTab = null;
+
+add_task(function() {
+  info("Check preferences button existence and functionality");
+
+  Services.prefs.setBoolPref(PREF_INCONTENT, true);
+
+  yield PanelUI.show();
+  info("Menu panel was opened");
+
+  let preferencesButton = document.getElementById("preferences-button");
+  ok(preferencesButton, "Preferences button exists in Panel Menu");
+  preferencesButton.click();
+
+  newTab = gBrowser.selectedTab;
+  yield waitForPageLoad(newTab);
+
+  let openedPage = gBrowser.currentURI.spec;
+  is(openedPage, "about:preferences", "Preferences page was opened");
+});
+
+add_task(function asyncCleanup() {
+  if (gBrowser.tabs.length == 1)
+    gBrowser.addTab("about:blank");
+
+  gBrowser.removeTab(gBrowser.selectedTab);
+  info("Tabs were restored");
+
+  // restore the browser.preferences.inContent preference
+  Services.prefs.clearUserPref(PREF_INCONTENT);
+});
+
+function waitForPageLoad(aTab) {
+  let deferred = Promise.defer();
+
+  let timeoutId = setTimeout(() => {
+    aTab.linkedBrowser.removeEventListener("load", onTabLoad, true);
+    deferred.reject("Page didn't load within " + 20000 + "ms");
+  }, 20000);
+
+  function onTabLoad(event) {
+    clearTimeout(timeoutId);
+    aTab.linkedBrowser.removeEventListener("load", onTabLoad, true);
+    info("Tab event received: " + "load");
+    deferred.resolve();
+ }
+  aTab.linkedBrowser.addEventListener("load", onTabLoad, true, true);
+  return deferred.promise;
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_1089591_still_customizable_after_reset.js
@@ -0,0 +1,25 @@
+"use strict";
+
+// Dragging the elements again after a reset should work
+add_task(function* () {
+  yield startCustomizing();
+  let historyButton = document.getElementById("wrapper-history-panelmenu");
+  let devButton = document.getElementById("wrapper-developer-button");
+
+  ok(historyButton && devButton, "Draggable elements should exist");
+  simulateItemDrag(historyButton, devButton);
+  gCustomizeMode.reset();
+  yield waitForCondition(() => !gCustomizeMode.resetting);
+  ok(CustomizableUI.inDefaultState, "Should be back in default state");
+
+  historyButton = document.getElementById("wrapper-history-panelmenu");
+  devButton = document.getElementById("wrapper-developer-button");
+  ok(historyButton && devButton, "Draggable elements should exist");
+  simulateItemDrag(historyButton, devButton);
+
+  yield endCustomizing();
+});
+
+add_task(function* asyncCleanup() {
+  yield resetCustomization();
+});
--- a/browser/components/customizableui/test/browser_880382_drag_wide_widgets_in_panel.js
+++ b/browser/components/customizableui/test/browser_880382_drag_wide_widgets_in_panel.js
@@ -18,16 +18,17 @@ add_task(function() {
                              "zoom-controls",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button"];
+  removeDeveloperButtonIfDevEdition(placementsAfterMove);
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(zoomControls, printButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let newWindowButton = document.getElementById("new-window-button");
   simulateItemDrag(zoomControls, newWindowButton);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
@@ -44,16 +45,17 @@ add_task(function() {
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button"];
+  removeDeveloperButtonIfDevEdition(placementsAfterMove);
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(zoomControls, savePageButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(CustomizableUI.inDefaultState, "Should be in default state.");
 });
 
 
 // Dragging the zoom controls to be before the new-window button should not move any widgets.
@@ -68,16 +70,17 @@ add_task(function() {
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button"];
+  removeDeveloperButtonIfDevEdition(placementsAfterMove);
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(zoomControls, newWindowButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
 // Dragging the zoom controls to be before the history-panelmenu should move the zoom-controls in to the row higher than the history-panelmenu.
 add_task(function() {
@@ -91,16 +94,17 @@ add_task(function() {
                              "zoom-controls",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button"];
+  removeDeveloperButtonIfDevEdition(placementsAfterMove);
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(zoomControls, historyPanelMenu);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let newWindowButton = document.getElementById("new-window-button");
   simulateItemDrag(zoomControls, newWindowButton);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
@@ -118,16 +122,17 @@ add_task(function() {
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "zoom-controls",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button"];
+  removeDeveloperButtonIfDevEdition(placementsAfterMove);
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(zoomControls, preferencesButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let newWindowButton = document.getElementById("new-window-button");
   simulateItemDrag(zoomControls, newWindowButton);
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
@@ -145,16 +150,17 @@ add_task(function() {
                                "save-page-button",
                                "print-button",
                                "history-panelmenu",
                                "fullscreen-button",
                                "find-button",
                                "preferences-button",
                                "add-ons-button",
                                "developer-button"];
+  removeDeveloperButtonIfDevEdition(placementsAfterInsert);
   addSwitchToMetroButtonInWindows8(placementsAfterInsert);
   simulateItemDrag(openFileButton, zoomControls);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterInsert);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let palette = document.getElementById("customization-palette");
   // Check that the palette items are re-wrapped correctly.
   let feedWrapper = document.getElementById("wrapper-feed-button");
   let feedButton = document.getElementById("feed-button");
@@ -184,16 +190,17 @@ add_task(function() {
                                "save-page-button",
                                "print-button",
                                "history-panelmenu",
                                "fullscreen-button",
                                "find-button",
                                "preferences-button",
                                "add-ons-button",
                                "developer-button"];
+  removeDeveloperButtonIfDevEdition(placementsAfterInsert);
   addSwitchToMetroButtonInWindows8(placementsAfterInsert);
   simulateItemDrag(openFileButton, editControls);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterInsert);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   let palette = document.getElementById("customization-palette");
   // Check that the palette items are re-wrapped correctly.
   let feedWrapper = document.getElementById("wrapper-feed-button");
   let feedButton = document.getElementById("feed-button");
@@ -220,16 +227,17 @@ add_task(function() {
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button"];
+  removeDeveloperButtonIfDevEdition(placementsAfterMove);
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(editControls, zoomControls);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
 // Dragging the edit-controls to be before the new-window-button should
 // move the zoom-controls before the edit-controls.
@@ -244,16 +252,17 @@ add_task(function() {
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button"];
+  removeDeveloperButtonIfDevEdition(placementsAfterMove);
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(editControls, newWindowButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(editControls, zoomControls);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
@@ -271,16 +280,17 @@ add_task(function() {
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button"];
+  removeDeveloperButtonIfDevEdition(placementsAfterMove);
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(editControls, privateBrowsingButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(editControls, zoomControls);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
@@ -298,16 +308,17 @@ add_task(function() {
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button"];
+  removeDeveloperButtonIfDevEdition(placementsAfterMove);
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(editControls, savePageButton);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(editControls, zoomControls);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
@@ -324,16 +335,17 @@ add_task(function() {
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "edit-controls",
                              "developer-button"];
+  removeDeveloperButtonIfDevEdition(placementsAfterMove);
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(editControls, panel);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(editControls, zoomControls);
   ok(CustomizableUI.inDefaultState, "Should still be in default state.");
 });
 
@@ -349,16 +361,17 @@ add_task(function() {
                              "save-page-button",
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "developer-button"];
+  removeDeveloperButtonIfDevEdition(placementsAfterMove);
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   let paletteChildElementCount = palette.childElementCount;
   simulateItemDrag(editControls, palette);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   is(paletteChildElementCount + 1, palette.childElementCount,
      "The palette should have a new child, congratulations!");
   is(editControls.parentNode.id, "wrapper-edit-controls",
      "The edit-controls should be properly wrapped.");
@@ -389,16 +402,17 @@ add_task(function() {
                                "print-button",
                                "history-panelmenu",
                                "fullscreen-button",
                                "find-button",
                                "preferences-button",
                                "add-ons-button",
                                "edit-controls",
                                "developer-button"];
+    removeDeveloperButtonIfDevEdition(placementsAfterMove);
     addSwitchToMetroButtonInWindows8(placementsAfterMove);
     simulateItemDrag(editControls, placeholder);
     assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
     let zoomControls = document.getElementById("zoom-controls");
     simulateItemDrag(editControls, zoomControls);
     ok(CustomizableUI.inDefaultState, "Should still be in default state.");
   }
 });
@@ -431,16 +445,17 @@ add_task(function() {
                              "print-button",
                              "history-panelmenu",
                              "fullscreen-button",
                              "find-button",
                              "preferences-button",
                              "add-ons-button",
                              "edit-controls",
                              "developer-button"];
+  removeDeveloperButtonIfDevEdition(placementsAfterMove);
   addSwitchToMetroButtonInWindows8(placementsAfterMove);
   simulateItemDrag(editControls, target);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove);
   let itemToDrag = "sync-button";
   let button = document.getElementById(itemToDrag);
   placementsAfterMove.splice(11, 0, itemToDrag);
   if (isInWin8()) {
     placementsAfterMove[10] = placementsAfterMove[11];
--- a/browser/components/customizableui/test/browser_890140_orphaned_placeholders.js
+++ b/browser/components/customizableui/test/browser_890140_orphaned_placeholders.js
@@ -4,76 +4,106 @@
 
 "use strict";
 
 requestLongerTimeout(2);
 
 // One orphaned item should have two placeholders next to it.
 add_task(function() {
   yield startCustomizing();
-  let btn = document.getElementById("open-file-button");
-  let panel = document.getElementById(CustomizableUI.AREA_PANEL);
-  let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
 
   if (isInWin8()) {
     CustomizableUI.removeWidgetFromArea("switch-to-metro-button");
-    placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
     ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
+  }
+  if (isInDevEdition()) {
+    CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_PANEL);
+    ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
+  }
+  if (!isInWin8() && !isInDevEdition()) {
+    ok(CustomizableUI.inDefaultState, "Should be in default state.");
   } else {
-    ok(CustomizableUI.inDefaultState, "Should be in default state.");
+    ok(!CustomizableUI.inDefaultState, "Should not be in default state if on Win8 or DevEdition.");
   }
 
+  let btn = document.getElementById("open-file-button");
+  let panel = document.getElementById(CustomizableUI.AREA_PANEL);
+  let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
+
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placements);
   is(getVisiblePlaceholderCount(panel), 2, "Should only have 2 visible placeholders before exiting");
 
   yield endCustomizing();
   yield startCustomizing();
   is(getVisiblePlaceholderCount(panel), 2, "Should only have 2 visible placeholders after re-entering");
 
   if (isInWin8()) {
     CustomizableUI.addWidgetToArea("switch-to-metro-button", CustomizableUI.AREA_PANEL);
   }
+  if (isInDevEdition()) {
+    CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_NAVBAR, 2);
+  }
+
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
 // Two orphaned items should have one placeholder next to them (case 1).
 add_task(function() {
   yield startCustomizing();
+
+  if (isInDevEdition()) {
+    CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_PANEL);
+  }
+
   let btn = document.getElementById("open-file-button");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
-
   let placementsAfterAppend = placements;
 
   if (!isInWin8()) {
     placementsAfterAppend = placements.concat(["open-file-button"]);
     simulateItemDrag(btn, panel);
   }
 
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterAppend);
-  is(CustomizableUI.inDefaultState, isInWin8(), "Should only be in default state if on Win8");
+
+  if (isInWin8() && !isInDevEdition()) {
+    ok(CustomizableUI.inDefaultState, "Should be in default state if on Win8 and not on DevEdition.");
+  } else {
+    ok(!CustomizableUI.inDefaultState, "Should not be in default state if not Win8 or on DevEdition.");
+  }
+
   is(getVisiblePlaceholderCount(panel), 1, "Should only have 1 visible placeholder before exiting");
 
   yield endCustomizing();
   yield startCustomizing();
   is(getVisiblePlaceholderCount(panel), 1, "Should only have 1 visible placeholder after re-entering");
 
   let palette = document.getElementById("customization-palette");
   simulateItemDrag(btn, palette);
 
   if (!isInWin8()) {
     btn = document.getElementById("open-file-button");
     simulateItemDrag(btn, palette);
   }
+  if (isInDevEdition()) {
+    CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_NAVBAR, 2);
+  }
+
   ok(CustomizableUI.inDefaultState, "Should be in default state again."); 
 });
 
 // Two orphaned items should have one placeholder next to them (case 2).
 add_task(function() {
   yield startCustomizing();
+
+  if (isInDevEdition()) {
+    CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_PANEL);
+  }
+
   let btn = document.getElementById("add-ons-button");
   let btn2 = document.getElementById("developer-button");
   let btn3 = document.getElementById("switch-to-metro-button");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let palette = document.getElementById("customization-palette");
   let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
 
   let placementsAfterAppend = placements.filter(p => p != btn.id && p != btn2.id);
@@ -96,60 +126,93 @@ add_task(function() {
   simulateItemDrag(btn, panel);
   simulateItemDrag(btn2, panel);
 
   if (isInWin8()) {
     simulateItemDrag(btn3, panel);
   }
 
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placements);
+
+  if (isInDevEdition()) {
+    CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_NAVBAR, 2);
+  }
+
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
 // A wide widget at the bottom of the panel should have three placeholders after it.
 add_task(function() {
   yield startCustomizing();
+
+  if (isInDevEdition()) {
+    CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_PANEL);
+  }
+
   let btn = document.getElementById("edit-controls");
-  let developerButton = document.getElementById("developer-button");
-  let metroBtn = document.getElementById("switch-to-metro-button");
+  let btn2 = document.getElementById("developer-button");
+  let btn3 = document.getElementById("switch-to-metro-button");
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   let palette = document.getElementById("customization-palette");
   let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL);
 
   placements.pop();
-  simulateItemDrag(developerButton, palette);
+  simulateItemDrag(btn2, palette);
+
   if (isInWin8()) {
     // Remove switch-to-metro-button
     placements.pop();
-    simulateItemDrag(metroBtn, palette);
+    simulateItemDrag(btn3, palette);
   }
 
   let placementsAfterAppend = placements.concat([placements.shift()]);
   simulateItemDrag(btn, panel);
   assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterAppend);
   ok(!CustomizableUI.inDefaultState, "Should no longer be in default state.");
   is(getVisiblePlaceholderCount(panel), 3, "Should have 3 visible placeholders before exiting");
 
   yield endCustomizing();
   yield startCustomizing();
   is(getVisiblePlaceholderCount(panel), 3, "Should have 3 visible placeholders after re-entering");
 
-  simulateItemDrag(developerButton, panel);
+  simulateItemDrag(btn2, panel);
+
   if (isInWin8()) {
-    simulateItemDrag(metroBtn, panel);
+    simulateItemDrag(btn3, panel);
   }
+
   let zoomControls = document.getElementById("zoom-controls");
   simulateItemDrag(btn, zoomControls);
+
+  if (isInDevEdition()) {
+    CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_NAVBAR, 2);
+  }
+
   ok(CustomizableUI.inDefaultState, "Should be in default state again.");
 });
 
 // The default placements should have two placeholders at the bottom (or 1 in win8).
 add_task(function() {
   yield startCustomizing();
-  let numPlaceholders = isInWin8() ? 1 : 2;
+  let numPlaceholders = -1;
+
+  if (isInWin8()) {
+    if (isInDevEdition()) {
+      numPlaceholders = 2;
+    } else {
+      numPlaceholders = 1;
+    }
+  } else {
+    if (isInDevEdition()) {
+      numPlaceholders = 3;
+    } else {
+      numPlaceholders = 2;
+    }
+  }
+
   let panel = document.getElementById(CustomizableUI.AREA_PANEL);
   ok(CustomizableUI.inDefaultState, "Should be in default state.");
   is(getVisiblePlaceholderCount(panel), numPlaceholders, "Should have " + numPlaceholders + " visible placeholders before exiting");
 
   yield endCustomizing();
   yield startCustomizing();
   is(getVisiblePlaceholderCount(panel), numPlaceholders, "Should have " + numPlaceholders + " visible placeholders after re-entering");
 
--- a/browser/components/customizableui/test/head.js
+++ b/browser/components/customizableui/test/head.js
@@ -108,16 +108,30 @@ function removeCustomToolbars() {
 function getToolboxCustomToolbarId(toolbarName) {
   return "__customToolbar_" + toolbarName.replace(" ", "_");
 }
 
 function resetCustomization() {
   return CustomizableUI.reset();
 }
 
+XPCOMUtils.defineLazyGetter(this, 'gDeveloperButtonInNavbar', function() {
+  return getAreaWidgetIds(CustomizableUI.AREA_NAVBAR).indexOf("developer-button") != -1;
+});
+
+function isInDevEdition() {
+  return gDeveloperButtonInNavbar;
+}
+
+function removeDeveloperButtonIfDevEdition(areaPanelPlacements) {
+  if (isInDevEdition()) {
+    areaPanelPlacements.splice(areaPanelPlacements.indexOf("developer-button"), 1);
+  }
+}
+
 function isInWin8() {
   if (!Services.metro)
     return false;
   return Services.metro.supported;
 }
 
 function addSwitchToMetroButtonInWindows8(areaPanelPlacements) {
   if (isInWin8()) {
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -492,17 +492,17 @@ loop.panel = (function(_, mozL10n) {
     },
 
     handleMouseLeave: function(event) {
       this.setState({urlCopied: false});
     },
 
     _isActive: function() {
       // XXX bug 1074679 will implement this properly
-      return this.props.room.currSize > 0;
+      return this.props.room.participants.length > 0;
     },
 
     render: function() {
       var room = this.props.room;
       var roomClasses = React.addons.classSet({
         "room-entry": true,
         "room-active": this._isActive()
       });
@@ -533,67 +533,84 @@ loop.panel = (function(_, mozL10n) {
    * Room list.
    */
   var RoomList = React.createClass({displayName: 'RoomList',
     mixins: [Backbone.Events],
 
     propTypes: {
       store: React.PropTypes.instanceOf(loop.store.RoomListStore).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      rooms: React.PropTypes.array
+      userDisplayName: React.PropTypes.string.isRequired  // for room creation
     },
 
     getInitialState: function() {
-      var storeState = this.props.store.getStoreState();
-      return {
-        error: this.props.error || storeState.error,
-        rooms: this.props.rooms || storeState.rooms,
-      };
+      return this.props.store.getStoreState();
     },
 
-    componentWillMount: function() {
-      this.listenTo(this.props.store, "change", this._onRoomListChanged);
+    componentDidMount: function() {
+      this.listenTo(this.props.store, "change", this._onStoreStateChanged);
 
+      // XXX this should no longer be necessary once have a better mechanism
+      // for updating the list (possibly as part of the content side of bug
+      // 1074665.
       this.props.dispatcher.dispatch(new sharedActions.GetAllRooms());
     },
 
     componentWillUnmount: function() {
       this.stopListening(this.props.store);
     },
 
-    _onRoomListChanged: function() {
+    _onStoreStateChanged: function() {
       this.setState(this.props.store.getStoreState());
     },
 
     _getListHeading: function() {
       var numRooms = this.state.rooms.length;
       if (numRooms === 0) {
         return mozL10n.get("rooms_list_no_current_conversations");
       }
       return mozL10n.get("rooms_list_current_conversations", {num: numRooms});
     },
 
+    _hasPendingOperation: function() {
+      return this.state.pendingCreation || this.state.pendingInitialRetrieval;
+    },
+
+    handleCreateButtonClick: function() {
+      this.props.dispatcher.dispatch(new sharedActions.CreateRoom({
+        nameTemplate: mozL10n.get("rooms_default_room_name_template"),
+        roomOwner: this.props.userDisplayName
+      }));
+    },
+
     openRoom: function(room) {
       // XXX implement me; see bug 1074678
     },
 
     render: function() {
       if (this.state.error) {
         // XXX Better end user reporting of errors.
-        console.error(this.state.error);
+        console.error("RoomList error", this.state.error);
       }
 
       return (
-        React.DOM.div({className: "room-list"}, 
+        React.DOM.div({className: "rooms"}, 
           React.DOM.h1(null, this._getListHeading()), 
-          
+          React.DOM.div({className: "room-list"}, 
             this.state.rooms.map(function(room, i) {
               return RoomEntry({key: i, room: room, openRoom: this.openRoom});
             }, this)
-          
+          ), 
+          React.DOM.p(null, 
+            React.DOM.button({className: "btn btn-info", 
+                    onClick: this.handleCreateButtonClick, 
+                    disabled: this._hasPendingOperation()}, 
+              mozL10n.get("rooms_new_room_button_label")
+            )
+          )
         )
       );
     }
   });
 
   /**
    * Panel view.
    */
@@ -662,17 +679,18 @@ loop.panel = (function(_, mozL10n) {
      */
     _renderRoomsTab: function() {
       if (!navigator.mozLoop.getLoopBoolPref("rooms.enabled")) {
         return null;
       }
       return (
         Tab({name: "rooms"}, 
           RoomList({dispatcher: this.props.dispatcher, 
-                    store: this.props.roomListStore})
+                    store: this.props.roomListStore, 
+                    userDisplayName: this._getUserDisplayName()})
         )
       );
     },
 
     startForm: function(name, contact) {
       this.refs[name].initForm(contact);
       this.selectTab(name);
     },
@@ -688,20 +706,23 @@ loop.panel = (function(_, mozL10n) {
     componentDidMount: function() {
       window.addEventListener("LoopStatusChanged", this._onStatusChanged);
     },
 
     componentWillUnmount: function() {
       window.removeEventListener("LoopStatusChanged", this._onStatusChanged);
     },
 
+    _getUserDisplayName: function() {
+      return this.state.userProfile && this.state.userProfile.email ||
+             __("display_name_guest");
+    },
+
     render: function() {
       var NotificationListView = sharedViews.NotificationListView;
-      var displayName = this.state.userProfile && this.state.userProfile.email ||
-                        __("display_name_guest");
       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"}, 
@@ -726,17 +747,17 @@ loop.panel = (function(_, mozL10n) {
             ), 
             Tab({name: "contacts_import", hidden: true}, 
               ContactDetailsForm({ref: "contacts_import", mode: "import", 
                                   selectTab: this.selectTab})
             )
           ), 
           React.DOM.div({className: "footer"}, 
             React.DOM.div({className: "user-details"}, 
-              UserIdentity({displayName: displayName}), 
+              UserIdentity({displayName: this._getUserDisplayName()}), 
               AvailabilityDropdown(null)
             ), 
             React.DOM.div({className: "signin-details"}, 
               AuthLink(null), 
               React.DOM.div({className: "footer-signin-separator"}), 
               SettingsDropdown(null)
             )
           )
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -492,17 +492,17 @@ loop.panel = (function(_, mozL10n) {
     },
 
     handleMouseLeave: function(event) {
       this.setState({urlCopied: false});
     },
 
     _isActive: function() {
       // XXX bug 1074679 will implement this properly
-      return this.props.room.currSize > 0;
+      return this.props.room.participants.length > 0;
     },
 
     render: function() {
       var room = this.props.room;
       var roomClasses = React.addons.classSet({
         "room-entry": true,
         "room-active": this._isActive()
       });
@@ -533,67 +533,84 @@ loop.panel = (function(_, mozL10n) {
    * Room list.
    */
   var RoomList = React.createClass({
     mixins: [Backbone.Events],
 
     propTypes: {
       store: React.PropTypes.instanceOf(loop.store.RoomListStore).isRequired,
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
-      rooms: React.PropTypes.array
+      userDisplayName: React.PropTypes.string.isRequired  // for room creation
     },
 
     getInitialState: function() {
-      var storeState = this.props.store.getStoreState();
-      return {
-        error: this.props.error || storeState.error,
-        rooms: this.props.rooms || storeState.rooms,
-      };
+      return this.props.store.getStoreState();
     },
 
-    componentWillMount: function() {
-      this.listenTo(this.props.store, "change", this._onRoomListChanged);
+    componentDidMount: function() {
+      this.listenTo(this.props.store, "change", this._onStoreStateChanged);
 
+      // XXX this should no longer be necessary once have a better mechanism
+      // for updating the list (possibly as part of the content side of bug
+      // 1074665.
       this.props.dispatcher.dispatch(new sharedActions.GetAllRooms());
     },
 
     componentWillUnmount: function() {
       this.stopListening(this.props.store);
     },
 
-    _onRoomListChanged: function() {
+    _onStoreStateChanged: function() {
       this.setState(this.props.store.getStoreState());
     },
 
     _getListHeading: function() {
       var numRooms = this.state.rooms.length;
       if (numRooms === 0) {
         return mozL10n.get("rooms_list_no_current_conversations");
       }
       return mozL10n.get("rooms_list_current_conversations", {num: numRooms});
     },
 
+    _hasPendingOperation: function() {
+      return this.state.pendingCreation || this.state.pendingInitialRetrieval;
+    },
+
+    handleCreateButtonClick: function() {
+      this.props.dispatcher.dispatch(new sharedActions.CreateRoom({
+        nameTemplate: mozL10n.get("rooms_default_room_name_template"),
+        roomOwner: this.props.userDisplayName
+      }));
+    },
+
     openRoom: function(room) {
       // XXX implement me; see bug 1074678
     },
 
     render: function() {
       if (this.state.error) {
         // XXX Better end user reporting of errors.
-        console.error(this.state.error);
+        console.error("RoomList error", this.state.error);
       }
 
       return (
-        <div className="room-list">
+        <div className="rooms">
           <h1>{this._getListHeading()}</h1>
-          {
+          <div className="room-list">{
             this.state.rooms.map(function(room, i) {
               return <RoomEntry key={i} room={room} openRoom={this.openRoom} />;
             }, this)
-          }
+          }</div>
+          <p>
+            <button className="btn btn-info"
+                    onClick={this.handleCreateButtonClick}
+                    disabled={this._hasPendingOperation()}>
+              {mozL10n.get("rooms_new_room_button_label")}
+            </button>
+          </p>
         </div>
       );
     }
   });
 
   /**
    * Panel view.
    */
@@ -662,17 +679,18 @@ loop.panel = (function(_, mozL10n) {
      */
     _renderRoomsTab: function() {
       if (!navigator.mozLoop.getLoopBoolPref("rooms.enabled")) {
         return null;
       }
       return (
         <Tab name="rooms">
           <RoomList dispatcher={this.props.dispatcher}
-                    store={this.props.roomListStore} />
+                    store={this.props.roomListStore}
+                    userDisplayName={this._getUserDisplayName()}/>
         </Tab>
       );
     },
 
     startForm: function(name, contact) {
       this.refs[name].initForm(contact);
       this.selectTab(name);
     },
@@ -688,20 +706,23 @@ loop.panel = (function(_, mozL10n) {
     componentDidMount: function() {
       window.addEventListener("LoopStatusChanged", this._onStatusChanged);
     },
 
     componentWillUnmount: function() {
       window.removeEventListener("LoopStatusChanged", this._onStatusChanged);
     },
 
+    _getUserDisplayName: function() {
+      return this.state.userProfile && this.state.userProfile.email ||
+             __("display_name_guest");
+    },
+
     render: function() {
       var NotificationListView = sharedViews.NotificationListView;
-      var displayName = this.state.userProfile && this.state.userProfile.email ||
-                        __("display_name_guest");
       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">
@@ -726,17 +747,17 @@ loop.panel = (function(_, mozL10n) {
             </Tab>
             <Tab name="contacts_import" hidden={true}>
               <ContactDetailsForm ref="contacts_import" mode="import"
                                   selectTab={this.selectTab}/>
             </Tab>
           </TabView>
           <div className="footer">
             <div className="user-details">
-              <UserIdentity displayName={displayName} />
+              <UserIdentity displayName={this._getUserDisplayName()} />
               <AvailabilityDropdown />
             </div>
             <div className="signin-details">
               <AuthLink />
               <div className="footer-signin-separator" />
               <SettingsDropdown />
             </div>
           </div>
--- a/browser/components/loop/content/shared/css/panel.css
+++ b/browser/components/loop/content/shared/css/panel.css
@@ -134,27 +134,47 @@ body {
 }
 
 .content-area input:not(.pristine):invalid {
   border-color: #d74345;
   box-shadow: 0 0 4px #c43c3e;
 }
 
 /* Rooms */
-.room-list {
+.rooms {
   background: #f5f5f5;
+  min-height: 100px;
 }
 
-.room-list > h1 {
+.rooms > h1 {
   font-weight: bold;
   color: #999;
   padding: .5rem 1rem;
   border-bottom: 1px solid #ddd;
 }
 
+.rooms > p {
+  border-top: 1px solid #ddd;
+  padding: 1rem 0;
+  margin: 0;
+}
+
+.rooms > p > .btn {
+  display: block;
+  font-size: 1rem;
+  margin: 0 auto;
+  padding: .5rem 1rem;
+  border-radius: 3px;
+}
+
+.room-list {
+  max-height: 335px; /* XXX better computation needed */
+  overflow: scroll;
+}
+
 .room-list > .room-entry {
   padding: 1rem 1rem 0 .5rem;
 }
 
 .room-list > .room-entry > h2 {
   font-size: .85rem;
   color: #777;
 }
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -123,28 +123,47 @@ loop.shared.actions = (function() {
     SetMute: Action.define("setMute", {
       // The part of the stream to enable, e.g. "audio" or "video"
       type: String,
       // Whether or not to enable the stream.
       enabled: Boolean
     }),
 
     /**
+     * Creates a new room.
+     * XXX: should move to some roomActions module - refs bug 1079284
+     */
+    CreateRoom: Action.define("createRoom", {
+      // The localized template to use to name the new room
+      // (eg. "Conversation {{conversationLabel}}").
+      nameTemplate: String,
+      roomOwner: String
+    }),
+
+    /**
+     * Rooms creation error.
+     * XXX: should move to some roomActions module - refs bug 1079284
+     */
+    CreateRoomError: Action.define("createRoomError", {
+      error: Error
+    }),
+
+    /**
      * Retrieves room list.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     GetAllRooms: Action.define("getAllRooms", {
     }),
 
     /**
      * An error occured while trying to fetch the room list.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     GetAllRoomsError: Action.define("getAllRoomsError", {
-      error: String
+      error: Error
     }),
 
     /**
      * Updates room list.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     UpdateRoomList: Action.define("updateRoomList", {
       roomList: Array
@@ -153,11 +172,11 @@ loop.shared.actions = (function() {
     /**
      * Primes localRoomStore with roomLocalId, which triggers the EmptyRoomView
      * to do any necessary setup.
      *
      * XXX should move to localRoomActions module
      */
     SetupEmptyRoom: Action.define("setupEmptyRoom", {
       localRoomId: String
-    }),
+    })
   };
 })();
--- a/browser/components/loop/content/shared/js/roomListStore.js
+++ b/browser/components/loop/content/shared/js/roomListStore.js
@@ -52,107 +52,283 @@ loop.store = loop.store || {};
    *                                registering to consume actions.
    * - {mozLoop}         mozLoop    The MozLoop API object.
    *
    * @extends {Backbone.Events}
    * @param {Object} options Options object.
    */
   function RoomListStore(options) {
     options = options || {};
-    this.storeState = {error: null, rooms: []};
 
     if (!options.dispatcher) {
       throw new Error("Missing option dispatcher");
     }
-    this.dispatcher = options.dispatcher;
+    this._dispatcher = options.dispatcher;
 
     if (!options.mozLoop) {
       throw new Error("Missing option mozLoop");
     }
-    this.mozLoop = options.mozLoop;
+    this._mozLoop = options.mozLoop;
 
-    this.dispatcher.register(this, [
+    this._dispatcher.register(this, [
+      "createRoom",
+      "createRoomError",
       "getAllRooms",
       "getAllRoomsError",
       "openRoom",
       "updateRoomList"
     ]);
   }
 
   RoomListStore.prototype = _.extend({
     /**
-     * Retrieves current store state.
+     * Maximum size given to createRoom; only 2 is supported (and is
+     * always passed) because that's what the user-experience is currently
+     * designed and tested to handle.
+     * @type {Number}
+     */
+    maxRoomCreationSize: 2,
+
+    /**
+     * The number of hours for which the room will exist.
+     * @type {Number}
+     */
+    defaultExpiresIn: 5,
+
+    /**
+     * Internal store state representation.
+     * @type {Object}
+     * @see  #getStoreState
+     */
+    _storeState: {
+      error: null,
+      pendingCreation: false,
+      pendingInitialRetrieval: false,
+      rooms: []
+    },
+
+    /**
+     * Retrieves current store state. The returned state object holds the
+     * following properties:
+     *
+     * - {Boolean} pendingCreation         Pending room creation flag.
+     * - {Boolean} pendingInitialRetrieval Pending initial list retrieval flag.
+     * - {Array}   rooms                   The current room list.
+     * - {Error}   error                   Latest error encountered, if any.
      *
      * @return {Object}
      */
     getStoreState: function() {
-      return this.storeState;
+      return this._storeState;
+    },
+
+    /**
+     * Updates store state and trigger a "change" event.
+     *
+     * @param {Object} newState The new store state.
+     */
+    setStoreState: function(newState) {
+      for (var key in newState) {
+        this._storeState[key] = newState[key];
+      }
+      this.trigger("change");
+    },
+
+    /**
+     * Registers mozLoop.rooms events.
+     */
+    startListeningToRoomEvents: function() {
+      // Rooms event registration
+      this._mozLoop.rooms.on("add", this._onRoomAdded.bind(this));
+      this._mozLoop.rooms.on("update", this._onRoomUpdated.bind(this));
+      this._mozLoop.rooms.on("remove", this._onRoomRemoved.bind(this));
+    },
+
+    /**
+     * Local proxy helper to dispatch an action.
+     *
+     * @param {Action} action The action to dispatch.
+     */
+    _dispatchAction: function(action) {
+      this._dispatcher.dispatch(action);
     },
 
     /**
-     * Updates store states and trigger a "change" event.
+     * Updates current room list when a new room is available.
      *
-     * @param {Object} state The new store state.
+     * @param {String} eventName     The event name (unused).
+     * @param {Object} addedRoomData The added room data.
+     */
+    _onRoomAdded: function(eventName, addedRoomData) {
+      addedRoomData.participants = [];
+      addedRoomData.ctime = new Date().getTime();
+      this._dispatchAction(new sharedActions.UpdateRoomList({
+        roomList: this._storeState.rooms.concat(new Room(addedRoomData))
+      }));
+    },
+
+    /**
+     * Executed when a room is updated.
+     *
+     * @param {String} eventName       The event name (unused).
+     * @param {Object} updatedRoomData The updated room data.
      */
-    setStoreState: function(state) {
-      this.storeState = state;
-      this.trigger("change");
+    _onRoomUpdated: function(eventName, updatedRoomData) {
+      this._dispatchAction(new sharedActions.UpdateRoomList({
+        roomList: this._storeState.rooms.map(function(room) {
+          return room.roomToken === updatedRoomData.roomToken ?
+                 updatedRoomData : room;
+        })
+      }));
     },
 
     /**
+     * Executed when a room is removed.
+     *
+     * @param {String} eventName       The event name (unused).
+     * @param {Object} removedRoomData The removed room data.
+     */
+    _onRoomRemoved: function(eventName, removedRoomData) {
+      this._dispatchAction(new sharedActions.UpdateRoomList({
+        roomList: this._storeState.rooms.filter(function(room) {
+          return room.roomToken !== removedRoomData.roomToken;
+        })
+      }));
+    },
+
+
+    /**
      * Maps and sorts the raw room list received from the mozLoop API.
      *
      * @param  {Array} rawRoomList Raw room list.
      * @return {Array}
      */
-    _processRawRoomList: function(rawRoomList) {
+    _processRoomList: function(rawRoomList) {
       if (!rawRoomList) {
         return [];
       }
       return rawRoomList
         .map(function(rawRoom) {
           return new Room(rawRoom);
         })
         .slice()
         .sort(function(a, b) {
           return b.ctime - a.ctime;
         });
     },
 
     /**
+     * Finds the next available room number in the provided room list.
+     *
+     * @param  {String} nameTemplate The room name template; should contain a
+     *                               {{conversationLabel}} placeholder.
+     * @return {Number}
+     */
+    findNextAvailableRoomNumber: function(nameTemplate) {
+      var searchTemplate = nameTemplate.replace("{{conversationLabel}}", "");
+      var searchRegExp = new RegExp("^" + searchTemplate + "(\\d+)$");
+
+      var roomNumbers = this._storeState.rooms.map(function(room) {
+        var match = searchRegExp.exec(room.roomName);
+        return match && match[1] ? parseInt(match[1], 10) : 0;
+      });
+
+      if (!roomNumbers.length) {
+        return 1;
+      }
+
+      return Math.max.apply(null, roomNumbers) + 1;
+    },
+
+    /**
+     * Generates a room names against the passed template string.
+     *
+     * @param  {String} nameTemplate The room name template.
+     * @return {String}
+     */
+    _generateNewRoomName: function(nameTemplate) {
+      var roomLabel = this.findNextAvailableRoomNumber(nameTemplate);
+      return nameTemplate.replace("{{conversationLabel}}", roomLabel);
+    },
+
+    /**
+     * Creates a new room.
+     *
+     * @param {sharedActions.CreateRoom} actionData The new room information.
+     */
+    createRoom: function(actionData) {
+      this.setStoreState({pendingCreation: true});
+
+      var roomCreationData = {
+        roomName:  this._generateNewRoomName(actionData.nameTemplate),
+        roomOwner: actionData.roomOwner,
+        maxSize:   this.maxRoomCreationSize,
+        expiresIn: this.defaultExpiresIn
+      };
+
+      this._mozLoop.rooms.create(roomCreationData, function(err) {
+        this.setStoreState({pendingCreation: false});
+        if (err) {
+         this._dispatchAction(new sharedActions.CreateRoomError({error: err}));
+        }
+      }.bind(this));
+    },
+
+    /**
+     * Executed when a room creation error occurs.
+     *
+     * @param {sharedActions.CreateRoomError} actionData The action data.
+     */
+    createRoomError: function(actionData) {
+      this.setStoreState({
+        error: actionData.error,
+        pendingCreation: false
+      });
+    },
+
+    /**
      * Gather the list of all available rooms from the MozLoop API.
      */
     getAllRooms: function() {
-      this.mozLoop.rooms.getAll(function(err, rawRoomList) {
+      this.setStoreState({pendingInitialRetrieval: true});
+      this._mozLoop.rooms.getAll(null, function(err, rawRoomList) {
         var action;
+
+        this.setStoreState({pendingInitialRetrieval: false});
+
         if (err) {
           action = new sharedActions.GetAllRoomsError({error: err});
         } else {
           action = new sharedActions.UpdateRoomList({roomList: rawRoomList});
         }
-        this.dispatcher.dispatch(action);
+
+        this._dispatchAction(action);
+
+        // We can only start listening to room events after getAll() has been
+        // called executed first.
+        this.startListeningToRoomEvents();
       }.bind(this));
     },
 
     /**
      * Updates current error state in case getAllRooms failed.
      *
-     * @param {sharedActions.UpdateRoomListError} actionData The action data.
+     * @param {sharedActions.GetAllRoomsError} actionData The action data.
      */
     getAllRoomsError: function(actionData) {
       this.setStoreState({error: actionData.error});
     },
 
     /**
      * Updates current room list.
      *
      * @param {sharedActions.UpdateRoomList} actionData The action data.
      */
     updateRoomList: function(actionData) {
       this.setStoreState({
         error: undefined,
-        rooms: this._processRawRoomList(actionData.roomList)
+        rooms: this._processRoomList(actionData.roomList)
       });
     },
   }, Backbone.Events);
 
   loop.store.RoomListStore = RoomListStore;
 })();
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -46,19 +46,20 @@ describe("loop.panel", function() {
       telemetryAdd: sinon.spy(),
       contacts: {
         getAll: function(callback) {
           callback(null, []);
         },
         on: sandbox.stub()
       },
       rooms: {
-        getAll: function(callback) {
+        getAll: function(version, callback) {
           callback(null, []);
-        }
+        },
+        on: sandbox.stub()
       }
     };
 
     document.mozL10n.initialize(navigator.mozLoop);
     // XXX prevent a race whenever mozL10n hasn't been initialized yet
     setTimeout(done, 0);
   });
 
@@ -620,17 +621,17 @@ describe("loop.panel", function() {
                                   true);
         });
 
       it("should notify the user when the operation failed", function() {
         fakeClient.requestCallUrl = function(_, cb) {
           cb("fake error");
         };
         sandbox.stub(notifications, "errorL10n");
-        var view = TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
+        TestUtils.renderIntoDocument(loop.panel.CallUrlResult({
           notifications: notifications,
           client: fakeClient
         }));
 
         sinon.assert.calledOnce(notifications.errorL10n);
         sinon.assert.calledWithExactly(notifications.errorL10n,
                                        "unable_retrieve_url");
       });
@@ -699,41 +700,86 @@ describe("loop.panel", function() {
 
         TestUtils.SimulateNative.mouseOut(roomNode);
 
         expect(buttonNode.classList.contains("checked")).eql(false);
       });
   });
 
   describe("loop.panel.RoomList", function() {
-    var roomListStore, dispatcher;
+    var roomListStore, dispatcher, fakeEmail;
 
     beforeEach(function() {
+      fakeEmail = "fakeEmail@example.com";
       dispatcher = new loop.Dispatcher();
       roomListStore = new loop.store.RoomListStore({
         dispatcher: dispatcher,
         mozLoop: navigator.mozLoop
       });
+      roomListStore.setStoreState({
+        pendingCreation: false,
+        pendingInitialRetrieval: false,
+        rooms: [],
+        error: undefined
+      });
     });
 
     function createTestComponent() {
       return TestUtils.renderIntoDocument(loop.panel.RoomList({
         store: roomListStore,
-        dispatcher: dispatcher
+        dispatcher: dispatcher,
+        userDisplayName: fakeEmail
       }));
     }
 
     it("should dispatch a GetAllRooms action on mount", function() {
       var dispatch = sandbox.stub(dispatcher, "dispatch");
 
       createTestComponent();
 
       sinon.assert.calledOnce(dispatch);
       sinon.assert.calledWithExactly(dispatch, new sharedActions.GetAllRooms());
     });
+
+    it("should dispatch a CreateRoom action when clicking on the Start a " +
+       "conversation button",
+      function() {
+        navigator.mozLoop.userProfile = {email: fakeEmail};
+        var dispatch = sandbox.stub(dispatcher, "dispatch");
+        var view = createTestComponent();
+
+        TestUtils.Simulate.click(view.getDOMNode().querySelector("button"));
+
+        sinon.assert.calledWith(dispatch, new sharedActions.CreateRoom({
+          nameTemplate: "fakeText",
+          roomOwner: fakeEmail
+        }));
+      });
+
+    it("should disable the create button when a creation operation is ongoing",
+      function() {
+        var dispatch = sandbox.stub(dispatcher, "dispatch");
+        roomListStore.setStoreState({pendingCreation: true});
+
+        var view = createTestComponent();
+
+        var buttonNode = view.getDOMNode().querySelector("button[disabled]");
+        expect(buttonNode).to.not.equal(null);
+    });
+
+    it("should disable the create button when a list retrieval operation is pending",
+      function() {
+        var dispatch = sandbox.stub(dispatcher, "dispatch");
+        roomListStore.setStoreState({pendingInitialRetrieval: true});
+
+        var view = createTestComponent();
+
+        var buttonNode = view.getDOMNode().querySelector("button[disabled]");
+        expect(buttonNode).to.not.equal(null);
+    });
   });
 
   describe('loop.panel.ToSView', function() {
 
     it("should render when the value of loop.seenToS is not set", function() {
       var view = TestUtils.renderIntoDocument(loop.panel.ToSView());
 
       TestUtils.findRenderedDOMComponentWithClass(view, "terms-service");
--- a/browser/components/loop/test/shared/roomListStore_test.js
+++ b/browser/components/loop/test/shared/roomListStore_test.js
@@ -1,30 +1,55 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 var expect = chai.expect;
 
 describe("loop.store.Room", function () {
   "use strict";
+
   describe("#constructor", function() {
     it("should validate room values", function() {
       expect(function() {
         new loop.store.Room();
       }).to.Throw(Error, /missing required/);
     });
   });
 });
 
 describe("loop.store.RoomListStore", function () {
   "use strict";
 
   var sharedActions = loop.shared.actions;
   var sandbox, dispatcher;
 
+  var fakeRoomList = [{
+    roomToken: "_nxD4V4FflQ",
+    roomUrl: "http://sample/_nxD4V4FflQ",
+    roomName: "First Room Name",
+    maxSize: 2,
+    participants: [],
+    ctime: 1405517546
+  }, {
+    roomToken: "QzBbvGmIZWU",
+    roomUrl: "http://sample/QzBbvGmIZWU",
+    roomName: "Second Room Name",
+    maxSize: 2,
+    participants: [],
+    ctime: 1405517418
+  }, {
+    roomToken: "3jKS_Els9IU",
+    roomUrl: "http://sample/3jKS_Els9IU",
+    roomName: "Third Room Name",
+    maxSize: 3,
+    clientMaxSize: 2,
+    participants: [],
+    ctime: 1405518241
+  }];
+
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
     dispatcher = new loop.Dispatcher();
   });
 
   afterEach(function() {
     sandbox.restore();
   });
@@ -38,93 +63,267 @@ describe("loop.store.RoomListStore", fun
 
     it("should throw an error if mozLoop is missing", function() {
       expect(function() {
         new loop.store.RoomListStore({dispatcher: dispatcher});
       }).to.Throw(/mozLoop/);
     });
   });
 
-  describe("#getAllRooms", function() {
-    var store, fakeMozLoop;
-    var fakeRoomList = [{
-      roomToken: "_nxD4V4FflQ",
-      roomUrl: "http://sample/_nxD4V4FflQ",
-      roomName: "First Room Name",
-      maxSize: 2,
-      participants: [],
-      ctime: 1405517546
-    }, {
-      roomToken: "QzBbvGmIZWU",
-      roomUrl: "http://sample/QzBbvGmIZWU",
-      roomName: "Second Room Name",
-      maxSize: 2,
-      participants: [],
-      ctime: 1405517418
-    }, {
-      roomToken: "3jKS_Els9IU",
-      roomUrl: "http://sample/3jKS_Els9IU",
-      roomName: "Third Room Name",
-      maxSize: 3,
-      clientMaxSize: 2,
-      participants: [],
-      ctime: 1405518241
-    }];
+  describe("constructed", function() {
+    var fakeMozLoop, store;
 
     beforeEach(function() {
       fakeMozLoop = {
         rooms: {
-          getAll: function(cb) {
-            cb(null, fakeRoomList);
-          }
+          create: function() {},
+          getAll: function() {},
+          on: sandbox.stub()
         }
       };
       store = new loop.store.RoomListStore({
         dispatcher: dispatcher,
         mozLoop: fakeMozLoop
       });
+      store.setStoreState({
+        error: undefined,
+        pendingCreation: false,
+        pendingInitialRetrieval: false,
+        rooms: []
+      });
     });
 
-    it("should trigger a list:changed event", function(done) {
-      store.on("change", function() {
-        done();
+    describe("MozLoop rooms event listeners", function() {
+      beforeEach(function() {
+        _.extend(fakeMozLoop.rooms, Backbone.Events);
+
+        fakeMozLoop.rooms.getAll = function(version, cb) {
+          cb(null, fakeRoomList);
+        };
+
+        store.getAllRooms(); // registers event listeners
+      });
+
+      describe("add", function() {
+        it("should add the room entry to the list", function() {
+          fakeMozLoop.rooms.trigger("add", "add", {
+            roomToken: "newToken",
+            roomUrl: "http://sample/newToken",
+            roomName: "New room",
+            maxSize: 2,
+            participants: [],
+            ctime: 1405517546
+          });
+
+          expect(store.getStoreState().rooms).to.have.length.of(4);
+        });
       });
 
-      dispatcher.dispatch(new sharedActions.GetAllRooms());
+      describe("update", function() {
+        it("should update a room entry", function() {
+          fakeMozLoop.rooms.trigger("update", "update", {
+            roomToken: "_nxD4V4FflQ",
+            roomUrl: "http://sample/_nxD4V4FflQ",
+            roomName: "Changed First Room Name",
+            maxSize: 2,
+            participants: [],
+            ctime: 1405517546
+          });
+
+          expect(store.getStoreState().rooms).to.have.length.of(3);
+          expect(store.getStoreState().rooms.some(function(room) {
+            return room.roomName === "Changed First Room Name";
+          })).eql(true);
+        });
+      });
+
+      describe("remove", function() {
+        it("should remove a room from the list", function() {
+          fakeMozLoop.rooms.trigger("remove", "remove", {
+            roomToken: "_nxD4V4FflQ"
+          });
+
+          expect(store.getStoreState().rooms).to.have.length.of(2);
+          expect(store.getStoreState().rooms.some(function(room) {
+            return room.roomToken === "_nxD4V4FflQ";
+          })).eql(false);
+        });
+      });
+    });
+
+    describe("#findNextAvailableRoomNumber", function() {
+      var fakeNameTemplate = "RoomWord {{conversationLabel}}";
+
+      it("should find next available room number from an empty room list",
+        function() {
+          store.setStoreState({rooms: []});
+
+          expect(store.findNextAvailableRoomNumber(fakeNameTemplate)).eql(1);
+        });
+
+      it("should find next available room number from a non empty room list",
+        function() {
+          store.setStoreState({
+            rooms: [{roomName: "RoomWord 1"}]
+          });
+
+          expect(store.findNextAvailableRoomNumber(fakeNameTemplate)).eql(2);
+        });
+
+      it("should not be sensitive to initial list order", function() {
+        store.setStoreState({
+          rooms: [{roomName: "RoomWord 99"}, {roomName: "RoomWord 98"}]
+        });
+
+        expect(store.findNextAvailableRoomNumber(fakeNameTemplate)).eql(100);
+      });
     });
 
-    it("should fetch the room list from the mozLoop API", function(done) {
-      store.once("change", function() {
+    describe("#createRoom", function() {
+      var fakeNameTemplate = "Conversation {{conversationLabel}}";
+      var fakeLocalRoomId = "777";
+      var fakeOwner = "fake@invalid";
+      var fakeRoomCreationData = {
+        nameTemplate: fakeNameTemplate,
+        roomOwner: fakeOwner
+      };
+
+      var fakeCreatedRoom = {
+        roomName: "Conversation 1",
+        roomToken: "fake",
+        roomUrl: "http://invalid",
+        maxSize: 42,
+        participants: [],
+        ctime: 1234567890
+      };
+
+      beforeEach(function() {
+        store.setStoreState({pendingCreation: false, rooms: []});
+      });
+
+      it("should request creation of a new room", function() {
+        sandbox.stub(fakeMozLoop.rooms, "create");
+
+        store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
+
+        sinon.assert.calledWith(fakeMozLoop.rooms.create, {
+          roomName: "Conversation 1",
+          roomOwner: fakeOwner,
+          maxSize: store.maxRoomCreationSize,
+          expiresIn: store.defaultExpiresIn
+        });
+      });
+
+      it("should store any creation encountered error", function() {
+        var err = new Error("fake");
+        sandbox.stub(fakeMozLoop.rooms, "create", function(data, cb) {
+          cb(err);
+        });
+
+        store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
+
+        expect(store.getStoreState().error).eql(err);
+      });
+
+      it("should switch the pendingCreation state flag to true", function() {
+        sandbox.stub(fakeMozLoop.rooms, "create");
+
+        store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
+
+        expect(store.getStoreState().pendingCreation).eql(true);
+      });
+
+      it("should switch the pendingCreation state flag to false once the " +
+         "operation is done", function() {
+        sandbox.stub(fakeMozLoop.rooms, "create", function(data, cb) {
+          cb();
+        });
+
+        store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
+
+        expect(store.getStoreState().pendingCreation).eql(false);
+      });
+    });
+
+    describe("#setStoreState", function() {
+      it("should update store state data", function() {
+        store.setStoreState({pendingCreation: true});
+
+        expect(store.getStoreState().pendingCreation).eql(true);
+      });
+
+      it("should trigger a `change` event", function(done) {
+        store.once("change", function() {
+          done();
+        });
+
+        store.setStoreState({pendingCreation: true});
+      });
+    });
+
+    describe("#getAllRooms", function() {
+      it("should fetch the room list from the MozLoop API", function() {
+        fakeMozLoop.rooms.getAll = function(version, cb) {
+          cb(null, fakeRoomList);
+        };
+
+        store.getAllRooms(new sharedActions.GetAllRooms());
+
         expect(store.getStoreState().error).to.be.a.undefined;
         expect(store.getStoreState().rooms).to.have.length.of(3);
-        done();
       });
 
-      dispatcher.dispatch(new sharedActions.GetAllRooms());
-    });
+      it("should order the room list using ctime desc", function() {
+        fakeMozLoop.rooms.getAll = function(version, cb) {
+          cb(null, fakeRoomList);
+        };
 
-    it("should order the room list using ctime desc", function(done) {
-      store.once("change", function() {
+        store.getAllRooms(new sharedActions.GetAllRooms());
+
         var storeState = store.getStoreState();
         expect(storeState.error).to.be.a.undefined;
         expect(storeState.rooms[0].ctime).eql(1405518241);
         expect(storeState.rooms[1].ctime).eql(1405517546);
         expect(storeState.rooms[2].ctime).eql(1405517418);
-        done();
+      });
+
+      it("should report an error", function() {
+        var err = new Error("fake");
+        fakeMozLoop.rooms.getAll = function(version, cb) {
+          cb(err);
+        };
+
+        dispatcher.dispatch(new sharedActions.GetAllRooms());
+
+        expect(store.getStoreState().error).eql(err);
       });
 
-      dispatcher.dispatch(new sharedActions.GetAllRooms());
-    });
+      it("should register event listeners after the list is retrieved",
+        function() {
+          sandbox.stub(store, "startListeningToRoomEvents");
+          fakeMozLoop.rooms.getAll = function(version, cb) {
+            cb(null, fakeRoomList);
+          };
 
-    it("should report an error", function() {
-      fakeMozLoop.rooms.getAll = function(cb) {
-        cb("fakeError");
-      };
+          store.getAllRooms();
+
+          sinon.assert.calledOnce(store.startListeningToRoomEvents);
+        });
 
-      store.once("change", function() {
-        var storeState = store.getStoreState();
-        expect(storeState.error).eql("fakeError");
+      it("should set the pendingInitialRetrieval flag to true", function() {
+        store.getAllRooms();
+
+        expect(store.getStoreState().pendingInitialRetrieval).eql(true);
       });
 
-      dispatcher.dispatch(new sharedActions.GetAllRooms());
+      it("should set pendingInitialRetrieval to false once the action is " +
+         "performed", function() {
+        fakeMozLoop.rooms.getAll = function(version, cb) {
+          cb(null, fakeRoomList);
+        };
+
+        store.getAllRooms();
+
+        expect(store.getStoreState().pendingInitialRetrieval).eql(false);
+      });
     });
   });
 });
--- a/browser/components/loop/ui/fake-mozLoop.js
+++ b/browser/components/loop/ui/fake-mozLoop.js
@@ -60,14 +60,15 @@ navigator.mozLoop = {
   copyString: function() {},
   contacts: {
     getAll: function(callback) {
       callback(null, []);
     },
     on: function() {}
   },
   rooms: {
-    getAll: function(callback) {
+    getAll: function(version, callback) {
       callback(null, fakeRooms);
-    }
+    },
+    on: function() {}
   },
   fxAEnabled: true
 };
--- a/browser/components/loop/ui/ui-showcase.css
+++ b/browser/components/loop/ui/ui-showcase.css
@@ -11,38 +11,38 @@
 .showcase {
   min-width: 350px;
   max-width: 730px;
 }
 
 .showcase > header {
   position: fixed;
   top: 0;
-  background-color: #fbfbfb;
+  background-color: #fff;
   z-index: 100000;
   width: 100%;
   padding-bottom: 1em;
 }
 
 .showcase > header > h1,
 .showcase > section > h1 {
   font-size: 2em;
   font-weight: bold;
   margin: .5em 0;
 }
 
 .showcase-menu > a {
   margin-right: .5em;
-  padding: .4rem;
+  padding: .2rem;
   margin-top: .2rem;
 }
 
 .showcase > section {
   position: relative;
-  padding-top: 14em;
+  padding-top: 15em;
   clear: both;
 }
 
 .showcase > section > h1 {
   margin: 1em 0;
   border-bottom: 1px solid #aaa;
 }
 
@@ -64,18 +64,18 @@
   margin: 1.5em 0;
 }
 
 .showcase > section .example > h3 {
   font-size: 1.2em;
   font-weight: bold;
   border-bottom: 1px dashed #aaa;
   margin: 1em 0;
-  margin-top: -14em;
-  padding-top: 14em;
+  margin-top: -15em;
+  padding-top: 15em;
   text-align: left;
 }
 
 .showcase > section .example > h3 a {
   text-decoration: none;
   color: #555;
 }
 
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -2339,22 +2339,17 @@ let E10SUINotification = {
   // e10s testing period to Nightly users.
   CURRENT_NOTICE_COUNT: 1,
   CURRENT_PROMPT_PREF: "browser.displayedE10SPrompt.1",
   PREVIOUS_PROMPT_PREF: "browser.displayedE10SPrompt",
 
   checkStatus: function() {
     let skipE10sChecks = false;
     try {
-      // This order matters, because
-      // browser.tabs.remote.autostart.disabled-because-using-a11y is not
-      // always defined and will throw when not present.
-      // privacy.trackingprotection.enabled is always defined.
       skipE10sChecks = (UpdateChannel.get() != "nightly") ||
-                       Services.prefs.getBoolPref("privacy.trackingprotection.enabled") ||
                        Services.prefs.getBoolPref("browser.tabs.remote.autostart.disabled-because-using-a11y");
     } catch(e) {}
 
     if (skipE10sChecks) {
       return;
     }
 
     if (Services.appinfo.browserTabsRemoteAutostart) {
--- a/browser/components/tabview/test/browser.ini
+++ b/browser/components/tabview/test/browser.ini
@@ -1,9 +1,11 @@
 [DEFAULT]
+skip-if = e10s # Bug 1092281
+
 support-files =
   dummy_page.html
   head.js
   search1.html
   search2.html
   test_bug600645.html
   test_bug644097.html
   test_bug678374.html
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -83,17 +83,18 @@ browser.jar:
     content/browser/devtools/webaudioeditor/views/context.js           (webaudioeditor/views/context.js)
     content/browser/devtools/webaudioeditor/views/inspector.js         (webaudioeditor/views/inspector.js)
     content/browser/devtools/profiler.xul                              (profiler/profiler.xul)
     content/browser/devtools/profiler.js                               (profiler/profiler.js)
     content/browser/devtools/ui-recordings.js                          (profiler/ui-recordings.js)
     content/browser/devtools/ui-profile.js                             (profiler/ui-profile.js)
 #ifdef MOZ_DEVTOOLS_PERFTOOLS
     content/browser/devtools/performance.xul                           (performance/performance.xul)
-    content/browser/devtools/performance.js                            (performance/performance.js)
+    content/browser/devtools/performance/controller.js                 (performance/controller.js)
+    content/browser/devtools/performance/views/main.js                 (performance/views/main.js)
 #endif
     content/browser/devtools/responsivedesign/resize-commands.js       (responsivedesign/resize-commands.js)
     content/browser/devtools/commandline.css                           (commandline/commandline.css)
     content/browser/devtools/commandlineoutput.xhtml                   (commandline/commandlineoutput.xhtml)
     content/browser/devtools/commandlinetooltip.xhtml                  (commandline/commandlinetooltip.xhtml)
     content/browser/devtools/commandline/commands-index.js             (commandline/commands-index.js)
     content/browser/devtools/framework/toolbox-window.xul              (framework/toolbox-window.xul)
     content/browser/devtools/framework/toolbox-options.xul             (framework/toolbox-options.xul)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/controller.js
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/devtools/Loader.jsm");
+Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
+
+devtools.lazyRequireGetter(this, "Services");
+devtools.lazyRequireGetter(this, "promise");
+devtools.lazyRequireGetter(this, "EventEmitter",
+  "devtools/toolkit/event-emitter");
+devtools.lazyRequireGetter(this, "DevToolsUtils",
+  "devtools/toolkit/DevToolsUtils");
+
+// Events emitted by the `PerformanceController`
+const EVENTS = {
+  // When a recording is started or stopped via the controller
+  RECORDING_STARTED: "Performance:RecordingStarted",
+  RECORDING_STOPPED: "Performance:RecordingStopped",
+
+  // Emitted by the PerformanceView on record button click
+  UI_START_RECORDING: "Performance:UI:StartRecording",
+  UI_STOP_RECORDING: "Performance:UI:StopRecording"
+};
+
+/**
+ * The current target and the profiler connection, set by this tool's host.
+ */
+let gToolbox, gTarget, gFront;
+
+/**
+ * Initializes the profiler controller and views.
+ */
+let startupPerformance = Task.async(function*() {
+  yield promise.all([
+    PrefObserver.register(),
+    PerformanceController.initialize(),
+    PerformanceView.initialize()
+  ]);
+});
+
+/**
+ * Destroys the profiler controller and views.
+ */
+let shutdownPerformance = Task.async(function*() {
+  yield promise.all([
+    PrefObserver.unregister(),
+    PerformanceController.destroy(),
+    PerformanceView.destroy()
+  ]);
+});
+
+/**
+ * Observes pref changes on the devtools.profiler branch and triggers the
+ * required frontend modifications.
+ */
+let PrefObserver = {
+  register: function() {
+    this.branch = Services.prefs.getBranch("devtools.profiler.");
+    this.branch.addObserver("", this, false);
+  },
+  unregister: function() {
+    this.branch.removeObserver("", this);
+  },
+  observe: function(subject, topic, pref) {
+    Prefs.refresh();
+  }
+};
+
+/**
+ * Functions handling target-related lifetime events and
+ * UI interaction.
+ */
+let PerformanceController = {
+  /**
+   * Listen for events emitted by the current tab target and
+   * main UI events.
+   */
+  initialize: function() {
+    this.startRecording = this.startRecording.bind(this);
+    this.stopRecording = this.stopRecording.bind(this);
+
+    PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording);
+    PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording);
+  },
+
+  /**
+   * Remove events handled by the PerformanceController
+   */
+  destroy: function() {
+    PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording);
+    PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording);
+  },
+
+  /**
+   * Starts recording with the PerformanceFront. Emits `EVENTS.RECORDING_STARTED`
+   * when the front is starting to record.
+   */
+  startRecording: Task.async(function *() {
+    yield gFront.startRecording();
+    this.emit(EVENTS.RECORDING_STARTED);
+  }),
+
+  /**
+   * Stops recording with the PerformanceFront. Emits `EVENTS.RECORDING_STOPPED`
+   * when the front stops recording.
+   */
+  stopRecording: Task.async(function *() {
+    let results = yield gFront.stopRecording();
+    this.emit(EVENTS.RECORDING_STOPPED, results);
+  })
+};
+
+/**
+ * Convenient way of emitting events from the controller.
+ */
+EventEmitter.decorate(PerformanceController);
+
+/**
+ * Shortcuts for accessing various profiler preferences.
+ */
+const Prefs = new ViewHelpers.Prefs("devtools.profiler", {
+});
+
+/**
+ * DOM query helpers.
+ */
+function $(selector, target = document) {
+  return target.querySelector(selector);
+}
+function $$(selector, target = document) {
+  return target.querySelectorAll(selector);
+}
deleted file mode 100644
--- a/browser/devtools/performance/performance.js
+++ /dev/null
@@ -1,97 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-"use strict";
-
-const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
-
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource://gre/modules/devtools/Loader.jsm");
-Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
-
-devtools.lazyRequireGetter(this, "Services");
-devtools.lazyRequireGetter(this, "promise");
-devtools.lazyRequireGetter(this, "EventEmitter",
-  "devtools/toolkit/event-emitter");
-devtools.lazyRequireGetter(this, "DevToolsUtils",
-  "devtools/toolkit/DevToolsUtils");
-
-/**
- * The current target and the profiler connection, set by this tool's host.
- */
-let gToolbox, gTarget, gFront;
-
-/**
- * Initializes the profiler controller and views.
- */
-let startupPerformance = Task.async(function*() {
-  yield promise.all([
-    PrefObserver.register(),
-    EventsHandler.initialize()
-  ]);
-});
-
-/**
- * Destroys the profiler controller and views.
- */
-let shutdownPerformance = Task.async(function*() {
-  yield promise.all([
-    PrefObserver.unregister(),
-    EventsHandler.destroy()
-  ]);
-});
-
-/**
- * Observes pref changes on the devtools.profiler branch and triggers the
- * required frontend modifications.
- */
-let PrefObserver = {
-  register: function() {
-    this.branch = Services.prefs.getBranch("devtools.profiler.");
-    this.branch.addObserver("", this, false);
-  },
-  unregister: function() {
-    this.branch.removeObserver("", this);
-  },
-  observe: function(subject, topic, pref) {
-    Prefs.refresh();
-  }
-};
-
-/**
- * Functions handling target-related lifetime events.
- */
-let EventsHandler = {
-  /**
-   * Listen for events emitted by the current tab target.
-   */
-  initialize: function() {
-  },
-
-  /**
-   * Remove events emitted by the current tab target.
-   */
-  destroy: function() {
-  }
-};
-
-/**
- * Shortcuts for accessing various profiler preferences.
- */
-const Prefs = new ViewHelpers.Prefs("devtools.profiler", {
-});
-
-/**
- * Convenient way of emitting events from the panel window.
- */
-EventEmitter.decorate(this);
-
-/**
- * DOM query helpers.
- */
-function $(selector, target = document) {
-  return target.querySelector(selector);
-}
-function $$(selector, target = document) {
-  return target.querySelectorAll(selector);
-}
--- a/browser/devtools/performance/performance.xul
+++ b/browser/devtools/performance/performance.xul
@@ -9,17 +9,18 @@
 <?xml-stylesheet href="chrome://browser/skin/devtools/performance.css" type="text/css"?>
 <!DOCTYPE window [
   <!ENTITY % profilerDTD SYSTEM "chrome://browser/locale/devtools/profiler.dtd">
   %profilerDTD;
 ]>
 
 <window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
   <script src="chrome://browser/content/devtools/theme-switching.js"/>
-  <script type="application/javascript" src="performance.js"/>
+  <script type="application/javascript" src="performance/controller.js"/>
+  <script type="application/javascript" src="performance/views/main.js"/>
 
   <vbox class="theme-body" flex="1">
     <toolbar id="performance-toolbar" class="devtools-toolbar">
       <hbox id="performance-toolbar-controls-recordings" class="devtools-toolbarbutton-group">
         <toolbarbutton id="record-button"
                        class="devtools-toolbarbutton"
                        tooltiptext="&profilerUI.recordButton.tooltip;"/>
         <toolbarbutton id="clear-button"
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -23,8 +23,9 @@ support-files =
 # needs shared connection with profiler's shared connection
 #[browser_perf-shared-connection-01.js]
 [browser_perf-shared-connection-02.js]
 [browser_perf-shared-connection-03.js]
 # bug 1077464
 #[browser_perf-shared-connection-04.js]
 [browser_perf-data-samples.js]
 [browser_perf-data-massaging-01.js]
+[browser_perf-ui-recording.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-ui-recording.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that the controller handles recording via the `stopwatch` button
+ * in the UI.
+ */
+let WAIT_TIME = 10;
+
+function spawnTest () {
+  let { panel } = yield initPerformance(SIMPLE_URL);
+  let { EVENTS, PerformanceController } = panel.panelWin;
+  let front = panel.panelWin.gFront;
+
+  ok(!nsIProfilerModule.IsActive(),
+    "The built-in profiler module should not have been automatically started.");
+
+  yield startRecording(panel);
+  busyWait(WAIT_TIME); // allow the profiler module to sample some cpu activity
+
+  ok(nsIProfilerModule.IsActive(),
+    "The built-in profiler module should now be active.");
+
+  yield stopRecording(panel);
+
+  ok(nsIProfilerModule.IsActive(),
+    "The built-in profiler module should still be active.");
+
+  yield teardown(panel);
+  finish();
+}
--- a/browser/devtools/performance/test/head.js
+++ b/browser/devtools/performance/test/head.js
@@ -202,8 +202,64 @@ function busyWait(time) {
   let stack;
   while (Date.now() - start < time) { stack = Components.stack; }
 }
 
 function idleWait(time) {
   return DevToolsUtils.waitForTime(time);
 }
 
+function* startRecording(panel) {
+  let win = panel.panelWin;
+  let clicked = panel.panelWin.PerformanceView.once(win.EVENTS.UI_START_RECORDING);
+  let started = panel.panelWin.PerformanceController.once(win.EVENTS.RECORDING_STARTED);
+  let button = win.$("#record-button");
+
+  ok(!button.hasAttribute("checked"),
+    "The record button should not be checked yet.");
+
+  ok(!button.hasAttribute("locked"),
+    "The record button should not be locked yet.");
+
+  EventUtils.synthesizeMouseAtCenter(button, {}, win);
+
+  yield clicked;
+
+  ok(button.hasAttribute("checked"),
+    "The record button should now be checked.");
+  ok(button.hasAttribute("locked"),
+    "The record button should be locked.");
+
+  yield started;
+
+  ok(button.hasAttribute("checked"),
+    "The record button should still be checked.");
+  ok(!button.hasAttribute("locked"),
+    "The record button should not be locked.");
+}
+
+function* stopRecording(panel) {
+  let win = panel.panelWin;
+  let clicked = panel.panelWin.PerformanceView.once(win.EVENTS.UI_STOP_RECORDING);
+  let ended = panel.panelWin.PerformanceController.once(win.EVENTS.RECORDING_STOPPED);
+  let button = win.$("#record-button");
+
+  ok(button.hasAttribute("checked"),
+    "The record button should already be checked.");
+  ok(!button.hasAttribute("locked"),
+    "The record button should not be locked yet.");
+
+  EventUtils.synthesizeMouseAtCenter(button, {}, win);
+
+  yield clicked;
+
+  ok(!button.hasAttribute("checked"),
+    "The record button should not be checked.");
+  ok(button.hasAttribute("locked"),
+    "The record button should be locked.");
+
+  yield ended;
+
+  ok(!button.hasAttribute("checked"),
+    "The record button should not be checked.");
+  ok(!button.hasAttribute("locked"),
+    "The record button should not be locked.");
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/views/main.js
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+/**
+ * Master view handler for the performance tool.
+ */
+let PerformanceView = {
+  /**
+   * Sets up the view with event binding.
+   */
+  initialize: function () {
+    this._recordButton = $("#record-button");
+
+    this._onRecordButtonClick = this._onRecordButtonClick.bind(this);
+    this._unlockRecordButton = this._unlockRecordButton.bind(this);
+
+    this._recordButton.addEventListener("mouseup", this._onRecordButtonClick);
+
+    // Bind to controller events to unlock the record button
+    PerformanceController.on(EVENTS.RECORDING_STARTED, this._unlockRecordButton);
+    PerformanceController.on(EVENTS.RECORDING_STOPPED, this._unlockRecordButton);
+  },
+
+  /**
+   * Unbinds events.
+   */
+  destroy: function () {
+    this._recordButton.removeEventListener("mouseup", this._onRecordButtonClick);
+    PerformanceController.off(EVENTS.RECORDING_STARTED, this._unlockRecordButton);
+    PerformanceController.off(EVENTS.RECORDING_STOPPED, this._unlockRecordButton);
+  },
+
+  /**
+   * Removes the `locked` attribute on the record button.
+   */
+  _unlockRecordButton: function () {
+    this._recordButton.removeAttribute("locked");
+  },
+
+  /**
+   * Handler for clicking the record button.
+   */
+  _onRecordButtonClick: function (e) {
+    if (this._recordButton.hasAttribute("checked")) {
+      this._recordButton.removeAttribute("checked");
+      this._recordButton.setAttribute("locked", "true");
+      this.emit(EVENTS.UI_STOP_RECORDING);
+    } else {
+      this._recordButton.setAttribute("checked", "true");
+      this._recordButton.setAttribute("locked", "true");
+      this.emit(EVENTS.UI_START_RECORDING);
+    }
+  }
+};
+
+/**
+ * Convenient way of emitting events from the view.
+ */
+EventEmitter.decorate(PerformanceView);
--- a/browser/extensions/pdfjs/README.mozilla
+++ b/browser/extensions/pdfjs/README.mozilla
@@ -1,4 +1,4 @@
 This is the pdf.js project output, https://github.com/mozilla/pdf.js
 
-Current extension version is: 1.0.907
+Current extension version is: 1.0.937
 
--- a/browser/extensions/pdfjs/content/build/pdf.js
+++ b/browser/extensions/pdfjs/content/build/pdf.js
@@ -17,18 +17,18 @@
 /*jshint globalstrict: false */
 /* globals PDFJS */
 
 // Initializing PDFJS global object (if still undefined)
 if (typeof PDFJS === 'undefined') {
   (typeof window !== 'undefined' ? window : this).PDFJS = {};
 }
 
-PDFJS.version = '1.0.907';
-PDFJS.build = 'e9072ac';
+PDFJS.version = '1.0.937';
+PDFJS.build = '308646d';
 
 (function pdfjsWrapper() {
   // Use strict in our context only - users might not want it
   'use strict';
 
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
 /* Copyright 2012 Mozilla Foundation
@@ -2794,16 +2794,18 @@ var Metadata = PDFJS.Metadata = (functio
 })();
 
 
 // <canvas> contexts store most of the state we need natively.
 // However, PDF needs a bit more state, which we store here.
 
 // Minimal font size that would be used during canvas fillText operations.
 var MIN_FONT_SIZE = 16;
+// Maximum font size that would be used during canvas fillText operations.
+var MAX_FONT_SIZE = 100;
 var MAX_GROUP_SIZE = 4096;
 
 var COMPILE_TYPE3_GLYPHS = true;
 
 function createScratchCanvas(width, height) {
   var canvas = document.createElement('canvas');
   canvas.width = width;
   canvas.height = height;
@@ -3994,19 +3996,19 @@ var CanvasGraphics = (function CanvasGra
 
       var italic = fontObj.italic ? 'italic' : 'normal';
       var typeface = '"' + name + '", ' + fontObj.fallbackName;
 
       // Some font backends cannot handle fonts below certain size.
       // Keeping the font at minimal size and using the fontSizeScale to change
       // the current transformation matrix before the fillText/strokeText.
       // See https://bugzilla.mozilla.org/show_bug.cgi?id=726227
-      var browserFontSize = size >= MIN_FONT_SIZE ? size : MIN_FONT_SIZE;
-      this.current.fontSizeScale = browserFontSize !== MIN_FONT_SIZE ? 1.0 :
-                                   size / MIN_FONT_SIZE;
+      var browserFontSize = size < MIN_FONT_SIZE ? MIN_FONT_SIZE :
+                            size > MAX_FONT_SIZE ? MAX_FONT_SIZE : size;
+      this.current.fontSizeScale = size / browserFontSize;
 
       var rule = italic + ' ' + bold + ' ' + browserFontSize + 'px ' + typeface;
       this.ctx.font = rule;
     },
     setTextRenderingMode: function CanvasGraphics_setTextRenderingMode(mode) {
       this.current.textRenderingMode = mode;
     },
     setTextRise: function CanvasGraphics_setTextRise(rise) {
@@ -4231,19 +4233,21 @@ var CanvasGraphics = (function CanvasGra
       var font = current.font;
       var fontSize = current.fontSize;
       var fontDirection = current.fontDirection;
       var charSpacing = current.charSpacing;
       var wordSpacing = current.wordSpacing;
       var textHScale = current.textHScale * fontDirection;
       var fontMatrix = current.fontMatrix || FONT_IDENTITY_MATRIX;
       var glyphsLength = glyphs.length;
+      var isTextInvisible =
+        current.textRenderingMode === TextRenderingMode.INVISIBLE;
       var i, glyph, width;
 
-      if (fontSize === 0) {
+      if (isTextInvisible || fontSize === 0) {
         return;
       }
 
       ctx.save();
       ctx.transform.apply(ctx, current.textMatrix);
       ctx.translate(current.x, current.y);
 
       ctx.scale(textHScale, fontDirection);
--- a/browser/extensions/pdfjs/content/build/pdf.worker.js
+++ b/browser/extensions/pdfjs/content/build/pdf.worker.js
@@ -17,18 +17,18 @@
 /*jshint globalstrict: false */
 /* globals PDFJS */
 
 // Initializing PDFJS global object (if still undefined)
 if (typeof PDFJS === 'undefined') {
   (typeof window !== 'undefined' ? window : this).PDFJS = {};
 }
 
-PDFJS.version = '1.0.907';
-PDFJS.build = 'e9072ac';
+PDFJS.version = '1.0.937';
+PDFJS.build = '308646d';
 
 (function pdfjsWrapper() {
   // Use strict in our context only - users might not want it
   'use strict';
 
 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
 /* Copyright 2012 Mozilla Foundation
@@ -4637,17 +4637,17 @@ var WidgetAnnotation = (function WidgetA
     var namedItem = dict;
     var ref = params.ref;
     while (namedItem) {
       var parent = namedItem.get('Parent');
       var parentRef = namedItem.getRaw('Parent');
       var name = namedItem.get('T');
       if (name) {
         fieldName.unshift(stringToPDFString(name));
-      } else {
+      } else if (parent && ref) {
         // The field name is absent, that means more than one field
         // with the same name may exist. Replacing the empty name
         // with the '`' plus index in the parent's 'Kids' array.
         // This is not in the PDF spec but necessary to id the
         // the input controls.
         var kids = parent.get('Kids');
         var j, jj;
         for (j = 0, jj = kids.length; j < jj; j++) {
@@ -34605,28 +34605,62 @@ var JpxImage = (function JpxImageClosure
           lbox = length - position + headerSize;
         }
         if (lbox < headerSize) {
           throw new Error('JPX Error: Invalid box field size');
         }
         var dataLength = lbox - headerSize;
         var jumpDataLength = true;
         switch (tbox) {
-          case 0x6A501A1A: // 'jP\032\032'
-            // TODO
-            break;
           case 0x6A703268: // 'jp2h'
             jumpDataLength = false; // parsing child boxes
             break;
           case 0x636F6C72: // 'colr'
-            // TODO
+            // Colorspaces are not used, the CS from the PDF is used.
+            var method = data[position];
+            var precedence = data[position + 1];
+            var approximation = data[position + 2];
+            if (method === 1) {
+              // enumerated colorspace
+              var colorspace = readUint32(data, position + 3);
+              switch (colorspace) {
+                case 16: // this indicates a sRGB colorspace
+                case 17: // this indicates a grayscale colorspace
+                case 18: // this indicates a YUV colorspace
+                  break;
+                default:
+                  warn('Unknown colorspace ' + colorspace);
+                  break;
+              }
+            } else if (method === 2) {
+              info('ICC profile not supported');
+            }
             break;
           case 0x6A703263: // 'jp2c'
             this.parseCodestream(data, position, position + dataLength);
             break;
+          case 0x6A502020: // 'jP\024\024'
+            if (0x0d0a870a !== readUint32(data, position)) {
+              warn('Invalid JP2 signature');
+            }
+            break;
+          // The following header types are valid but currently not used:
+          case 0x6A501A1A: // 'jP\032\032'
+          case 0x66747970: // 'ftyp'
+          case 0x72726571: // 'rreq'
+          case 0x72657320: // 'res '
+          case 0x69686472: // 'ihdr'
+            break;
+          default:
+            var headerType = String.fromCharCode((tbox >> 24) & 0xFF,
+                                                 (tbox >> 16) & 0xFF,
+                                                 (tbox >> 8) & 0xFF,
+                                                 tbox & 0xFF);
+            warn('Unsupported header type ' + tbox + ' (' + headerType + ')');
+            break;
         }
         if (jumpDataLength) {
           position += dataLength;
         }
       }
     },
     parseImageProperties: function JpxImage_parseImageProperties(stream) {
       var newByte = stream.getByte();
@@ -35048,16 +35082,21 @@ var JpxImage = (function JpxImageClosure
         precinctNumber = pj + pi * precinctParameters.numprecinctswide;
         codeblock.tbx0_ = Math.max(subband.tbx0, codeblock.tbx0);
         codeblock.tby0_ = Math.max(subband.tby0, codeblock.tby0);
         codeblock.tbx1_ = Math.min(subband.tbx1, codeblock.tbx1);
         codeblock.tby1_ = Math.min(subband.tby1, codeblock.tby1);
         codeblock.precinctNumber = precinctNumber;
         codeblock.subbandType = subband.type;
         codeblock.Lblock = 3;
+
+        if (codeblock.tbx1_ <= codeblock.tbx0_ ||
+            codeblock.tby1_ <= codeblock.tby0_) {
+          continue;
+        }
         codeblocks.push(codeblock);
         // building precinct for the sub-band
         var precinct = precincts[precinctNumber];
         if (precinct !== undefined) {
           if (i < precinct.cbxMin) {
             precinct.cbxMin = i;
           } else if (i > precinct.cbxMax) {
             precinct.cbxMax = i;
@@ -35330,38 +35369,38 @@ var JpxImage = (function JpxImageClosure
       }
       value = readBits(7);
       return value + 37;
     }
     var tileIndex = context.currentTile.index;
     var tile = context.tiles[tileIndex];
     var packetsIterator = tile.packetsIterator;
     while (position < dataLength) {
-      var packet = packetsIterator.nextPacket();
+      alignToByte();
       if (!readBits(1)) {
-        alignToByte();
         continue;
       }
+      var packet = packetsIterator.nextPacket();
       var layerNumber = packet.layerNumber;
       var queue = [], codeblock;
       for (var i = 0, ii = packet.codeblocks.length; i < ii; i++) {
         codeblock = packet.codeblocks[i];
         var precinct = codeblock.precinct;
         var codeblockColumn = codeblock.cbx - precinct.cbxMin;
         var codeblockRow = codeblock.cby - precinct.cbyMin;
         var codeblockIncluded = false;
         var firstTimeInclusion = false;
         var valueReady;
-        if ('included' in codeblock) {
+        if (codeblock['included'] !== undefined) {
           codeblockIncluded = !!readBits(1);
         } else {
           // reading inclusion tree
           precinct = codeblock.precinct;
           var inclusionTree, zeroBitPlanesTree;
-          if ('inclusionTree' in precinct) {
+          if (precinct['inclusionTree'] !== undefined) {
             inclusionTree = precinct.inclusionTree;
           } else {
             // building inclusion and zero bit-planes trees
             var width = precinct.cbxMax - precinct.cbxMin + 1;
             var height = precinct.cbyMax - precinct.cbyMin + 1;
             inclusionTree = new InclusionTree(width, height, layerNumber);
             zeroBitPlanesTree = new TagTree(width, height);
             precinct.inclusionTree = inclusionTree;
@@ -35416,17 +35455,17 @@ var JpxImage = (function JpxImageClosure
           codingpasses: codingpasses,
           dataLength: codedDataLength
         });
       }
       alignToByte();
       while (queue.length > 0) {
         var packetItem = queue.shift();
         codeblock = packetItem.codeblock;
-        if (!('data' in codeblock)) {
+        if (codeblock['data'] === undefined) {
           codeblock.data = [];
         }
         codeblock.data.push({
           data: data,
           start: offset + position,
           end: offset + position + packetItem.dataLength,
           codingpasses: packetItem.codingpasses
         });
@@ -35446,17 +35485,17 @@ var JpxImage = (function JpxImageClosure
 
     for (var i = 0, ii = codeblocks.length; i < ii; ++i) {
       var codeblock = codeblocks[i];
       var blockWidth = codeblock.tbx1_ - codeblock.tbx0_;
       var blockHeight = codeblock.tby1_ - codeblock.tby0_;
       if (blockWidth === 0 || blockHeight === 0) {
         continue;
       }
-      if (!('data' in codeblock)) {
+      if (codeblock['data'] === undefined) {
         continue;
       }
 
       var bitModel, currentCodingpassType;
       bitModel = new BitModel(blockWidth, blockHeight, codeblock.subbandType,
                               codeblock.zeroBitPlanes, mb);
       currentCodingpassType = 2; // first bit plane starts from cleanup
 
@@ -35701,20 +35740,20 @@ var JpxImage = (function JpxImageClosure
     return resultImages;
   }
   function initializeTile(context, tileIndex) {
     var siz = context.SIZ;
     var componentsCount = siz.Csiz;
     var tile = context.tiles[tileIndex];
     for (var c = 0; c < componentsCount; c++) {
       var component = tile.components[c];
-      var qcdOrQcc = (c in context.currentTile.QCC ?
+      var qcdOrQcc = (context.currentTile.QCC[c] !== undefined ?
         context.currentTile.QCC[c] : context.currentTile.QCD);
       component.quantizationParameters = qcdOrQcc;
-      var codOrCoc = (c in context.currentTile.COC ?
+      var codOrCoc = (context.currentTile.COC[c] !== undefined  ?
         context.currentTile.COC[c] : context.currentTile.COD);
       component.codingStyleParameters = codOrCoc;
     }
     tile.codingStyleDefaultParameters = context.currentTile.COD;
   }
 
   // Section B.10.2 Tag trees
   var TagTree = (function TagTreeClosure() {
@@ -35733,17 +35772,17 @@ var JpxImage = (function JpxImageClosure
       }
     }
     TagTree.prototype = {
       reset: function TagTree_reset(i, j) {
         var currentLevel = 0, value = 0, level;
         while (currentLevel < this.levels.length) {
           level = this.levels[currentLevel];
           var index = i + j * level.width;
-          if (index in level.items) {
+          if (level.items[index] !== undefined) {
             value = level.items[index];
             break;
           }
           level.index = index;
           i >>= 1;
           j >>= 1;
           currentLevel++;
         }
--- a/browser/extensions/pdfjs/content/web/debugger.js
+++ b/browser/extensions/pdfjs/content/web/debugger.js
@@ -119,18 +119,18 @@ var FontInspector = (function FontInspec
       var download = document.createElement('a');
       if (url) {
         url = /url\(['"]?([^\)"']+)/.exec(url);
         download.href = url[1];
       } else if (fontObj.data) {
         url = URL.createObjectURL(new Blob([fontObj.data], {
           type: fontObj.mimeType
         }));
+        download.href = url;
       }
-      download.href = url;
       download.textContent = 'Download';
       var logIt = document.createElement('a');
       logIt.href = '';
       logIt.textContent = 'Log';
       logIt.addEventListener('click', function(event) {
         event.preventDefault();
         console.log(fontObj);
       });
--- a/browser/extensions/pdfjs/content/web/l10n.js
+++ b/browser/extensions/pdfjs/content/web/l10n.js
@@ -118,16 +118,20 @@
       return gLanguage;
     },
 
     // get the direction (ltr|rtl) of the current language
     getDirection: function() {
       // http://www.w3.org/International/questions/qa-scripts
       // Arabic, Hebrew, Farsi, Pashto, Urdu
       var rtlList = ['ar', 'he', 'fa', 'ps', 'ur'];
-      return (rtlList.indexOf(gLanguage) >= 0 ? 'rtl' : 'ltr');
+
+      // use the short language code for "full" codes like 'ar-sa' (issue 5440) 
+      var shortCode = gLanguage.split('-')[0];
+
+      return (rtlList.indexOf(shortCode) >= 0) ? 'rtl' : 'ltr';
     },
 
     // translate an element or document fragment
     translate: translateFragment
   };
 })(this);
 
--- a/dom/tests/mochitest/notification/mochitest.ini
+++ b/dom/tests/mochitest/notification/mochitest.ini
@@ -1,12 +1,12 @@
 [DEFAULT]
 skip-if = e10s || buildapp == 'mulet'
 support-files =
   MockServices.js
   NotificationTest.js
 
 [test_notification_basics.html]
 [test_notification_storage.html]
-skip-if = (toolkit == 'gonk')
+skip-if = toolkit == 'android' || toolkit == 'gonk' #bug 960762
 [test_bug931307.html]
 skip-if = (toolkit == 'gonk' && debug) #debug-only timeout
 [test_notification_resend.html]
--- a/mobile/locales/en-US/searchplugins/wikipedia.xml
+++ b/mobile/locales/en-US/searchplugins/wikipedia.xml
@@ -9,14 +9,15 @@
 <Url type="application/x-suggestions+json" method="GET" template="https://en.wikipedia.org/w/api.php">
   <Param name="action" value="opensearch"/>
   <Param name="search" value="{searchTerms}"/>
 </Url>
 <Url type="text/html" method="GET" template="https://en.wikipedia.org/wiki/Special:Search">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
+<!-- Search activity -->
 <Url type="text/html" method="GET" rel="mobile" template="https://en.m.wikipedia.org/wiki/Special:Search">
   <Param name="search" value="{searchTerms}"/>
   <Param name="sourceid" value="Mozilla-search"/>
 </Url>
 <SearchForm>https://en.wikipedia.org/wiki/Special:Search</SearchForm>
 </SearchPlugin>
--- a/mobile/locales/en-US/searchplugins/yahoo.xml
+++ b/mobile/locales/en-US/searchplugins/yahoo.xml
@@ -13,10 +13,16 @@
   <Param name="command" value="{searchTerms}" />
   <MozParam name="nresults" condition="pref" pref="maxSuggestions" />
 </Url>
 <Url type="text/html" method="GET" template="https://search.yahoo.com/search">
   <Param name="p" value="{searchTerms}" />
   <Param name="ei" value="UTF-8" />
   <Param name="fr" value="mozmob1" />
 </Url>
+<!-- Search activity -->
+<Url type="text/html" method="GET" rel="mobile" template="https://search.yahoo.com/search">
+  <Param name="p" value="{searchTerms}" />
+  <Param name="ei" value="UTF-8" />
+  <Param name="fr" value="mozmob2" />
+</Url>
 <SearchForm>https://search.yahoo.com/</SearchForm>
 </SearchPlugin>
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -5107,16 +5107,21 @@ ThreadSources.prototype = {
 
   /**
    * Return a promise of a SourceMapConsumer for the source map for
    * |aScript|; if we already have such a promise extant, return that.
    * |aScript| must have a non-null sourceMapURL.
    */
   sourceMap: function (aScript) {
     dbg_assert(aScript.source.sourceMapURL, "Script should have a sourceMapURL");
+
+    if (this._sourceMapsByGeneratedSource[aScript.url]) {
+      return this._sourceMapsByGeneratedSource[aScript.url];
+    }
+
     let sourceMapURL = this._normalize(aScript.source.sourceMapURL, aScript.url);
     let map = this._fetchSourceMap(sourceMapURL, aScript.url)
       .then(aSourceMap => this.saveSourceMap(aSourceMap, aScript.url));
     this._sourceMapsByGeneratedSource[aScript.url] = map;
     return map;
   },
 
   /**
--- a/toolkit/devtools/server/tests/unit/head_dbg.js
+++ b/toolkit/devtools/server/tests/unit/head_dbg.js
@@ -593,8 +593,20 @@ function blackBox(sourceClient) {
  *
  * @param SourceClient sourceClient
  * @returns Promise
  */
 function unBlackBox(sourceClient) {
   dumpn("Un-black boxing source: " + sourceClient.actor);
   return rdpRequest(sourceClient, sourceClient.unblackBox);
 }
+
+/**
+ * Do a fake reload which clears the thread debugger
+ *
+ * @param TabClient tabClient
+ * @returns Promise<response>
+ */
+function reload(tabClient) {
+  let deferred = promise.defer();
+  tabClient._reload({}, deferred.resolve);
+  return deferred.promise;
+}
--- a/toolkit/devtools/server/tests/unit/test_sourcemaps-13.js
+++ b/toolkit/devtools/server/tests/unit/test_sourcemaps-13.js
@@ -1,29 +1,31 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
- * Test that we don't permanently cache source maps.
+ * Test that we don't permanently cache source maps across reloads.
  */
 
 var gDebuggee;
 var gClient;
 var gThreadClient;
+var gTabClient;
 
 Components.utils.import("resource:///modules/devtools/SourceMap.jsm");
 
 function run_test()
 {
   initTestDebuggerServer();
   gDebuggee = addTestGlobal("test-source-map");
   gClient = new DebuggerClient(DebuggerServer.connectPipe());
   gClient.connect(function() {
     attachTestTabAndResume(gClient, "test-source-map", function(aResponse, aTabClient, aThreadClient) {
       gThreadClient = aThreadClient;
+      gTabClient = aTabClient;
       setup_code();
     });
   });
   do_test_pending();
 }
 
 // The MAP_FILE_NAME is .txt so that the OS will definitely have an extension ->
 // content type mapping for the extension. If it doesn't (like .map or .json),
@@ -54,46 +56,45 @@ function setup_code() {
   test_initial_sources();
 }
 
 function test_initial_sources() {
   gThreadClient.getSources(function ({ error, sources }) {
     do_check_true(!error);
     do_check_eq(sources.length, 1);
     do_check_eq(sources[0].url, getFileUrl(TEMP_FILE_1, true));
-    setup_new_code();
+    reload(gTabClient).then(setup_new_code);
   });
 }
 
 function setup_new_code() {
   let node = new SourceNode(1, 0,
                             getFileUrl(TEMP_FILE_2, true),
                             "function temporary2() {}\n");
   let { code, map } = node.toStringWithSourceMap({
     file: getFileUrl(TEMP_GENERATED_SOURCE, true)
   });
 
   code += "\n//# sourceMappingURL=" + getFileUrl(MAP_FILE_NAME, true);
   writeFile(MAP_FILE_NAME, map.toString());
 
+  gClient.addOneTimeListener("newSource", test_new_sources);
   Cu.evalInSandbox(code,
                    gDebuggee,
                    "1.8",
                    getFileUrl(TEMP_GENERATED_SOURCE, true),
                    1);
-
-  gClient.addOneTimeListener("newSource", test_new_sources);
 }
 
 function test_new_sources() {
   gThreadClient.getSources(function ({ error, sources }) {
     do_check_true(!error);
 
     // Should now have TEMP_FILE_2 as a source.
-    do_check_eq(sources.length, 2);
+    do_check_eq(sources.length, 1);
     let s = sources.filter(s => s.url === getFileUrl(TEMP_FILE_2, true))[0];
     do_check_true(!!s);
 
     finish_test();
   });
 }
 
 function finish_test() {
--- a/toolkit/devtools/server/tests/unit/testactors.js
+++ b/toolkit/devtools/server/tests/unit/testactors.js
@@ -115,24 +115,31 @@ TestTabActor.prototype = {
 
   onDetach: function(aRequest) {
     if (!this._attached) {
       return { "error":"wrongState" };
     }
     return { type: "detached" };
   },
 
+  onReload: function(aRequest) {
+    this.threadActor.clearDebuggees();
+    this.threadActor.dbg.addDebuggees();
+    return {};
+  },
+
   /* Support for DebuggerServer.addTabActor. */
   _createExtraActors: createExtraActors,
   _appendExtraActors: appendExtraActors
 };
 
 TestTabActor.prototype.requestTypes = {
   "attach": TestTabActor.prototype.onAttach,
-  "detach": TestTabActor.prototype.onDetach
+  "detach": TestTabActor.prototype.onDetach,
+  "reload": TestTabActor.prototype.onReload
 };
 
 exports.register = function(handle) {
   handle.setRootActor(createRootActor);
 };
 
 exports.unregister = function(handle) {
   handle.setRootActor(null);
--- a/toolkit/xre/nsAppRunner.cpp
+++ b/toolkit/xre/nsAppRunner.cpp
@@ -2330,26 +2330,42 @@ SelectProfile(nsIProfileLock* *aResult, 
   ar = CheckArg("profilemanager", true);
   if (ar == ARG_BAD) {
     PR_fprintf(PR_STDERR, "Error: argument --profilemanager is invalid when argument --osint is specified\n");
     return NS_ERROR_FAILURE;
   } else if (ar == ARG_FOUND && CanShowProfileManager()) {
     return ShowProfileManager(aProfileSvc, aNative);
   }
 
+#ifndef MOZ_DEV_EDITION
+  // If the only existing profile is the dev-edition-profile and this is not
+  // Developer Edition, then no valid profiles were found.
+  if (count == 1) {
+    nsCOMPtr<nsIToolkitProfile> deProfile;
+    // GetSelectedProfile will auto-select the only profile if there's just one
+    aProfileSvc->GetSelectedProfile(getter_AddRefs(deProfile));
+    nsAutoCString profileName;
+    deProfile->GetName(profileName);
+    if (profileName.EqualsLiteral("dev-edition-default")) {
+      count = 0;
+    }
+  }
+#endif
+
   if (!count) {
     gDoMigration = true;
     gDoProfileReset = false;
 
     // create a default profile
     nsCOMPtr<nsIToolkitProfile> profile;
     nsresult rv = aProfileSvc->CreateProfile(nullptr, // choose a default dir for us
                                              NS_LITERAL_CSTRING("default"),
                                              getter_AddRefs(profile));
     if (NS_SUCCEEDED(rv)) {
+      aProfileSvc->SetDefaultProfile(profile);
       aProfileSvc->Flush();
       rv = profile->Lock(nullptr, aResult);
       if (NS_SUCCEEDED(rv)) {
         if (aProfileName)
           aProfileName->AssignLiteral("default");
         return NS_OK;
       }
     }
@@ -4606,23 +4622,16 @@ mozilla::BrowserTabsRemoteAutostart()
 
     if (accelDisabled) {
       gBrowserTabsRemoteAutostart = false;
       LogE10sBlockedReason("Hardware acceleration is disabled.");
     }
   }
 #endif
 
-  bool tpEnabled = Preferences::GetBool("privacy.trackingprotection.enabled",
-                                        false);
-  if (tpEnabled) {
-    gBrowserTabsRemoteAutostart = false;
-    LogE10sBlockedReason("Tracking protection is enabled");
-  }
-
   mozilla::Telemetry::Accumulate(mozilla::Telemetry::E10S_AUTOSTART, gBrowserTabsRemoteAutostart);
   if (Preferences::GetBool("browser.enabledE10SFromPrompt", false)) {
     mozilla::Telemetry::Accumulate(mozilla::Telemetry::E10S_STILL_ACCEPTED_FROM_PROMPT,
                                     gBrowserTabsRemoteAutostart);
   }
   if (prefEnabled) {
     mozilla::Telemetry::Accumulate(mozilla::Telemetry::E10S_BLOCKED_FROM_RUNNING,
                                     !gBrowserTabsRemoteAutostart);