Merge m-c -> cedar
authorJonathan Griffin <jgriffin@mozilla.com>
Thu, 15 Jan 2015 12:40:46 -0800
changeset 326724 bc68c7d838666273abfd208876f89f97671f8734
parent 326723 45563790949b96399274c9be403d0b3dc5bbd513 (current diff)
parent 237183 206bf1a98cd7a032fa6004298a68b48b51327ba6 (diff)
child 326725 336eba646adc2da1ffa4b47aed565042e67bbfcb
push id10169
push userdminor@mozilla.com
push dateThu, 28 Jan 2016 13:10:48 +0000
milestone38.0a1
Merge m-c -> cedar
browser/components/places/BrowserPlaces.manifest
browser/components/places/PlacesProtocolHandler.js
browser/config/mozconfigs/linux64/debug-nonunified
browser/config/mozconfigs/linux64/nightly-nonunified
browser/config/mozconfigs/macosx-universal/nightly-nonunified
browser/config/mozconfigs/macosx64/debug-nonunified
browser/config/mozconfigs/win32/debug-nonunified
browser/config/mozconfigs/win32/nightly-nonunified
browser/config/mozconfigs/win64/debug-nonunified
browser/config/mozconfigs/win64/nightly-nonunified
dom/canvas/test/reftest/colors-half-alpha.png
dom/canvas/test/reftest/colors.png
dom/canvas/test/reftest/half-colors-half-alpha.png
dom/canvas/test/reftest/half-colors.png
dom/canvas/test/reftest/webgl-color-alpha-test.html
dom/canvas/test/reftest/webgl-orientation-test.html
dom/canvas/test/reftest/white-top-left.png
js/src/vm/ForkJoin.cpp
js/src/vm/ForkJoin.h
js/src/vm/ThreadPool.cpp
js/src/vm/ThreadPool.h
mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_back.png
mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_forward.png
mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_reload.png
mobile/android/base/resources/drawable-large-hdpi-v11/menu.png
mobile/android/base/resources/drawable-large-mdpi-v11/ic_menu_back.png
mobile/android/base/resources/drawable-large-mdpi-v11/ic_menu_forward.png
mobile/android/base/resources/drawable-large-mdpi-v11/ic_menu_reload.png
mobile/android/base/resources/drawable-large-mdpi-v11/menu.png
mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_back.png
mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_forward.png
mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_reload.png
mobile/android/base/resources/drawable-large-xhdpi-v11/menu.png
mobile/android/base/resources/drawable-xlarge-hdpi-v11/ic_menu_bookmark_add.png
mobile/android/base/resources/drawable-xlarge-hdpi-v11/ic_menu_bookmark_remove.png
mobile/android/base/resources/drawable-xlarge-mdpi-v11/ic_menu_bookmark_add.png
mobile/android/base/resources/drawable-xlarge-mdpi-v11/ic_menu_bookmark_remove.png
mobile/android/base/resources/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_add.png
mobile/android/base/resources/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_remove.png
mobile/android/config/mozconfigs/android-api-10/debug-nonunified
mobile/android/config/mozconfigs/android-api-10/nightly-nonunified
mobile/android/config/mozconfigs/android-api-11/debug-nonunified
mobile/android/config/mozconfigs/android-api-11/nightly-nonunified
mobile/android/config/mozconfigs/android/debug-nonunified
mobile/android/config/mozconfigs/android/nightly-nonunified
security/pkix/include/pkix/nullptr.h
testing/web-platform/fetchlogs.py
testing/web-platform/harness/wptrunner/update.py
testing/web-platform/meta/custom-elements/registering-custom-elements/unresolved-element-pseudoclass/unresolved-element-pseudoclass-css-test-custom-tag.html.ini
testing/web-platform/meta/custom-elements/registering-custom-elements/unresolved-element-pseudoclass/unresolved-element-pseudoclass-css-test-type-extension.html.ini
testing/web-platform/meta/workers/semantics/interface-objects/001.html.ini
testing/web-platform/tests/app-uri/resources/idlharness.js
testing/web-platform/tests/app-uri/resources/testharness.css
testing/web-platform/tests/app-uri/resources/testharness.js
testing/web-platform/tests/app-uri/resources/testharnessreport.js
testing/web-platform/tests/old-tests/submission/Infraware/Session_History/resources/WebIDLParser.js
testing/web-platform/tests/old-tests/submission/Infraware/Session_History/resources/apisample.htm
testing/web-platform/tests/old-tests/submission/Infraware/Session_History/resources/apisample2.htm
testing/web-platform/tests/old-tests/submission/Infraware/Session_History/resources/apisample3.htm
testing/web-platform/tests/old-tests/submission/Infraware/Session_History/resources/idlharness.js
testing/web-platform/tests/old-tests/submission/Infraware/Session_History/resources/readme.md
testing/web-platform/tests/old-tests/submission/Infraware/Session_History/resources/testharness.css
testing/web-platform/tests/old-tests/submission/Infraware/Session_History/resources/testharness.js
testing/web-platform/tests/old-tests/submission/Infraware/Session_History/resources/testharnessreport.js
testing/web-platform/tests/workers/semantics/interface-objects/001.html
testing/web-platform/update.py
toolkit/components/places/nsPIPlacesHistoryListenersNotifier.idl
--- a/AUTHORS
+++ b/AUTHORS
@@ -9,16 +9,17 @@ contribution to Mozilla, see http://www.
 <1010mozilla@Ostermiller.com>
 Aaron Boodman <aa@google.com>
 Aaron Kaluszka <ask@swva.net>
 Aaron Leventhal <aaronleventhal@moonset.net>
 Aaron Nowack <anowack@mimiru.net>
 Aaron Reed <aaronr@us.ibm.com>
 Aaron Spangler <aaron@spangler.ods.org>
 Aaron Train <aaron.train@gmail.com>
+Abdelrhman Ahmed <a.ahmed1026@gmail.com>
 Achim Hasenmueller <achimha@innotek.de>
 ActiveState Tool Corp.
 Adam Barth <hk9565@gmail.com>
 Adam Christian <adam.christian@gmail.com>
 Adam Hauner
 Adam Lock <adamlock@netscape.com>
 Adam L. Peller
 Adam Souzis <adam@souzis.com>
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Merge day clobber
\ No newline at end of file
+Bug 1101553 - remove nsPIPlacesHistoryListenersNotifier
--- a/accessible/atk/AccessibleWrap.cpp
+++ b/accessible/atk/AccessibleWrap.cpp
@@ -1014,25 +1014,30 @@ GetProxy(AtkObject* aObj)
 
 AtkObject*
 GetWrapperFor(ProxyAccessible* aProxy)
 {
   return reinterpret_cast<AtkObject*>(aProxy->GetWrapper() & ~IS_PROXY);
 }
 
 static uint16_t
-GetInterfacesForProxy(ProxyAccessible* aProxy)
+GetInterfacesForProxy(ProxyAccessible* aProxy, uint32_t aInterfaces)
 {
-  return MAI_INTERFACE_COMPONENT;
+  uint16_t interfaces = 1 << MAI_INTERFACE_COMPONENT;
+  if (aInterfaces & Interfaces::HYPERTEXT)
+    interfaces |= (1 << MAI_INTERFACE_HYPERTEXT) | (1 << MAI_INTERFACE_TEXT)
+        | (1 << MAI_INTERFACE_EDITABLE_TEXT);
+
+  return interfaces;
 }
 
 void
-a11y::ProxyCreated(ProxyAccessible* aProxy)
+a11y::ProxyCreated(ProxyAccessible* aProxy, uint32_t aInterfaces)
 {
-  GType type = GetMaiAtkType(GetInterfacesForProxy(aProxy));
+  GType type = GetMaiAtkType(GetInterfacesForProxy(aProxy, aInterfaces));
   NS_ASSERTION(type, "why don't we have a type!");
 
   AtkObject* obj =
     reinterpret_cast<AtkObject *>
     (g_object_new(type, nullptr));
   if (!obj)
     return;
 
--- a/accessible/atk/nsMaiInterfaceText.cpp
+++ b/accessible/atk/nsMaiInterfaceText.cpp
@@ -4,16 +4,17 @@
  * 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/. */
 
 #include "InterfaceInitFuncs.h"
 
 #include "Accessible-inl.h"
 #include "HyperTextAccessible-inl.h"
 #include "nsMai.h"
+#include "ProxyAccessible.h"
 
 #include "nsIAccessibleTypes.h"
 #include "nsIPersistentProperties2.h"
 #include "nsISimpleEnumerator.h"
 
 #include "mozilla/Likely.h"
 
 using namespace mozilla;
@@ -105,31 +106,33 @@ ConvertTexttoAsterisks(AccessibleWrap* a
 }
 
 extern "C" {
 
 static gchar*
 getTextCB(AtkText *aText, gint aStartOffset, gint aEndOffset)
 {
   AccessibleWrap* accWrap = GetAccessibleWrap(ATK_OBJECT(aText));
-  if (!accWrap)
-    return nullptr;
+  nsAutoString autoStr;
+  if (accWrap) {
+    HyperTextAccessible* text = accWrap->AsHyperText();
+    if (!text || !text->IsTextRole())
+      return nullptr;
 
-  HyperTextAccessible* text = accWrap->AsHyperText();
-  if (!text || !text->IsTextRole())
-    return nullptr;
-
-    nsAutoString autoStr;
     text->TextSubstring(aStartOffset, aEndOffset, autoStr);
 
     ConvertTexttoAsterisks(accWrap, autoStr);
-    NS_ConvertUTF16toUTF8 cautoStr(autoStr);
+  } else if (ProxyAccessible* proxy = GetProxy(ATK_OBJECT(aText))) {
+    proxy->TextSubstring(aStartOffset, aEndOffset, autoStr);
+  }
 
-    //copy and return, libspi will free it.
-    return (cautoStr.get()) ? g_strdup(cautoStr.get()) : nullptr;
+  NS_ConvertUTF16toUTF8 cautoStr(autoStr);
+
+  //copy and return, libspi will free it.
+  return (cautoStr.get()) ? g_strdup(cautoStr.get()) : nullptr;
 }
 
 static gchar*
 getTextAfterOffsetCB(AtkText *aText, gint aOffset,
                      AtkTextBoundary aBoundaryType,
                      gint *aStartOffset, gint *aEndOffset)
 {
   AccessibleWrap* accWrap = GetAccessibleWrap(ATK_OBJECT(aText));
--- a/accessible/base/DocManager.cpp
+++ b/accessible/base/DocManager.cpp
@@ -544,10 +544,10 @@ DocManager::RemoteDocAdded(DocAccessible
   if (!sRemoteDocuments) {
     sRemoteDocuments = new nsTArray<DocAccessibleParent*>;
     ClearOnShutdown(&sRemoteDocuments);
   }
 
   MOZ_ASSERT(!sRemoteDocuments->Contains(aDoc),
       "How did we already have the doc!");
   sRemoteDocuments->AppendElement(aDoc);
-  ProxyCreated(aDoc);
+  ProxyCreated(aDoc, 0);
 }
--- a/accessible/base/Platform.h
+++ b/accessible/base/Platform.h
@@ -50,17 +50,17 @@ void PlatformInit();
  * Note this is called before internal accessibility support is shutdown.
  */
 void PlatformShutdown();
 
 /**
  * called when a new ProxyAccessible is created, so the platform may setup a
  * wrapper for it, or take other action.
  */
-void ProxyCreated(ProxyAccessible*);
+void ProxyCreated(ProxyAccessible* aProxy, uint32_t aInterfaces);
 
 /**
  * Called just before a ProxyAccessible is destroyed so its wrapper can be
  * disposed of and other action taken.
  */
 void ProxyDestroyed(ProxyAccessible*);
 
 /**
--- a/accessible/ipc/DocAccessibleChild.cpp
+++ b/accessible/ipc/DocAccessibleChild.cpp
@@ -2,37 +2,49 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "DocAccessibleChild.h"
 
 #include "Accessible-inl.h"
+#include "ProxyAccessible.h"
 
 #include "nsIPersistentProperties2.h"
 #include "nsISimpleEnumerator.h"
 
 namespace mozilla {
 namespace a11y {
 
-void
+static uint32_t
+InterfacesFor(Accessible* aAcc)
+{
+  uint32_t interfaces = 0;
+  if (aAcc->IsHyperText() && aAcc->AsHyperText()->IsTextRole())
+    interfaces |= Interfaces::HYPERTEXT;
+
+  return interfaces;
+}
+
+static void
 SerializeTree(Accessible* aRoot, nsTArray<AccessibleData>& aTree)
 {
   uint64_t id = reinterpret_cast<uint64_t>(aRoot->UniqueID());
   uint32_t role = aRoot->Role();
   uint32_t childCount = aRoot->ChildCount();
+  uint32_t interfaces = InterfacesFor(aRoot);
 
   // OuterDocAccessibles are special because we don't want to serialize the
   // child doc here, we'll call PDocAccessibleConstructor in
   // NotificationController.
   if (childCount == 1 && aRoot->GetChildAt(0)->IsDoc())
     childCount = 0;
 
-  aTree.AppendElement(AccessibleData(id, role, childCount));
+  aTree.AppendElement(AccessibleData(id, role, childCount, interfaces));
   for (uint32_t i = 0; i < childCount; i++)
     SerializeTree(aRoot->GetChildAt(i), aTree);
 }
 
 void
 DocAccessibleChild::ShowEvent(AccShowEvent* aShowEvent)
 {
   Accessible* parent = aShowEvent->Parent();
@@ -112,10 +124,24 @@ DocAccessibleChild::RecvAttributes(const
     rv = propElem->GetValue(value);
     NS_ENSURE_SUCCESS(rv, false);
 
     aAttributes->AppendElement(Attribute(name, value));
     }
 
   return true;
 }
+
+bool
+DocAccessibleChild::RecvTextSubstring(const uint64_t& aID,
+                                      const int32_t& aStartOffset,
+                                      const int32_t& aEndOffset,
+                                      nsString* aText)
+{
+  Accessible* acc = mDoc->GetAccessibleByUniqueID((void*)aID);
+  if (!acc || !acc->IsHyperText())
+    return false;
+
+  acc->AsHyperText()->TextSubstring(aStartOffset, aEndOffset, *aText);
+  return true;
 }
 }
+}
--- a/accessible/ipc/DocAccessibleChild.h
+++ b/accessible/ipc/DocAccessibleChild.h
@@ -44,16 +44,20 @@ public:
   virtual bool RecvName(const uint64_t& aID, nsString* aName) MOZ_OVERRIDE;
 
   /*
    * Get the description for the accessible with given id.
    */
   virtual bool RecvDescription(const uint64_t& aID, nsString* aDesc) MOZ_OVERRIDE;
 
   virtual bool RecvAttributes(const uint64_t& aID, nsTArray<Attribute> *aAttributes) MOZ_OVERRIDE;
+  virtual bool RecvTextSubstring(const uint64_t& aID,
+                                 const int32_t& aStartOffset,
+                                 const int32_t& aEndOffset, nsString* aText)
+    MOZ_OVERRIDE;
 
 private:
   DocAccessible* mDoc;
 };
 
 }
 }
 
--- a/accessible/ipc/DocAccessibleParent.cpp
+++ b/accessible/ipc/DocAccessibleParent.cpp
@@ -2,16 +2,17 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "DocAccessibleParent.h"
 #include "nsAutoPtr.h"
 #include "mozilla/a11y/Platform.h"
+#include "ProxyAccessible.h"
 
 namespace mozilla {
 namespace a11y {
 
 bool
 DocAccessibleParent::RecvShowEvent(const ShowEventData& aData)
 {
   if (aData.NewTree().IsEmpty()) {
@@ -69,17 +70,17 @@ DocAccessibleParent::AddSubtree(ProxyAcc
     return 0;
   }
 
   auto role = static_cast<a11y::role>(newChild.Role());
   ProxyAccessible* newProxy =
     new ProxyAccessible(newChild.ID(), aParent, this, role);
   aParent->AddChildAt(aIdxInParent, newProxy);
   mAccessibles.PutEntry(newChild.ID())->mProxy = newProxy;
-  ProxyCreated(newProxy);
+  ProxyCreated(newProxy, newChild.Interfaces());
 
   uint32_t accessibles = 1;
   uint32_t kids = newChild.ChildrenCount();
   for (uint32_t i = 0; i < kids; i++) {
     uint32_t consumed = AddSubtree(newProxy, aNewTree, aIdx + accessibles, i);
     if (!consumed)
       return 0;
 
@@ -137,32 +138,34 @@ DocAccessibleParent::AddChildDoc(DocAcce
   ProxyAccessible* outerDoc = mAccessibles.GetEntry(aParentID)->mProxy;
   if (!outerDoc)
     return false;
 
   aChildDoc->mParent = outerDoc;
   outerDoc->SetChildDoc(aChildDoc);
   mChildDocs.AppendElement(aChildDoc);
   aChildDoc->mParentDoc = this;
-  ProxyCreated(aChildDoc);
+  ProxyCreated(aChildDoc, 0);
   return true;
 }
 
 PLDHashOperator
 DocAccessibleParent::ShutdownAccessibles(ProxyEntry* entry, void*)
 {
   ProxyDestroyed(entry->mProxy);
   return PL_DHASH_NEXT;
 }
 
 void
-DocAccessibleParent::ActorDestroy(ActorDestroyReason aWhy)
+DocAccessibleParent::Destroy()
 {
   MOZ_ASSERT(mChildDocs.IsEmpty(),
       "why wheren't the child docs destroyed already?");
+  MOZ_ASSERT(!mShutdown);
+  mShutdown = true;
 
   mAccessibles.EnumerateEntries(ShutdownAccessibles, nullptr);
   ProxyDestroyed(this);
   mParentDoc ? mParentDoc->RemoveChildDoc(this)
     : GetAccService()->RemoteDocShutdown(this);
 }
 }
 }
--- a/accessible/ipc/DocAccessibleParent.h
+++ b/accessible/ipc/DocAccessibleParent.h
@@ -21,17 +21,17 @@ namespace a11y {
  * These objects live in the main process and comunicate with and represent
  * an accessible document in a content process.
  */
 class DocAccessibleParent : public ProxyAccessible,
     public PDocAccessibleParent
 {
 public:
   DocAccessibleParent() :
-    ProxyAccessible(this), mParentDoc(nullptr)
+    ProxyAccessible(this), mParentDoc(nullptr), mShutdown(false)
   { MOZ_COUNT_CTOR_INHERITED(DocAccessibleParent, ProxyAccessible); }
   ~DocAccessibleParent()
   {
     MOZ_COUNT_DTOR_INHERITED(DocAccessibleParent, ProxyAccessible);
     MOZ_ASSERT(mChildDocs.Length() == 0);
     MOZ_ASSERT(!mParentDoc);
   }
 
@@ -40,17 +40,22 @@ public:
    * process it is firing an event.
    */
   virtual bool RecvEvent(const uint64_t& aID, const uint32_t& aType)
     MOZ_OVERRIDE;
 
   virtual bool RecvShowEvent(const ShowEventData& aData) MOZ_OVERRIDE;
   virtual bool RecvHideEvent(const uint64_t& aRootID) MOZ_OVERRIDE;
 
-  virtual void ActorDestroy(ActorDestroyReason aWhy) MOZ_OVERRIDE;
+  void Destroy();
+  virtual void ActorDestroy(ActorDestroyReason aWhy) MOZ_OVERRIDE
+  {
+    if (!mShutdown)
+      Destroy();
+  }
 
   /*
    * Return the main processes representation of the parent document (if any)
    * of the document this object represents.
    */
   DocAccessibleParent* Parent() const { return mParentDoc; }
 
   /*
@@ -110,14 +115,15 @@ private:
   nsTArray<DocAccessibleParent*> mChildDocs;
   DocAccessibleParent* mParentDoc;
 
   /*
    * Conceptually this is a map from IDs to proxies, but we store the ID in the
    * proxy object so we can't use a real map.
    */
   nsTHashtable<ProxyEntry> mAccessibles;
+  bool mShutdown;
 };
 
 }
 }
 
 #endif
--- a/accessible/ipc/PDocAccessible.ipdl
+++ b/accessible/ipc/PDocAccessible.ipdl
@@ -9,16 +9,17 @@ include protocol PContent;
 namespace mozilla {
 namespace a11y {
 
 struct AccessibleData
 {
   uint64_t ID;
   uint32_t Role;
   uint32_t ChildrenCount;
+  uint32_t Interfaces;
 };
 
 struct ShowEventData
 {
   uint64_t ID;
   uint32_t Idx;
   AccessibleData[] NewTree;
 };
@@ -44,12 +45,14 @@ parent:
   ShowEvent(ShowEventData data);
   HideEvent(uint64_t aRootID);
 
 child:
   prio(high) sync State(uint64_t aID) returns(uint64_t states);
   prio(high) sync Name(uint64_t aID) returns(nsString name);
   prio(high) sync Description(uint64_t aID) returns(nsString desc);
   prio(high) sync Attributes(uint64_t aID) returns(Attribute[] attributes);
+prio(high) sync TextSubstring(uint64_t aID, int32_t aStartOffset, int32_t
+                              aEndOffset) returns(nsString aText);
 };
 
 }
 }
--- a/accessible/ipc/ProxyAccessible.cpp
+++ b/accessible/ipc/ProxyAccessible.cpp
@@ -18,16 +18,21 @@ ProxyAccessible::Shutdown()
   MOZ_ASSERT(!mOuterDoc);
 
   // XXX Ideally  this wouldn't be necessary, but it seems OuterDoc accessibles
   // can be destroyed before the doc they own.
   if (!mOuterDoc) {
     uint32_t childCount = mChildren.Length();
     for (uint32_t idx = 0; idx < childCount; idx++)
       mChildren[idx]->Shutdown();
+  } else {
+    if (mChildren.Length() != 1)
+      MOZ_CRASH("outer doc doesn't own adoc!");
+
+    static_cast<DocAccessibleParent*>(mChildren[0])->Destroy();
   }
 
   mChildren.Clear();
   ProxyDestroyed(this);
   mDoc->RemoveAccessible(this);
 }
 
 void
@@ -64,10 +69,17 @@ ProxyAccessible::Description(nsString& a
   unused << mDoc->SendDescription(mID, &aDesc);
 }
 
 void
 ProxyAccessible::Attributes(nsTArray<Attribute> *aAttrs) const
 {
   unused << mDoc->SendAttributes(mID, aAttrs);
 }
+
+void
+ProxyAccessible::TextSubstring(int32_t aStartOffset, int32_t aEndOfset,
+                               nsString& aText) const
+{
+  unused << mDoc->SendTextSubstring(mID, aStartOffset, aEndOfset, &aText);
 }
 }
+}
--- a/accessible/ipc/ProxyAccessible.h
+++ b/accessible/ipc/ProxyAccessible.h
@@ -75,16 +75,22 @@ public:
   void Description(nsString& aDesc) const;
 
   /**
    * Get the set of attributes on the proxied accessible.
    */
   void Attributes(nsTArray<Attribute> *aAttrs) const;
 
   /**
+   * Get the text between the given offsets.
+   */
+  void TextSubstring(int32_t aStartOffset, int32_t aEndOfset,
+                     nsString& aText) const;
+
+  /**
    * Allow the platform to store a pointers worth of data on us.
    */
   uintptr_t GetWrapper() const { return mWrapper; }
   void SetWrapper(uintptr_t aWrapper) { mWrapper = aWrapper; }
 
   /*
    * Return the ID of the accessible being proxied.
    */
@@ -103,12 +109,17 @@ private:
   nsTArray<ProxyAccessible*> mChildren;
   DocAccessibleParent* mDoc;
   uintptr_t mWrapper;
   uint64_t mID;
   role mRole : 31;
   bool mOuterDoc : 1;
 };
 
+enum Interfaces
+{
+  HYPERTEXT = 1
+};
+
 }
 }
 
 #endif
--- a/accessible/mac/Platform.mm
+++ b/accessible/mac/Platform.mm
@@ -29,17 +29,17 @@ PlatformInit()
 }
 
 void
 PlatformShutdown()
 {
 }
 
 void
-ProxyCreated(ProxyAccessible*)
+ProxyCreated(ProxyAccessible*, uint32_t)
 {
 }
 
 void
 ProxyDestroyed(ProxyAccessible*)
 {
 }
 
--- a/accessible/other/Platform.cpp
+++ b/accessible/other/Platform.cpp
@@ -15,17 +15,17 @@ a11y::PlatformInit()
 }
 
 void
 a11y::PlatformShutdown()
 {
 }
 
 void
-a11y::ProxyCreated(ProxyAccessible*)
+a11y::ProxyCreated(ProxyAccessible*, uint32_t)
 {
 }
 
 void
 a11y::ProxyDestroyed(ProxyAccessible*)
 {
 }
 
--- a/accessible/windows/msaa/Platform.cpp
+++ b/accessible/windows/msaa/Platform.cpp
@@ -30,17 +30,17 @@ void
 a11y::PlatformShutdown()
 {
   ::DestroyCaret();
 
   nsWinUtils::ShutdownWindowEmulation();
 }
 
 void
-a11y::ProxyCreated(ProxyAccessible*)
+a11y::ProxyCreated(ProxyAccessible*, uint32_t)
 {
 }
 
 void
 a11y::ProxyDestroyed(ProxyAccessible*)
 {
 }
 
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -725,32 +725,40 @@ pref("hal.processPriorityManager.gonk.BA
 pref("hal.processPriorityManager.gonk.BACKGROUND_HOMESCREEN.KillUnderKB", 8192);
 pref("hal.processPriorityManager.gonk.BACKGROUND_HOMESCREEN.cgroup", "apps/bg_non_interactive");
 
 pref("hal.processPriorityManager.gonk.BACKGROUND.OomScoreAdjust", 667);
 pref("hal.processPriorityManager.gonk.BACKGROUND.KillUnderKB", 20480);
 pref("hal.processPriorityManager.gonk.BACKGROUND.cgroup", "apps/bg_non_interactive");
 
 // Control group definitions (i.e., CPU priority groups) for B2G processes.
+//
+// memory_swappiness -   0 - The kernel will swap only to avoid an out of memory condition
+// memory_swappiness -  60 - The default value.
+// memory_swappiness - 100 - The kernel will swap aggressively.
 
 // Foreground apps
 pref("hal.processPriorityManager.gonk.cgroups.apps.cpu_shares", 1024);
 pref("hal.processPriorityManager.gonk.cgroups.apps.cpu_notify_on_migrate", 1);
+pref("hal.processPriorityManager.gonk.cgroups.apps.memory_swappiness", 10);
 
 // Foreground apps with high priority, 16x more CPU than foreground ones
 pref("hal.processPriorityManager.gonk.cgroups.apps/critical.cpu_shares", 16384);
 pref("hal.processPriorityManager.gonk.cgroups.apps/critical.cpu_notify_on_migrate", 1);
+pref("hal.processPriorityManager.gonk.cgroups.apps/critical.memory_swappiness", 0);
 
 // Background perceivable apps, ~10x less CPU than foreground ones
 pref("hal.processPriorityManager.gonk.cgroups.apps/bg_perceivable.cpu_shares", 103);
 pref("hal.processPriorityManager.gonk.cgroups.apps/bg_perceivable.cpu_notify_on_migrate", 0);
+pref("hal.processPriorityManager.gonk.cgroups.apps/bg_perceivable.memory_swappiness", 60);
 
 // Background apps, ~20x less CPU than foreground ones and ~2x less than perceivable ones
 pref("hal.processPriorityManager.gonk.cgroups.apps/bg_non_interactive.cpu_shares", 52);
 pref("hal.processPriorityManager.gonk.cgroups.apps/bg_non_interactive.cpu_notify_on_migrate", 0);
+pref("hal.processPriorityManager.gonk.cgroups.apps/bg_non_interactive.memory_swappiness", 100);
 
 // By default the compositor thread on gonk runs without real-time priority.  RT
 // priority can be enabled by setting this pref to a value between 1 and 99.
 // Note that audio processing currently runs at RT priority 2 or 3 at most.
 //
 // If RT priority is disabled, then the compositor nice value is used. We prefer
 // to use a nice value of -4, which matches Android's preferences. Setting a preference
 // of RT priority 1 would mean it is higher than audio, which is -16. The compositor
@@ -1075,8 +1083,11 @@ pref("dom.mozSettings.SettingsDB.verbose
 pref("dom.mozSettings.SettingsManager.verbose.enabled", false);
 pref("dom.mozSettings.SettingsRequestManager.verbose.enabled", false);
 pref("dom.mozSettings.SettingsService.verbose.enabled", false);
 
 // Controlling whether we want to allow forcing some Settings
 // IndexedDB transactions to be opened as readonly or keep everything as
 // readwrite.
 pref("dom.mozSettings.allowForceReadOnly", false);
+
+// RequestSync API is enabled by default on B2G.
+pref("dom.requestSync.enabled", true);
--- a/b2g/app/nsBrowserApp.cpp
+++ b/b2g/app/nsBrowserApp.cpp
@@ -22,17 +22,19 @@
 #include "nsCOMPtr.h"
 #include "nsIFile.h"
 #include "nsStringGlue.h"
 
 #ifdef XP_WIN
 // we want a wmain entry point
 #define XRE_DONT_SUPPORT_XPSP2 // See https://bugzil.la/1023941#c32
 #include "nsWindowsWMain.cpp"
+#if defined(_MSC_VER) && (_MSC_VER < 1900)
 #define snprintf _snprintf
+#endif
 #define strcasecmp _stricmp
 #endif
 
 #ifdef MOZ_WIDGET_GONK
 #include "GonkDisplay.h"
 #endif
 
 #include "BinaryPath.h"
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -10,28 +10,30 @@ Cu.import('resource://gre/modules/AlarmS
 Cu.import('resource://gre/modules/ActivitiesService.jsm');
 Cu.import('resource://gre/modules/NotificationDB.jsm');
 Cu.import('resource://gre/modules/Payment.jsm');
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 Cu.import('resource://gre/modules/UserAgentOverrides.jsm');
 Cu.import('resource://gre/modules/Keyboard.jsm');
 Cu.import('resource://gre/modules/ErrorPage.jsm');
 Cu.import('resource://gre/modules/AlertsHelper.jsm');
+Cu.import('resource://gre/modules/RequestSyncService.jsm');
 #ifdef MOZ_WIDGET_GONK
 Cu.import('resource://gre/modules/NetworkStatsService.jsm');
 Cu.import('resource://gre/modules/ResourceStatsService.jsm');
 #endif
 
 // Identity
 Cu.import('resource://gre/modules/SignInToWebsite.jsm');
 SignInToWebsiteController.init();
 
 Cu.import('resource://gre/modules/FxAccountsMgmtService.jsm');
 Cu.import('resource://gre/modules/DownloadsAPI.jsm');
 Cu.import('resource://gre/modules/MobileIdentityManager.jsm');
+Cu.import('resource://gre/modules/PresentationDeviceInfoManager.jsm');
 
 XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
                                   "resource://gre/modules/SystemAppProxy.jsm");
 
 Cu.import('resource://gre/modules/Webapps.jsm');
 DOMApplicationRegistry.allAppsLaunchable = true;
 
 XPCOMUtils.defineLazyServiceGetter(Services, 'env',
@@ -361,113 +363,69 @@ var shell = {
     this.contentBrowser.removeEventListener('mozbrowserscrollviewchange', this, true);
     this.contentBrowser.removeEventListener('mozbrowsertouchcarettap', this, true);
     ppmm.removeMessageListener("content-handler", this);
 
     UserAgentOverrides.uninit();
     IndexedDBPromptHelper.uninit();
   },
 
-  // If this key event actually represents a hardware button, send a
-  // mozChromeEvent with detail.type set to 'xxx-button-press' or
-  // 'xxx-button-release' instead. Note that no more mozChromeEvent for hardware
-  // buttons needed after Bug 1014418 is landed.
-  filterHardwareKeys: function shell_filterHardwareKeys(evt) {
-    var type;
-    switch (evt.keyCode) {
-      case evt.DOM_VK_HOME:         // Home button
-        type = 'home-button';
-        break;
-      case evt.DOM_VK_SLEEP:        // Sleep button
-      case evt.DOM_VK_END:          // On desktop we don't have a sleep button
-        type = 'sleep-button';
-        break;
-      case evt.DOM_VK_VOLUME_UP:      // Volume up button
-        type = 'volume-up-button';
-        break;
-      case evt.DOM_VK_VOLUME_DOWN:    // Volume down button
-        type = 'volume-down-button';
-        break;
-      case evt.DOM_VK_ESCAPE:       // Back button (should be disabled)
-        type = 'back-button';
-        break;
-      case evt.DOM_VK_CONTEXT_MENU: // Menu button
-        type = 'menu-button';
-        break;
-      case evt.DOM_VK_F1: // headset button
-        type = 'headset-button';
-        break;
-    }
+  // If this key event represents a hardware button which needs to be send as
+  // a message, broadcasts it with the message set to 'xxx-button-press' or
+  // 'xxx-button-release'.
+  broadcastHardwareKeys: function shell_broadcastHardwareKeys(evt) {
+    let type;
+    let message;
 
     let mediaKeys = {
       'MediaTrackNext': 'media-next-track-button',
       'MediaTrackPrevious': 'media-previous-track-button',
       'MediaPause': 'media-pause-button',
       'MediaPlay': 'media-play-button',
       'MediaPlayPause': 'media-play-pause-button',
       'MediaStop': 'media-stop-button',
       'MediaRewind': 'media-rewind-button',
       'MediaFastForward': 'media-fast-forward-button'
     };
 
-    let isMediaKey = false;
-    if (mediaKeys[evt.key]) {
-      isMediaKey = true;
-      type = mediaKeys[evt.key];
-    }
-
-    // The key doesn't represent a hardware button, so no mozChromeEvent.
-    if (!type) {
+    if (evt.keyCode == evt.DOM_VK_F1) {
+      type = 'headset-button';
+      message = 'headset-button';
+    } else if (mediaKeys[evt.key]) {
+      type = 'media-button';
+      message = mediaKeys[evt.key];
+    } else {
       return;
     }
 
     switch (evt.type) {
       case 'keydown':
-        type = type + '-press';
+        message = message + '-press';
         break;
       case 'keyup':
-        type = type + '-release';
+        message = message + '-release';
         break;
     }
 
-    // Let applications receive the headset button key press/release event.
-    if (evt.keyCode == evt.DOM_VK_F1 && type !== this.lastHardwareButtonEventType) {
-      this.lastHardwareButtonEventType = type;
-      gSystemMessenger.broadcastMessage('headset-button', type);
-      return;
-    }
-
-    if (isMediaKey) {
-      this.lastHardwareButtonEventType = type;
-      gSystemMessenger.broadcastMessage('media-button', type);
-      return;
-    }
-
-    // On my device, the physical hardware buttons (sleep and volume)
-    // send multiple events (press press release release), but the
-    // soft home button just sends one.  This hack is to manually
-    // "debounce" the keys. If the type of this event is the same as
-    // the type of the last one, then don't send it.  We'll never send
-    // two presses or two releases in a row.
-    // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=761067
-    if (type !== this.lastHardwareButtonEventType) {
-      this.lastHardwareButtonEventType = type;
-      this.sendChromeEvent({type: type});
+    // Let applications receive the headset button and media key press/release message.
+    if (message !== this.lastHardwareButtonMessage) {
+      this.lastHardwareButtonMessage = message;
+      gSystemMessenger.broadcastMessage(type, message);
     }
   },
 
-  lastHardwareButtonEventType: null, // property for the hack above
+  lastHardwareButtonMessage: null, // property for the hack above
   visibleNormalAudioActive: false,
 
   handleEvent: function shell_handleEvent(evt) {
     let content = this.contentBrowser.contentWindow;
     switch (evt.type) {
       case 'keydown':
       case 'keyup':
-        this.filterHardwareKeys(evt);
+        this.broadcastHardwareKeys(evt);
         break;
       case 'mozfullscreenchange':
         // When the screen goes fullscreen make sure to set the focus to the
         // main window so noboby can prevent the ESC key to get out fullscreen
         // mode
         if (document.mozFullScreen)
           Services.fm.focusedWindow = window;
         break;
--- a/b2g/components/B2GComponents.manifest
+++ b/b2g/components/B2GComponents.manifest
@@ -101,8 +101,12 @@ category command-line-handler m-b2gcmds 
 # MobileIdentityUIGlue.js
 component {83dbe26a-81f3-4a75-9541-3d0b7ca496b5} MobileIdentityUIGlue.js
 contract @mozilla.org/services/mobileid-ui-glue;1 {83dbe26a-81f3-4a75-9541-3d0b7ca496b5}
 
 # B2GAppMigrator.js
 component {7211ece0-b458-4635-9afc-f8d7f376ee95} B2GAppMigrator.js
 contract @mozilla.org/app-migrator;1 {7211ece0-b458-4635-9afc-f8d7f376ee95}
 
+# B2GPresentationDevicePrompt.js
+component {4a300c26-e99b-4018-ab9b-c48cf9bc4de1} B2GPresentationDevicePrompt.js
+contract @mozilla.org/presentation-device/prompt;1 {4a300c26-e99b-4018-ab9b-c48cf9bc4de1}
+
new file mode 100644
--- /dev/null
+++ b/b2g/components/B2GPresentationDevicePrompt.js
@@ -0,0 +1,87 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* 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";
+
+function debug(aMsg) {
+  //dump("-*- B2GPresentationDevicePrompt: " + aMsg + "\n");
+}
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+const kB2GPRESENTATIONDEVICEPROMPT_CONTRACTID = "@mozilla.org/presentation-device/prompt;1";
+const kB2GPRESENTATIONDEVICEPROMPT_CID        = Components.ID("{4a300c26-e99b-4018-ab9b-c48cf9bc4de1}");
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "SystemAppProxy",
+                                  "resource://gre/modules/SystemAppProxy.jsm");
+
+function B2GPresentationDevicePrompt() {}
+
+B2GPresentationDevicePrompt.prototype = {
+  classID: kB2GPRESENTATIONDEVICEPROMPT_CID,
+  contractID: kB2GPRESENTATIONDEVICEPROMPT_CONTRACTID,
+  classDescription: "B2G Presentation Device Prompt",
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevicePrompt]),
+
+  // nsIPresentationDevicePrompt
+  promptDeviceSelection: function(aRequest) {
+    let self = this;
+    let requestId = Cc["@mozilla.org/uuid-generator;1"]
+                      .getService(Ci.nsIUUIDGenerator).generateUUID().toString();
+
+    SystemAppProxy.addEventListener("mozContentEvent", function contentEvent(aEvent) {
+      let detail = aEvent.detail;
+      if (detail.id !== requestId) {
+        return;
+      }
+
+      SystemAppProxy.removeEventListener("mozContentEvent", contentEvent);
+
+      switch (detail.type) {
+        case "presentation-select-result":
+          debug("device " + detail.deviceId + " is selected by user");
+          let device = self._getDeviceById(detail.deviceId);
+          if (!device) {
+            debug("cancel request because device is not found");
+            aRequest.cancel();
+          }
+          aRequest.select(device);
+          break;
+        case "presentation-select-deny":
+          debug("request canceled by user");
+          aRequest.cancel();
+          break;
+      }
+    });
+
+    let detail = {
+      type: "presentation-select-device",
+      origin: aRequest.origin,
+      requestURL: aRequest.requestURL,
+      id: requestId,
+    };
+
+    SystemAppProxy.dispatchEvent(detail);
+  },
+
+  _getDeviceById: function(aDeviceId) {
+    let deviceManager = Cc["@mozilla.org/presentation-device/manager;1"]
+                          .getService(Ci.nsIPresentationDeviceManager);
+    let devices = deviceManager.getAvailableDevices().QueryInterface(Ci.nsIArray);
+
+    for (let i = 0; i < devices.length; i++) {
+      let device = devices.queryElementAt(i, Ci.nsIPresentationDevice);
+      if (device.id === aDeviceId) {
+        return device;
+      }
+    }
+
+    return null;
+  },
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([B2GPresentationDevicePrompt]);
--- a/b2g/components/moz.build
+++ b/b2g/components/moz.build
@@ -6,16 +6,17 @@
 
 DIRS += ['test']
 
 EXTRA_COMPONENTS += [
     'ActivitiesGlue.js',
     'AlertsService.js',
     'B2GAboutRedirector.js',
     'B2GAppMigrator.js',
+    'B2GPresentationDevicePrompt.js',
     'ContentPermissionPrompt.js',
     'FilePicker.js',
     'FxAccountsUIGlue.js',
     'HelperAppDialog.js',
     'InterAppCommUIGlue.js',
     'MailtoProtocolHandler.js',
     'MobileIdentityUIGlue.js',
     'OMAContentHandler.js',
--- a/b2g/components/test/mochitest/mochitest.ini
+++ b/b2g/components/test/mochitest/mochitest.ini
@@ -1,14 +1,16 @@
 [DEFAULT]
 run-if = toolkit == "gonk"
 support-files =
   permission_handler_chrome.js
   SandboxPromptTest.html
   filepicker_path_handler_chrome.js
   systemapp_helper.js
+  presentation_prompt_handler_chrome.js
 
 [test_filepicker_path.html]
 [test_permission_deny.html]
 [test_permission_gum_remember.html]
 skip-if = true # Bug 1019572 - frequent timeouts
 [test_sandbox_permission.html]
 [test_systemapp.html]
+[test_presentation_device_prompt.html]
new file mode 100644
--- /dev/null
+++ b/b2g/components/test/mochitest/presentation_prompt_handler_chrome.js
@@ -0,0 +1,94 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+'use strict';
+
+ function debug(str) {
+   dump('presentation_prompt_handler_chrome: ' + str + '\n');
+ }
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+const { XPCOMUtils } = Cu.import('resource://gre/modules/XPCOMUtils.jsm');
+const { SystemAppProxy } = Cu.import('resource://gre/modules/SystemAppProxy.jsm');
+
+const manager = Cc["@mozilla.org/presentation-device/manager;1"]
+                  .getService(Ci.nsIPresentationDeviceManager);
+
+const prompt = Cc['@mozilla.org/presentation-device/prompt;1']
+                 .getService(Ci.nsIPresentationDevicePrompt);
+
+function TestPresentationDevice(options) {
+  this.id = options.id;
+  this.name = options.name;
+  this.type = options.type;
+}
+
+TestPresentationDevice.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDevice]),
+  establishSessionTransport: function() {
+    return null;
+  },
+};
+
+function TestPresentationRequest(options) {
+  this.origin = options.origin;
+  this.requestURL = options.requestURL;
+}
+
+TestPresentationRequest.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPresentationDeviceRequest]),
+  select: function(device) {
+    let result = {
+      type: 'select',
+      device: {
+        id: device.id,
+        name: device.name,
+        type: device.type,
+      },
+    };
+    sendAsyncMessage('presentation-select-result', result);
+  },
+  cancel: function() {
+    let result = {
+      type: 'cancel',
+    };
+    sendAsyncMessage('presentation-select-result', result);
+  },
+};
+
+var testDevice = null;
+
+addMessageListener('setup', function(device_options) {
+  testDevice = new TestPresentationDevice(device_options);
+  manager.QueryInterface(Ci.nsIPresentationDeviceListener).addDevice(testDevice);
+  sendAsyncMessage('setup-complete');
+});
+
+let eventHandler = function(evt) {
+  if (!evt.detail || evt.detail.type !== 'presentation-select-device') {
+    return;
+  }
+
+  sendAsyncMessage('presentation-select-device', evt.detail);
+};
+
+SystemAppProxy.addEventListener('mozChromeEvent', eventHandler);
+
+// need to remove ChromeEvent listener after test finished.
+addMessageListener('teardown', function() {
+  if (testDevice) {
+    manager.removeDevice(testDevice);
+  }
+  SystemAppProxy.removeEventListener('mozChromeEvent', eventHandler);
+});
+
+addMessageListener('trigger-device-prompt', function(request_options) {
+  let request = new TestPresentationRequest(request_options);
+  prompt.promptDeviceSelection(request);
+});
+
+addMessageListener('presentation-select-response', function(detail) {
+  SystemAppProxy._sendCustomEvent('mozContentEvent', detail);
+});
+
new file mode 100644
--- /dev/null
+++ b/b2g/components/test/mochitest/test_presentation_device_prompt.html
@@ -0,0 +1,145 @@
+<!DOCTYPE HTML>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+   - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Presentation Device Selection</title>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Test for Presentation Device Selection</a>
+<script type="application/javascript;version=1.8">
+
+'use strict';
+
+SimpleTest.waitForExplicitFinish();
+
+var contentEventHandler = null;
+
+var gUrl = SimpleTest.getTestFileURL('presentation_prompt_handler_chrome.js');
+var gScript = SpecialPowers.loadChromeScript(gUrl);
+
+function testSetup() {
+  info('setup for device selection');
+  return new Promise(function(resolve, reject) {
+    let device = {
+      id: 'test-id',
+      name: 'test-name',
+      type: 'test-type',
+    };
+    gScript.addMessageListener('setup-complete', function() {
+      resolve(device);
+    });
+    gScript.sendAsyncMessage('setup', device);
+  });
+}
+
+function testSelected(device) {
+  info('test device selected by user');
+  return new Promise(function(resolve, reject) {
+    let request = {
+      origin: 'test-origin',
+      requestURL: 'test-requestURL',
+    };
+
+    gScript.addMessageListener('presentation-select-device', function contentEventHandler(detail) {
+      gScript.removeMessageListener('presentation-select-device', contentEventHandler);
+      ok(true, 'receive user prompt for device selection');
+      is(detail.origin, request.origin, 'expected origin');
+      is(detail.requestURL, request.requestURL, 'expected requestURL');
+      let response = {
+        id: detail.id,
+        type: 'presentation-select-result',
+        deviceId: device.id,
+      };
+      gScript.sendAsyncMessage('presentation-select-response', response);
+
+      gScript.addMessageListener('presentation-select-result', function resultHandler(result) {
+        gScript.removeMessageListener('presentation-select-result', resultHandler);
+        is(result.type, 'select', 'expect device selected');
+        is(result.device.id, device.id, 'expected device id');
+        is(result.device.name, device.name, 'expected device name');
+        is(result.device.type, device.type, 'expected devcie type');
+        resolve();
+      });
+    });
+
+    gScript.sendAsyncMessage('trigger-device-prompt', request);
+  });
+}
+
+function testSelectedNotExisted() {
+  info('test selected device doesn\'t exist');
+  return new Promise(function(resolve, reject) {
+    gScript.addMessageListener('presentation-select-device', function contentEventHandler(detail) {
+      gScript.removeMessageListener('presentation-select-device', contentEventHandler);
+      ok(true, 'receive user prompt for device selection');
+      let response = {
+        id: detail.id,
+        type: 'presentation-select-deny',
+        deviceId: undefined, // simulate device Id that doesn't exist
+      };
+      gScript.sendAsyncMessage('presentation-select-response', response);
+
+      gScript.addMessageListener('presentation-select-result', function resultHandler(result) {
+        gScript.removeMessageListener('presentation-select-result', resultHandler);
+        is(result.type, 'cancel', 'expect user cancel');
+        resolve();
+      });
+    });
+
+    let request = {
+      origin: 'test-origin',
+      requestURL: 'test-requestURL',
+    };
+    gScript.sendAsyncMessage('trigger-device-prompt', request);
+  });
+}
+
+function testDenied() {
+  info('test denial of device selection');
+  return new Promise(function(resolve, reject) {
+    gScript.addMessageListener('presentation-select-device', function contentEventHandler(detail) {
+      gScript.removeMessageListener('presentation-select-device', contentEventHandler);
+      ok(true, 'receive user prompt for device selection');
+      let response = {
+        id: detail.id,
+        type: 'presentation-select-deny',
+      };
+      gScript.sendAsyncMessage('presentation-select-response', response);
+
+      gScript.addMessageListener('presentation-select-result', function resultHandler(result) {
+        gScript.removeMessageListener('presentation-select-result', resultHandler);
+        is(result.type, 'cancel', 'expect user cancel');
+        resolve();
+      });
+    });
+
+    let request = {
+      origin: 'test-origin',
+      requestURL: 'test-requestURL',
+    };
+    gScript.sendAsyncMessage('trigger-device-prompt', request);
+  });
+}
+
+function runTests() {
+  testSetup()
+  .then(testSelected)
+  .then(testSelectedNotExisted)
+  .then(testDenied)
+  .then(function() {
+    info('test finished, teardown');
+    gScript.sendAsyncMessage('teardown');
+    gScript.destroy();
+    SimpleTest.finish();
+  });
+}
+
+window.addEventListener('load', runTests);
+</script>
+</pre>
+</body>
+</html>
--- a/b2g/config/dolphin/config.json
+++ b/b2g/config/dolphin/config.json
@@ -1,23 +1,25 @@
 {
     "config_version": 2,
+    "tooltool_manifest": "releng-dolphin.tt",
     "mock_target": "mozilla-centos6-x86_64",
     "mock_packages": ["ccache", "make", "bison", "flex", "gcc", "g++", "mpfr", "zlib-devel", "ncurses-devel", "zip", "autoconf213", "glibc-static", "perl-Digest-SHA", "wget", "alsa-lib", "atk", "cairo", "dbus-glib", "fontconfig", "freetype", "glib2", "gtk2", "libXRender", "libXt", "pango", "mozilla-python27-mercurial", "openssh-clients", "nss-devel", "glibc-devel.i686", "libstdc++.i686", "zlib-devel.i686", "ncurses-devel.i686", "libX11-devel.i686", "mesa-libGL-devel.i686", "mesa-libGL-devel", "libX11-devel", "git", "libxml2", "bc"],
     "mock_files": [["/home/cltbld/.ssh", "/home/mock_mozilla/.ssh"]],
     "build_targets": ["kernelheader", ""],
     "upload_files": [
         "{objdir}/dist/b2g-*.crashreporter-symbols.zip",
         "{objdir}/dist/b2g-*.tar.gz",
         "{workdir}/sources.xml"
     ],
     "public_upload_files": [
         "{objdir}/dist/b2g-*.crashreporter-symbols.zip",
         "{objdir}/dist/b2g-*.tar.gz",
-        "{workdir}/sources.xml"
+        "{workdir}/sources.xml",
+        "{objdir}/dist/b2g-update/*.mar"
     ],
     "zip_files": [
         ["{workdir}/out/target/product/scx15_sp7715ga/*.img", "out/target/product/scx15_sp7715ga/"],
         "{workdir}/flash.sh",
         "{workdir}/load-config.sh",
         "{workdir}/.config",
         "{workdir}/sources.xml",
         "{workdir}/profile.sh",
new file mode 100644
--- /dev/null
+++ b/b2g/config/dolphin/releng-dolphin.tt
@@ -0,0 +1,9 @@
+[
+{
+"size": 80458572,
+"digest": "e5101f9dee1e462f6cbd3897ea57eede41d23981825c7b20d91d23ab461875d54d3dfc24999aa58a31e8b01f49fb3140e05ffe5af2957ef1d1afb89fd0dfe1ad",
+"algorithm": "sha512",
+"filename": "gcc.tar.xz",
+"unpack": "True"
+}
+]
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,25 +10,25 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e0c735ec89df011ea7dd435087a9045ecff9ff9e">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="9946a490a9264b42e65385d703b28fa055ab2d42"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="ebc90190771a945d405f5d36efd813db6f77f965"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="049c281ad212bf528b2af8fc246b0dd0c9f97415"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="3f4cd30032f7d9002421bdb78860c28c78760888"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="8b880805d454664b3eed11d0f053cdeafa1ff06e"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="a1e239a0bb5cd1d69680bf1075883aa9a7bf2429"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="c7931763d41be602407ed9d71e2c0292c6597e00"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="83760d213fb3bec7b4117d266fcfbf6fe2ba14ab"/>
   <project name="device/common" path="device/common" revision="6a2995683de147791e516aae2ccb31fdfbe2ad30"/>
@@ -111,17 +111,17 @@
   <project name="platform/libcore" path="libcore" revision="e195beab082c09217318fc19250caeaf4c1bd800"/>
   <project name="platform/libnativehelper" path="libnativehelper" revision="feeb36c2bd4adfe285f98f5de92e0f3771b2c115"/>
   <project name="platform/ndk" path="ndk" revision="e58ef003be4306bb53a8c11331146f39e4eab31f"/>
   <project name="platform_prebuilts_misc" path="prebuilts/misc" remote="b2g" revision="0e7c060db684b409616fe67ea433ef19f5634c60"/>
   <project name="platform/prebuilts/ndk" path="prebuilts/ndk" revision="c792f0bd9fff7aea2887c60bbb3a9bbdb534ffa3"/>
   <project name="platform_prebuilts_qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="f7d9bf71cf6693474f3f2a81a4ba62c0fc5646aa"/>
   <project name="platform/prebuilts/sdk" path="prebuilts/sdk" revision="cfcef469537869947abb9aa1d656774cc2678d4c"/>
   <project name="platform/prebuilts/tools" path="prebuilts/tools" revision="5a48c04c4bb5f079bc757e29864a42427378e051"/>
-  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="288db53ad77084bd44791add5e3a4c266a6e9c60"/>
+  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="aa3adea9c18ae00d36e597d5890cf14cb7dfb105"/>
   <project name="platform/system/extras" path="system/extras" revision="10e78a05252b3de785f88c2d0b9ea8a428009c50"/>
   <project name="platform/system/media" path="system/media" revision="7ff72c2ea2496fa50b5e8a915e56e901c3ccd240"/>
   <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="fe95bc6f83af5c18a73aa86c96e7fa7f79b91477"/>
   <project name="platform/system/netd" path="system/netd" revision="3ae56364946d4a5bf5a5f83f12f9a45a30398e33"/>
   <project name="platform/system/security" path="system/security" revision="ee8068b9e7bfb2770635062fc9c2035be2142bd8"/>
   <project name="platform/system/vold" path="system/vold" revision="2e43efe1b30d0b98574d293059556aebd2f46454"/>
   <!--original fetch url was http://sprdsource.spreadtrum.com:8085/b2g/android-->
   <remote fetch="https://git.mozilla.org/external/sprd-aosp" name="sprd-aosp"/>
@@ -134,12 +134,12 @@
   <project name="platform/frameworks/av" path="frameworks/av" revision="4387fe988e5a1001f29ce05fcfda03ed2d32137b"/>
   <project name="platform/hardware/akm" path="hardware/akm" revision="6d3be412647b0eab0adff8a2768736cf4eb68039"/>
   <project groups="invensense" name="platform/hardware/invensense" path="hardware/invensense" revision="e6d9ab28b4f4e7684f6c07874ee819c9ea0002a2"/>
   <project name="platform/hardware/ril" path="hardware/ril" revision="865ce3b4a2ba0b3a31421ca671f4d6c5595f8690"/>
   <project name="kernel/common" path="kernel" revision="6c6f012cea17fb8b3263605737816cf6663432f1"/>
   <project name="platform/system/core" path="system/core" revision="53d584d4a4b4316e4de9ee5f210d662f89b44e7e"/>
   <project name="u-boot" path="u-boot" revision="5167e5eec5cb6b3147839da158637e6d953a4e4f"/>
   <project name="vendor/sprd/gps" path="vendor/sprd/gps" revision="6974f8e771d4d8e910357a6739ab124768891e8f"/>
-  <project name="vendor/sprd/open-source" path="vendor/sprd/open-source" revision="a6f913f0e114945d995680a6fe5cccb24c02b3c2"/>
+  <project name="vendor/sprd/open-source" path="vendor/sprd/open-source" revision="f56ab768cb9f1ad42fb0809ffec1424b1e693369"/>
   <project name="vendor/sprd/partner" path="vendor/sprd/partner" revision="8649c7145972251af11b0639997edfecabfc7c2e"/>
   <project name="vendor/sprd/proprietories" path="vendor/sprd/proprietories" revision="d2466593022f7078aaaf69026adf3367c2adb7bb"/>
 </manifest>
--- a/b2g/config/emulator-ics/releng-emulator-ics.tt
+++ b/b2g/config/emulator-ics/releng-emulator-ics.tt
@@ -1,2 +1,9 @@
 [
+{
+"size": 80458572,
+"digest": "e5101f9dee1e462f6cbd3897ea57eede41d23981825c7b20d91d23ab461875d54d3dfc24999aa58a31e8b01f49fb3140e05ffe5af2957ef1d1afb89fd0dfe1ad",
+"algorithm": "sha512",
+"filename": "gcc.tar.xz",
+"unpack": "True"
+}
 ]
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,23 +14,23 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="df362ace56338da8173d30d3e09e08c42c1accfa">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="9946a490a9264b42e65385d703b28fa055ab2d42"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="ebc90190771a945d405f5d36efd813db6f77f965"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="049c281ad212bf528b2af8fc246b0dd0c9f97415"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d5d3f93914558b6f168447b805cd799c8233e300"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="6fa7a4936414ceb4055fd27f7a30e76790f834fb"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="3f4cd30032f7d9002421bdb78860c28c78760888"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform_bionic" path="bionic" remote="b2g" revision="e2b3733ba3fa5e3f404e983d2e4142b1f6b1b846"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="425f8b5fadf5889834c5acd27d23c9e0b2129c28"/>
   <project name="device/common" path="device/common" revision="42b808b7e93d0619286ae8e59110b176b7732389"/>
   <project name="device/sample" path="device/sample" revision="237bd668d0f114d801a8d6455ef5e02cc3577587"/>
   <project name="platform_external_apriori" path="external/apriori" remote="b2g" revision="11816ad0406744f963537b23d68ed9c2afb412bd"/>
   <project name="platform/external/bluetooth/bluez" path="external/bluetooth/bluez" revision="52a1a862a8bac319652b8f82d9541ba40bfa45ce"/>
--- a/b2g/config/emulator-jb/releng-emulator-jb.tt
+++ b/b2g/config/emulator-jb/releng-emulator-jb.tt
@@ -1,2 +1,9 @@
 [
+{
+"size": 80458572,
+"digest": "e5101f9dee1e462f6cbd3897ea57eede41d23981825c7b20d91d23ab461875d54d3dfc24999aa58a31e8b01f49fb3140e05ffe5af2957ef1d1afb89fd0dfe1ad",
+"algorithm": "sha512",
+"filename": "gcc.tar.xz",
+"unpack": "True"
+}
 ]
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,20 +12,20 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="0e94c080bee081a50aa2097527b0b40852f9143f">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="9946a490a9264b42e65385d703b28fa055ab2d42"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="ebc90190771a945d405f5d36efd813db6f77f965"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="049c281ad212bf528b2af8fc246b0dd0c9f97415"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="3f4cd30032f7d9002421bdb78860c28c78760888"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="b89fda71fcd0fa0cf969310e75be3ea33e048b44"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="2e7d5348f35575870b3c7e567a9a9f6d66f8d6c5"/>
@@ -113,17 +113,17 @@
   <project name="platform/libnativehelper" path="libnativehelper" revision="4792069e90385889b0638e97ae62c67cdf274e22"/>
   <project name="platform/ndk" path="ndk" revision="7666b97bbaf1d645cdd6b4430a367b7a2bb53369"/>
   <project name="platform/prebuilts/misc" path="prebuilts/misc" revision="f6ab40b3257abc07741188fd173ac392575cc8d2"/>
   <project name="platform/prebuilts/ndk" path="prebuilts/ndk" revision="e52099755d0bd3a579130eefe8e58066cc6c0cb6"/>
   <project name="platform_prebuilts_qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="02c32feb2fe97037be0ac4dace3a6a5025ac895d"/>
   <project name="platform/prebuilts/sdk" path="prebuilts/sdk" revision="842e33e43a55ea44833b9e23e4d180fa17c843af"/>
   <project name="platform/prebuilts/tools" path="prebuilts/tools" revision="5db24726f0f42124304195a6bdea129039eeeaeb"/>
   <project name="platform/system/bluetooth" path="system/bluetooth" revision="930ae098543881f47eac054677726ee4b998b2f8"/>
-  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="288db53ad77084bd44791add5e3a4c266a6e9c60"/>
+  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="aa3adea9c18ae00d36e597d5890cf14cb7dfb105"/>
   <project name="platform_system_core" path="system/core" remote="b2g" revision="542d1f59dc331b472307e5bd043101d14d5a3a3e"/>
   <project name="platform/system/extras" path="system/extras" revision="18c1180e848e7ab8691940481f5c1c8d22c37b3e"/>
   <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="fe95bc6f83af5c18a73aa86c96e7fa7f79b91477"/>
   <project name="platform/system/media" path="system/media" revision="d90b836f66bf1d9627886c96f3a2d9c3007fbb80"/>
   <project name="platform/system/netd" path="system/netd" revision="56112dd7b811301b718d0643a82fd5cac9522073"/>
   <project name="platform/system/security" path="system/security" revision="f48ff68fedbcdc12b570b7699745abb6e7574907"/>
   <project name="platform/system/vold" path="system/vold" revision="8de05d4a52b5a91e7336e6baa4592f945a6ddbea"/>
   <default remote="caf" revision="refs/tags/android-4.3_r2.1" sync-j="4"/>
--- a/b2g/config/emulator-kk/releng-emulator-kk.tt
+++ b/b2g/config/emulator-kk/releng-emulator-kk.tt
@@ -1,2 +1,9 @@
 [
+{
+"size": 80458572,
+"digest": "e5101f9dee1e462f6cbd3897ea57eede41d23981825c7b20d91d23ab461875d54d3dfc24999aa58a31e8b01f49fb3140e05ffe5af2957ef1d1afb89fd0dfe1ad",
+"algorithm": "sha512",
+"filename": "gcc.tar.xz",
+"unpack": "True"
+}
 ]
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,25 +10,25 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e0c735ec89df011ea7dd435087a9045ecff9ff9e">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="9946a490a9264b42e65385d703b28fa055ab2d42"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="ebc90190771a945d405f5d36efd813db6f77f965"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="049c281ad212bf528b2af8fc246b0dd0c9f97415"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="3f4cd30032f7d9002421bdb78860c28c78760888"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="f92a936f2aa97526d4593386754bdbf02db07a12"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="6e47ff2790f5656b5b074407829ceecf3e6188c4"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="1950e4760fa14688b83cdbb5acaa1af9f82ef434"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="ac6eb97a37035c09fb5ede0852f0881e9aadf9ad"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="737f591c5f95477148d26602c7be56cbea0cdeb9"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="51da9b1981be481b92a59a826d4d78dc73d0989a"/>
   <project name="device/common" path="device/common" revision="798a3664597e6041985feab9aef42e98d458bc3d"/>
@@ -111,17 +111,17 @@
   <project name="platform/libcore" path="libcore" revision="9877ade9617bb0db6e59aa2a54719a9bc92600f3"/>
   <project name="platform/libnativehelper" path="libnativehelper" revision="46c96ace65eb1ccab05bf15b9bf8e53e443039af"/>
   <project name="platform/ndk" path="ndk" revision="cb5519af32ae7b4a9c334913a612462ecd04c5d0"/>
   <project name="platform_prebuilts_misc" path="prebuilts/misc" remote="b2g" revision="0e7c060db684b409616fe67ea433ef19f5634c60"/>
   <project name="platform/prebuilts/ndk" path="prebuilts/ndk" revision="6aa61f8557a22039a30b42b7f283996381fd625d"/>
   <project name="platform_prebuilts_qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="f7d9bf71cf6693474f3f2a81a4ba62c0fc5646aa"/>
   <project name="platform/prebuilts/sdk" path="prebuilts/sdk" revision="b562b01c93de9578d5db537b6a602a38e1aaa0ce"/>
   <project name="platform/prebuilts/tools" path="prebuilts/tools" revision="387f03e815f57d536dd922706db1622bddba8d81"/>
-  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="288db53ad77084bd44791add5e3a4c266a6e9c60"/>
+  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="aa3adea9c18ae00d36e597d5890cf14cb7dfb105"/>
   <project name="platform/system/extras" path="system/extras" revision="5356165f67f4a81c2ef28671c13697f1657590df"/>
   <project name="platform/system/media" path="system/media" revision="be0e2fe59a8043fa5200f75697df9220a99abe9d"/>
   <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="fe95bc6f83af5c18a73aa86c96e7fa7f79b91477"/>
   <project name="platform/system/netd" path="system/netd" revision="36704b0da24debcab8090156568ac236315036bb"/>
   <project name="platform/system/security" path="system/security" revision="583374f69f531ba68fc3dcbff1f74893d2a96406"/>
   <project name="platform/system/vold" path="system/vold" revision="d4455b8cf361f8353e8aebac15ffd64b4aedd2b9"/>
   <project name="platform/external/icu4c" path="external/icu4c" remote="aosp" revision="b4c6379528887dc25ca9991a535a8d92a61ad6b6"/>
   <project name="platform_frameworks_av" path="frameworks/av" remote="b2g" revision="f3cedd7fd9b1649aa5107d466be9078bb7602af6"/>
--- a/b2g/config/emulator/releng-emulator.tt
+++ b/b2g/config/emulator/releng-emulator.tt
@@ -1,2 +1,9 @@
 [
+{
+"size": 80458572,
+"digest": "e5101f9dee1e462f6cbd3897ea57eede41d23981825c7b20d91d23ab461875d54d3dfc24999aa58a31e8b01f49fb3140e05ffe5af2957ef1d1afb89fd0dfe1ad",
+"algorithm": "sha512",
+"filename": "gcc.tar.xz",
+"unpack": "True"
+}
 ]
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,23 +14,23 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="df362ace56338da8173d30d3e09e08c42c1accfa">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="9946a490a9264b42e65385d703b28fa055ab2d42"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="ebc90190771a945d405f5d36efd813db6f77f965"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="049c281ad212bf528b2af8fc246b0dd0c9f97415"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d5d3f93914558b6f168447b805cd799c8233e300"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="6fa7a4936414ceb4055fd27f7a30e76790f834fb"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="3f4cd30032f7d9002421bdb78860c28c78760888"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform_bionic" path="bionic" remote="b2g" revision="e2b3733ba3fa5e3f404e983d2e4142b1f6b1b846"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="425f8b5fadf5889834c5acd27d23c9e0b2129c28"/>
   <project name="device/common" path="device/common" revision="42b808b7e93d0619286ae8e59110b176b7732389"/>
   <project name="device/sample" path="device/sample" revision="237bd668d0f114d801a8d6455ef5e02cc3577587"/>
   <project name="platform_external_apriori" path="external/apriori" remote="b2g" revision="11816ad0406744f963537b23d68ed9c2afb412bd"/>
   <project name="platform/external/bluetooth/bluez" path="external/bluetooth/bluez" revision="52a1a862a8bac319652b8f82d9541ba40bfa45ce"/>
--- a/b2g/config/flame-kk/releng-flame-kk.tt
+++ b/b2g/config/flame-kk/releng-flame-kk.tt
@@ -1,9 +1,16 @@
 [
 {
 "size": 135359412,
 "digest": "45e677c9606cc4eec44ef4761df47ff431df1ffad17a5c6d21ce700a1c47f79e87a4aa9f30ae47ff060bd64f5b775d995780d88211f9a759ffa0d076beb4816b",
 "algorithm": "sha512",
 "filename": "backup-flame.tar.xz",
 "comment": "v18D"
+},
+{
+"size": 80458572,
+"digest": "e5101f9dee1e462f6cbd3897ea57eede41d23981825c7b20d91d23ab461875d54d3dfc24999aa58a31e8b01f49fb3140e05ffe5af2957ef1d1afb89fd0dfe1ad",
+"algorithm": "sha512",
+"filename": "gcc.tar.xz",
+"unpack": "True"
 }
 ]
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -10,25 +10,25 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="e0c735ec89df011ea7dd435087a9045ecff9ff9e">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="9946a490a9264b42e65385d703b28fa055ab2d42"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="ebc90190771a945d405f5d36efd813db6f77f965"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="049c281ad212bf528b2af8fc246b0dd0c9f97415"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="3f4cd30032f7d9002421bdb78860c28c78760888"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="8b880805d454664b3eed11d0f053cdeafa1ff06e"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.7" revision="a1e239a0bb5cd1d69680bf1075883aa9a7bf2429"/>
   <project groups="linux,x86" name="platform/prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" path="prebuilts/gcc/linux-x86/x86/i686-linux-android-4.7" revision="c7931763d41be602407ed9d71e2c0292c6597e00"/>
   <project groups="linux,x86" name="platform/prebuilts/python/linux-x86/2.7.5" path="prebuilts/python/linux-x86/2.7.5" revision="a32003194f707f66a2d8cdb913ed1869f1926c5d"/>
   <project name="device/common" path="device/common" revision="96d4d2006c4fcb2f19a3fa47ab10cb409faa017b"/>
@@ -111,17 +111,17 @@
   <project name="platform/libcore" path="libcore" revision="baf7d8068dd501cfa338d3a8b1b87216d6ce0571"/>
   <project name="platform/libnativehelper" path="libnativehelper" revision="50c4430e32849530ced32680fd6ee98963b3f7ac"/>
   <project name="platform/ndk" path="ndk" revision="e58ef003be4306bb53a8c11331146f39e4eab31f"/>
   <project name="platform_prebuilts_misc" path="prebuilts/misc" remote="b2g" revision="0e7c060db684b409616fe67ea433ef19f5634c60"/>
   <project name="platform/prebuilts/ndk" path="prebuilts/ndk" revision="c792f0bd9fff7aea2887c60bbb3a9bbdb534ffa3"/>
   <project name="platform_prebuilts_qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="f7d9bf71cf6693474f3f2a81a4ba62c0fc5646aa"/>
   <project name="platform/prebuilts/sdk" path="prebuilts/sdk" revision="69d524e80cdf3981006627c65ac85f3a871238a3"/>
   <project name="platform/prebuilts/tools" path="prebuilts/tools" revision="5a48c04c4bb5f079bc757e29864a42427378e051"/>
-  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="288db53ad77084bd44791add5e3a4c266a6e9c60"/>
+  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="aa3adea9c18ae00d36e597d5890cf14cb7dfb105"/>
   <project name="platform/system/extras" path="system/extras" revision="576f57b6510de59c08568b53c0fb60588be8689e"/>
   <project name="platform/system/media" path="system/media" revision="20c2fb4c896aa59f2e8379d755f439dc59a5cf9b"/>
   <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="fe95bc6f83af5c18a73aa86c96e7fa7f79b91477"/>
   <project name="platform/system/netd" path="system/netd" revision="a6531f7befb49b1c81bc0de7e51c5482b308e1c5"/>
   <project name="platform/system/security" path="system/security" revision="ee8068b9e7bfb2770635062fc9c2035be2142bd8"/>
   <project name="platform/system/vold" path="system/vold" revision="42fa2a0f14f965970a4b629a176bbd2666edf017"/>
   <project name="platform/external/curl" path="external/curl" revision="e68addd988448959ea8157c5de637346b4180c33"/>
   <project name="platform/external/icu4c" path="external/icu4c" revision="d3ec7428eb276db43b7ed0544e09344a6014806c"/>
--- a/b2g/config/flame/releng-flame.tt
+++ b/b2g/config/flame/releng-flame.tt
@@ -1,7 +1,14 @@
 [
 {"size": 149922032,
 "digest": "8d1a71552ffee561e93b5b3f1bb47866592ab958f908007c75561156430eb1b85a265bfc4dc2038e58dda0264daa9854877a84ef3b591c9ac2f1ab97c098e61e",
 "filename": "backup-flame.tar.xz",
 "algorithm": "sha512"
+},
+{
+"size": 80458572,
+"digest": "e5101f9dee1e462f6cbd3897ea57eede41d23981825c7b20d91d23ab461875d54d3dfc24999aa58a31e8b01f49fb3140e05ffe5af2957ef1d1afb89fd0dfe1ad",
+"algorithm": "sha512",
+"filename": "gcc.tar.xz",
+"unpack": "True"
 }
 ]
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,20 +12,20 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="0e94c080bee081a50aa2097527b0b40852f9143f">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="9946a490a9264b42e65385d703b28fa055ab2d42"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="ebc90190771a945d405f5d36efd813db6f77f965"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="049c281ad212bf528b2af8fc246b0dd0c9f97415"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="3f4cd30032f7d9002421bdb78860c28c78760888"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="95bb5b66b3ec5769c3de8d3f25d681787418e7d2"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="ebdad82e61c16772f6cd47e9f11936bf6ebe9aa0"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="8b880805d454664b3eed11d0f053cdeafa1ff06e"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "git_revision": "", 
         "remote": "", 
         "branch": ""
     }, 
-    "revision": "fe8bc669500de4e81dc5e1e0ef4e044222bdbeaa", 
+    "revision": "03effd58034893d2a12907faa6a8a41d3e923b50", 
     "repo_path": "integration/gaia-central"
 }
--- a/b2g/config/hamachi/releng-hamachi.tt
+++ b/b2g/config/hamachi/releng-hamachi.tt
@@ -5,10 +5,17 @@
 "algorithm": "sha512",
 "filename": "backup-hamachi.tar.xz"
 },
 {
 "size": 1570553,
 "digest": "ea03de74df73b05e939c314cd15c54aac7b5488a407b7cc4f5f263f3049a1f69642c567dd35c43d0bc3f0d599d0385a26ab2dd947a6b18f9044e4918b382eea7",
 "algorithm": "sha512",
 "filename": "Adreno200-AU_LINUX_ANDROID_ICS_CHOCO_CS.04.00.03.06.001.zip"
+},
+{
+"size": 80458572,
+"digest": "e5101f9dee1e462f6cbd3897ea57eede41d23981825c7b20d91d23ab461875d54d3dfc24999aa58a31e8b01f49fb3140e05ffe5af2957ef1d1afb89fd0dfe1ad",
+"algorithm": "sha512",
+"filename": "gcc.tar.xz",
+"unpack": "True"
 }
 ]
--- a/b2g/config/hamachi/releng-limited-memory.tt
+++ b/b2g/config/hamachi/releng-limited-memory.tt
@@ -23,10 +23,17 @@
 "algorithm": "sha512",
 "filename": "patches.tgz"
 },
 {
 "size": 302,
 "digest": "d6a969fad4e53b617a21026062767f4b89d301464c38e9c9d04ba7dc7dcad72f1b337c284243a81e74de713171af5dd9a13aaaa7efd10109a0847ed23bf6e539",
 "algorithm": "sha512",
 "filename": "tt_setup.sh"
+},
+{
+"size": 80458572,
+"digest": "e5101f9dee1e462f6cbd3897ea57eede41d23981825c7b20d91d23ab461875d54d3dfc24999aa58a31e8b01f49fb3140e05ffe5af2957ef1d1afb89fd0dfe1ad",
+"algorithm": "sha512",
+"filename": "gcc.tar.xz",
+"unpack": "True"
 }
 ]
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -12,21 +12,21 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="df362ace56338da8173d30d3e09e08c42c1accfa">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="9946a490a9264b42e65385d703b28fa055ab2d42"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="ebc90190771a945d405f5d36efd813db6f77f965"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="049c281ad212bf528b2af8fc246b0dd0c9f97415"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="3f4cd30032f7d9002421bdb78860c28c78760888"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform_bionic" path="bionic" remote="b2g" revision="1a2a32eda22ef2cd18f57f423a5e7b22a105a6f8"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="746bc48f34f5060f90801925dcdd964030c1ab6d"/>
   <project name="platform/development" path="development" revision="2460485184bc8535440bb63876d4e63ec1b4770c"/>
   <project name="device/common" path="device/common" revision="0dcc1e03659db33b77392529466f9eb685cdd3c7"/>
   <project name="device/sample" path="device/sample" revision="68b1cb978a20806176123b959cb05d4fa8adaea4"/>
   <project name="platform_external_apriori" path="external/apriori" remote="b2g" revision="11816ad0406744f963537b23d68ed9c2afb412bd"/>
--- a/b2g/config/helix/releng-helix.tt
+++ b/b2g/config/helix/releng-helix.tt
@@ -5,10 +5,17 @@
 "algorithm": "sha512",
 "filename": "helix-ics.tar.xz"
 },
 {
 "size": 1570553,
 "digest": "ea03de74df73b05e939c314cd15c54aac7b5488a407b7cc4f5f263f3049a1f69642c567dd35c43d0bc3f0d599d0385a26ab2dd947a6b18f9044e4918b382eea7",
 "algorithm": "sha512",
 "filename": "Adreno200-AU_LINUX_ANDROID_ICS_CHOCO_CS.04.00.03.06.001.zip"
+},
+{
+"size": 80458572,
+"digest": "e5101f9dee1e462f6cbd3897ea57eede41d23981825c7b20d91d23ab461875d54d3dfc24999aa58a31e8b01f49fb3140e05ffe5af2957ef1d1afb89fd0dfe1ad",
+"algorithm": "sha512",
+"filename": "gcc.tar.xz",
+"unpack": "True"
 }
 ]
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="df362ace56338da8173d30d3e09e08c42c1accfa">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="9946a490a9264b42e65385d703b28fa055ab2d42"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="ebc90190771a945d405f5d36efd813db6f77f965"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="049c281ad212bf528b2af8fc246b0dd0c9f97415"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform_bionic" path="bionic" remote="b2g" revision="1a2a32eda22ef2cd18f57f423a5e7b22a105a6f8"/>
--- a/b2g/config/mozconfigs/common
+++ b/b2g/config/mozconfigs/common
@@ -1,7 +1,16 @@
 # 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/.
 
 # This file is included at the top of all b2g mozconfigs
 
 . "$topsrcdir/build/mozconfig.common"
+
+# Normally, we'd set this unconditionally, but this file is also used
+# for local builds and there is no other mozconfig in this tree that
+# is included on device builds.
+if test -d $topsrcdir/../gcc/bin; then
+    HOST_CC="$topsrcdir/../gcc/bin/gcc"
+    HOST_CXX="$topsrcdir/../gcc/bin/g++"
+    ac_add_options --enable-stdcxx-compat
+fi
--- a/b2g/config/nexus-4/releng-mako.tt
+++ b/b2g/config/nexus-4/releng-mako.tt
@@ -11,11 +11,18 @@
 "algorithm": "sha512",
 "filename": "qcom-mako-jwr66v-30ef957c.tgz"
 },
 {
 "size": 378532,
 "digest": "27aced8feb0e757d61df37839e62410ff30a059cfa8f04897d29ab74b787c765313acf904b1f9cf311c3e682883514df7da54197665251ef9b8bdad6bd0f62c5",
 "algorithm": "sha512",
 "filename": "lge-mako-jwr66v-985845e4.tgz"
+},
+{
+"size": 80458572,
+"digest": "e5101f9dee1e462f6cbd3897ea57eede41d23981825c7b20d91d23ab461875d54d3dfc24999aa58a31e8b01f49fb3140e05ffe5af2957ef1d1afb89fd0dfe1ad",
+"algorithm": "sha512",
+"filename": "gcc.tar.xz",
+"unpack": "True"
 }
 ]
 
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,20 +12,20 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="0e94c080bee081a50aa2097527b0b40852f9143f">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="9946a490a9264b42e65385d703b28fa055ab2d42"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="ebc90190771a945d405f5d36efd813db6f77f965"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="049c281ad212bf528b2af8fc246b0dd0c9f97415"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="3f4cd30032f7d9002421bdb78860c28c78760888"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="9025e50b9d29b3cabbbb21e1dd94d0d13121a17e"/>
   <project groups="linux" name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="b89fda71fcd0fa0cf969310e75be3ea33e048b44"/>
   <project groups="linux,arm" name="platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" path="prebuilts/gcc/linux-x86/arm/arm-eabi-4.7" revision="2e7d5348f35575870b3c7e567a9a9f6d66f8d6c5"/>
@@ -113,17 +113,17 @@
   <project name="platform/libnativehelper" path="libnativehelper" revision="4792069e90385889b0638e97ae62c67cdf274e22"/>
   <project name="platform/ndk" path="ndk" revision="7666b97bbaf1d645cdd6b4430a367b7a2bb53369"/>
   <project name="platform/prebuilts/misc" path="prebuilts/misc" revision="f6ab40b3257abc07741188fd173ac392575cc8d2"/>
   <project name="platform/prebuilts/ndk" path="prebuilts/ndk" revision="e52099755d0bd3a579130eefe8e58066cc6c0cb6"/>
   <project name="platform_prebuilts_qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="02c32feb2fe97037be0ac4dace3a6a5025ac895d"/>
   <project name="platform/prebuilts/sdk" path="prebuilts/sdk" revision="842e33e43a55ea44833b9e23e4d180fa17c843af"/>
   <project name="platform/prebuilts/tools" path="prebuilts/tools" revision="5db24726f0f42124304195a6bdea129039eeeaeb"/>
   <project name="platform/system/bluetooth" path="system/bluetooth" revision="930ae098543881f47eac054677726ee4b998b2f8"/>
-  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="288db53ad77084bd44791add5e3a4c266a6e9c60"/>
+  <project name="platform_system_bluetoothd" path="system/bluetoothd" remote="b2g" revision="aa3adea9c18ae00d36e597d5890cf14cb7dfb105"/>
   <project name="platform_system_core" path="system/core" remote="b2g" revision="542d1f59dc331b472307e5bd043101d14d5a3a3e"/>
   <project name="platform/system/extras" path="system/extras" revision="18c1180e848e7ab8691940481f5c1c8d22c37b3e"/>
   <project name="platform_system_libfdio" path="system/libfdio" remote="b2g" revision="fe95bc6f83af5c18a73aa86c96e7fa7f79b91477"/>
   <project name="platform/system/media" path="system/media" revision="d90b836f66bf1d9627886c96f3a2d9c3007fbb80"/>
   <project name="platform/system/netd" path="system/netd" revision="56112dd7b811301b718d0643a82fd5cac9522073"/>
   <project name="platform/system/security" path="system/security" revision="f48ff68fedbcdc12b570b7699745abb6e7574907"/>
   <project name="platform/system/vold" path="system/vold" revision="8de05d4a52b5a91e7336e6baa4592f945a6ddbea"/>
   <default remote="caf" revision="refs/tags/android-4.3_r2.1" sync-j="4"/>
--- a/b2g/config/wasabi/releng-wasabi.tt
+++ b/b2g/config/wasabi/releng-wasabi.tt
@@ -11,10 +11,17 @@
 "algorithm": "sha512",
 "filename": "Adreno200-AU_LINUX_ANDROID_ICS_CHOCO_CS.04.00.03.06.001.zip"
 },
 {
 "size": 5730304,
 "digest": "709a281438607f10c9f55683303d5cc1d8720520e26a8ae45f48b7ccb9265ba8939c4a077ae3368afc891bbd7426013edfbbbb3dbdeab5b1f06e60901b141de6",
 "algorithm": "sha512",
 "filename": "boot.img"
+},
+{
+"size": 80458572,
+"digest": "e5101f9dee1e462f6cbd3897ea57eede41d23981825c7b20d91d23ab461875d54d3dfc24999aa58a31e8b01f49fb3140e05ffe5af2957ef1d1afb89fd0dfe1ad",
+"algorithm": "sha512",
+"filename": "gcc.tar.xz",
+"unpack": "True"
 }
 ]
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -12,22 +12,22 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="df362ace56338da8173d30d3e09e08c42c1accfa">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="9946a490a9264b42e65385d703b28fa055ab2d42"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="ebc90190771a945d405f5d36efd813db6f77f965"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="049c281ad212bf528b2af8fc246b0dd0c9f97415"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="3f4cd30032f7d9002421bdb78860c28c78760888"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform_bionic" path="bionic" remote="b2g" revision="e2b3733ba3fa5e3f404e983d2e4142b1f6b1b846"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="e0a9ac010df3afaa47ba107192c05ac8b5516435"/>
   <project name="platform/development" path="development" revision="a384622f5fcb1d2bebb9102591ff7ae91fe8ed2d"/>
   <project name="device/common" path="device/common" revision="7c65ea240157763b8ded6154a17d3c033167afb7"/>
   <project name="device/sample" path="device/sample" revision="c328f3d4409db801628861baa8d279fb8855892f"/>
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -205,16 +205,17 @@
 #ifdef MOZ_WEBSPEECH
 @BINPATH@/components/dom_webspeechrecognition.xpt
 #endif
 @BINPATH@/components/dom_xbl.xpt
 @BINPATH@/components/dom_xpath.xpt
 @BINPATH@/components/dom_xul.xpt
 @BINPATH@/components/dom_time.xpt
 @BINPATH@/components/dom_engineeringmode.xpt
+@BINPATH@/components/dom_presentation.xpt
 @BINPATH@/components/downloads.xpt
 @BINPATH@/components/editor.xpt
 @BINPATH@/components/embed_base.xpt
 @BINPATH@/components/extensions.xpt
 @BINPATH@/components/exthandler.xpt
 @BINPATH@/components/exthelper.xpt
 @BINPATH@/components/fastfind.xpt
 @BINPATH@/components/feeds.xpt
@@ -337,16 +338,19 @@
 @BINPATH@/components/xpcom_xpti.xpt
 @BINPATH@/components/xpconnect.xpt
 @BINPATH@/components/xulapp.xpt
 @BINPATH@/components/xul.xpt
 @BINPATH@/components/xultmpl.xpt
 @BINPATH@/components/zipwriter.xpt
 
 ; JavaScript components
+@BINPATH@/components/RequestSync.manifest
+@BINPATH@/components/RequestSyncManager.js
+@BINPATH@/components/RequestSyncScheduler.js
 @BINPATH@/components/ChromeNotifications.js
 @BINPATH@/components/ChromeNotifications.manifest
 @BINPATH@/components/ConsoleAPI.manifest
 @BINPATH@/components/ConsoleAPIStorage.js
 @BINPATH@/components/BrowserElementParent.manifest
 @BINPATH@/components/BrowserElementParent.js
 @BINPATH@/components/ContactManager.js
 @BINPATH@/components/ContactManager.manifest
@@ -368,17 +372,16 @@
 @BINPATH@/components/fuelApplication.manifest
 @BINPATH@/components/fuelApplication.js
 @BINPATH@/components/WebContentConverter.js
 @BINPATH@/components/BrowserComponents.manifest
 @BINPATH@/components/nsBrowserContentHandler.js
 @BINPATH@/components/nsBrowserGlue.js
 @BINPATH@/components/nsSetDefaultBrowser.manifest
 @BINPATH@/components/nsSetDefaultBrowser.js
-@BINPATH@/components/BrowserPlaces.manifest
 @BINPATH@/components/toolkitsearch.manifest
 @BINPATH@/components/nsTryToClose.manifest
 @BINPATH@/components/nsTryToClose.js
 @BINPATH@/components/passwordmgr.manifest
 @BINPATH@/components/nsLoginInfo.js
 @BINPATH@/components/nsLoginManager.js
 @BINPATH@/components/nsLoginManagerPrompter.js
 @BINPATH@/components/NetworkGeolocationProvider.manifest
@@ -396,16 +399,18 @@
 @BINPATH@/components/Downloads.manifest
 @BINPATH@/components/DownloadLegacy.js
 @BINPATH@/components/nsSidebar.manifest
 @BINPATH@/components/nsSidebar.js
 @BINPATH@/components/nsAsyncShutdown.manifest
 @BINPATH@/components/nsAsyncShutdown.js
 @BINPATH@/components/htmlMenuBuilder.js
 @BINPATH@/components/htmlMenuBuilder.manifest
+@BINPATH@/components/PresentationDeviceInfoManager.manifest
+@BINPATH@/components/PresentationDeviceInfoManager.js
 
 ; WiFi, NetworkManager, NetworkStats
 #ifdef MOZ_WIDGET_GONK
 @BINPATH@/components/DOMWifiManager.js
 @BINPATH@/components/DOMWifiManager.manifest
 @BINPATH@/components/DOMWifiP2pManager.js
 @BINPATH@/components/DOMWifiP2pManager.manifest
 @BINPATH@/components/NetworkInterfaceListService.js
@@ -492,17 +497,16 @@
 @BINPATH@/components/nsLivemarkService.js
 @BINPATH@/components/nsTaggingService.js
 @BINPATH@/components/nsPlacesDBFlush.js
 @BINPATH@/components/nsPlacesAutoComplete.manifest
 @BINPATH@/components/nsPlacesAutoComplete.js
 @BINPATH@/components/UnifiedComplete.manifest
 @BINPATH@/components/UnifiedComplete.js
 @BINPATH@/components/nsPlacesExpiration.js
-@BINPATH@/components/PlacesProtocolHandler.js
 @BINPATH@/components/PlacesCategoriesStarter.js
 @BINPATH@/components/nsDefaultCLH.manifest
 @BINPATH@/components/nsDefaultCLH.js
 @BINPATH@/components/nsContentPrefService.manifest
 @BINPATH@/components/nsContentPrefService.js
 @BINPATH@/components/nsContentDispatchChooser.manifest
 @BINPATH@/components/nsContentDispatchChooser.js
 @BINPATH@/components/nsHandlerService.manifest
@@ -723,17 +727,16 @@
 @BINPATH@/res/text_caret_tilt_right@2x.png
 @BINPATH@/res/grabber.gif
 #ifdef XP_MACOSX
 @BINPATH@/res/cursors/*
 #endif
 @BINPATH@/res/fonts/*
 @BINPATH@/res/dtd/*
 @BINPATH@/res/html/*
-@BINPATH@/res/langGroups.properties
 @BINPATH@/res/language.properties
 @BINPATH@/res/entityTables/*
 #ifdef XP_MACOSX
 @BINPATH@/res/MainMenu.nib/
 #endif
 
 ; svg
 @BINPATH@/res/svg.css
@@ -856,16 +859,17 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DL
 @BINPATH@/components/TelProtocolHandler.js
 @BINPATH@/components/B2GAboutRedirector.js
 @BINPATH@/components/FilePicker.js
 @BINPATH@/components/HelperAppDialog.js
 @BINPATH@/components/DownloadsUI.js
 @BINPATH@/components/InterAppCommUIGlue.js
 @BINPATH@/components/SystemMessageGlue.js
 @BINPATH@/components/B2GAppMigrator.js
+@BINPATH@/components/B2GPresentationDevicePrompt.js
 
 #ifndef MOZ_WIDGET_GONK
 @BINPATH@/components/SimulatorScreen.js
 #endif
 
 @BINPATH@/components/FxAccountsUIGlue.js
 @BINPATH@/components/services_fxaccounts.xpt
 
--- a/browser/app/nsBrowserApp.cpp
+++ b/browser/app/nsBrowserApp.cpp
@@ -38,17 +38,19 @@
 #ifdef XP_WIN
 // we want a wmain entry point
 #ifdef MOZ_ASAN
 // ASAN requires firefox.exe to be built with -MD, and it's OK if we don't
 // support Windows XP SP2 in ASAN builds.
 #define XRE_DONT_SUPPORT_XPSP2
 #endif
 #include "nsWindowsWMain.cpp"
+#if defined(_MSC_VER) && (_MSC_VER < 1900)
 #define snprintf _snprintf
+#endif
 #define strcasecmp _stricmp
 #endif
 #include "BinaryPath.h"
 
 #include "nsXPCOMPrivate.h" // for MAXPATHLEN and XPCOM_DLL
 
 #include "mozilla/Telemetry.h"
 #include "mozilla/WindowsDllBlocklist.h"
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1372,16 +1372,18 @@ pref("devtools.inspector.activeSidebar",
 pref("devtools.inspector.markupPreview", false);
 pref("devtools.inspector.remote", false);
 // Expand pseudo-elements by default in the rule-view
 pref("devtools.inspector.show_pseudo_elements", true);
 // The default size for image preview tooltips in the rule-view/computed-view/markup-view
 pref("devtools.inspector.imagePreviewTooltipSize", 300);
 // Enable user agent style inspection in rule-view
 pref("devtools.inspector.showUserAgentStyles", false);
+// Show all native anonymous content (like controls in <video> tags)
+pref("devtools.inspector.showAllAnonymousContent", false);
 
 // DevTools default color unit
 pref("devtools.defaultColorUnit", "hex");
 
 // Enable the Responsive UI tool
 pref("devtools.responsiveUI.no-reload-notification", false);
 
 // Enable the Debugger
@@ -1653,16 +1655,18 @@ pref("loop.gettingStarted.url", "https:/
 pref("loop.gettingStarted.resumeOnFirstJoin", false);
 pref("loop.learnMoreUrl", "https://www.firefox.com/hello/");
 pref("loop.legal.ToS_url", "https://www.mozilla.org/about/legal/terms/firefox-hello/");
 pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/firefox-hello/");
 pref("loop.do_not_disturb", false);
 pref("loop.ringtone", "chrome://browser/content/loop/shared/sounds/ringtone.ogg");
 pref("loop.retry_delay.start", 60000);
 pref("loop.retry_delay.limit", 300000);
+pref("loop.ping.interval", 1800000);
+pref("loop.ping.timeout", 10000);
 pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
 pref("loop.feedback.product", "Loop");
 pref("loop.debug.loglevel", "Error");
 pref("loop.debug.dispatcher", false);
 pref("loop.debug.websocket", false);
 pref("loop.debug.sdk", false);
 #ifdef DEBUG
 pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src 'self' data: http://www.gravatar.com/ about: file: chrome:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:");
@@ -1776,17 +1780,17 @@ pref("media.gmp-gmpopenh264.provider.ena
 pref("browser.apps.URL", "https://marketplace.firefox.com/discovery/");
 
 #ifdef NIGHTLY_BUILD
 pref("browser.polaris.enabled", false);
 pref("privacy.trackingprotection.ui.enabled", false);
 #endif
 
 #ifdef NIGHTLY_BUILD
-pref("browser.tabs.remote.autostart.1", false);
+pref("browser.tabs.remote.autostart.1", true);
 #endif
 
 // Temporary pref to allow printing in e10s windows on some platforms.
 #ifdef UNIX_BUT_NOT_MAC
 pref("print.enable_e10s_testing", false);
 #else
 pref("print.enable_e10s_testing", true);
 #endif
--- a/browser/base/content/browser-menubar.inc
+++ b/browser/base/content/browser-menubar.inc
@@ -545,16 +545,17 @@
               <menuitem id="menu_pageInfo"
                         accesskey="&pageInfoCmd.accesskey;"
                         label="&pageInfoCmd.label;"
 #ifndef XP_WIN
                         key="key_viewInfo"
 #endif
                         command="View:PageInfo"/>
               <menu id="menu_mirrorTabCmd"
+                    hidden="true"
                     accesskey="&mirrorTabCmd.accesskey;"
                     label="&mirrorTabCmd.label;">
                 <menupopup id="menu_mirrorTab-popup"
                            onpopupshowing="populateMirrorTabMenu(this)"/>
               </menu>
 #ifndef XP_UNIX
               <menuseparator id="prefSep"/>
               <menuitem id="menu_preferences"
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2935,17 +2935,17 @@ function getMeOutOfHere() {
 
 function BrowserFullScreen()
 {
   window.fullScreen = !window.fullScreen;
 }
 
 function mirrorShow(popup) {
   let services = CastingApps.getServicesForMirroring();
-  popup.ownerDocument.getElementById("menu_mirrorTabCmd").disabled = !services.length;
+  popup.ownerDocument.getElementById("menu_mirrorTabCmd").hidden = !services.length;
 }
 
 function mirrorMenuItemClicked(event) {
   gBrowser.selectedBrowser.messageManager.sendAsyncMessage("SecondScreen:tab-mirror",
                                                            {service: event.originalTarget._service});
 }
 
 function populateMirrorTabMenu(popup) {
@@ -6445,18 +6445,28 @@ function WindowIsClosing()
     return false;
 
   // Bug 967873 - Proxy nsDocumentViewer::PermitUnload to the child process
   if (gMultiProcessBrowser)
     return true;
 
   for (let browser of gBrowser.browsers) {
     let ds = browser.docShell;
-    if (ds.contentViewer && !ds.contentViewer.permitUnload())
+    // Passing true to permitUnload indicates we plan on closing the window.
+    // This means that once unload is permitted, all further calls to
+    // permitUnload will be ignored. This avoids getting multiple prompts
+    // to unload the page.
+    if (ds.contentViewer && !ds.contentViewer.permitUnload(true)) {
+      // ... however, if the user aborts closing, we need to undo that,
+      // to ensure they get prompted again when we next try to close the window.
+      // We do this on the window's toplevel docshell instead of on the tab, so
+      // that all tabs we iterated before will get this reset.
+      window.getInterface(Ci.nsIDocShell).contentViewer.resetCloseWindow();
       return false;
+    }
   }
 
   return true;
 }
 
 /**
  * Checks if this is the last full *browser* window around. If it is, this will
  * be communicated like quitting. Otherwise, we warn about closing multiple tabs.
--- a/browser/base/content/sanitize.js
+++ b/browser/base/content/sanitize.js
@@ -244,17 +244,18 @@ Sanitizer.prototype = {
         if (this.range)
           PlacesUtils.history.removeVisitsByTimeframe(this.range[0], this.range[1]);
         else
           PlacesUtils.history.removeAllPages();
 
         try {
           var os = Components.classes["@mozilla.org/observer-service;1"]
                              .getService(Components.interfaces.nsIObserverService);
-          os.notifyObservers(null, "browser:purge-session-history", "");
+          let clearStartingTime = this.range ? String(this.range[0]) : "";
+          os.notifyObservers(null, "browser:purge-session-history", clearStartingTime);
         }
         catch (e) { }
 
         try {
           var predictor = Components.classes["@mozilla.org/network/predictor;1"]
                                     .getService(Components.interfaces.nsINetworkPredictor);
           predictor.reset();
         } catch (e) { }
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -2038,35 +2038,16 @@
         <parameter name="aCloseWindowFastpath"/>
         <body>
           <![CDATA[
             if (aTab.closing ||
                 this._windowIsClosing)
               return false;
 
             var browser = this.getBrowserForTab(aTab);
-            if (!aTab._pendingPermitUnload && !aTabWillBeMoved) {
-              let ds = browser.docShell;
-              if (ds && ds.contentViewer) {
-                // We need to block while calling permitUnload() because it
-                // processes the event queue and may lead to another removeTab()
-                // call before permitUnload() even returned.
-                aTab._pendingPermitUnload = true;
-                let permitUnload = ds.contentViewer.permitUnload();
-                delete aTab._pendingPermitUnload;
-                // If we were closed during onbeforeunload, we return false now
-                // so we don't (try to) close the same tab again. Of course, we
-                // also stop if the unload was cancelled by the user:
-                if (aTab.closing || !permitUnload) {
-                  // NB: deliberately keep the _closedDuringPermitUnload set to
-                  // true so we keep exiting early in case of multiple calls.
-                  return false;
-                }
-              }
-            }
 
             var closeWindow = false;
             var newTab = false;
             if (this.tabs.length - this._removingTabs.length == 1) {
               closeWindow = aCloseWindowWithLastTab != null ? aCloseWindowWithLastTab :
                             !window.toolbar.visible ||
                               Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab");
 
@@ -2080,16 +2061,36 @@
                 // cancels the operation.  We are finished here in both cases.
                 this._windowIsClosing = window.closeWindow(true, window.warnAboutClosingWindow);
                 return null;
               }
 
               newTab = true;
             }
 
+            if (!aTab._pendingPermitUnload && !aTabWillBeMoved) {
+              let ds = browser.docShell;
+              if (ds && ds.contentViewer) {
+                // We need to block while calling permitUnload() because it
+                // processes the event queue and may lead to another removeTab()
+                // call before permitUnload() returns.
+                aTab._pendingPermitUnload = true;
+                let permitUnload = ds.contentViewer.permitUnload();
+                delete aTab._pendingPermitUnload;
+                // If we were closed during onbeforeunload, we return false now
+                // so we don't (try to) close the same tab again. Of course, we
+                // also stop if the unload was cancelled by the user:
+                if (aTab.closing || !permitUnload) {
+                  // NB: deliberately keep the _closedDuringPermitUnload set to
+                  // true so we keep exiting early in case of multiple calls.
+                  return false;
+                }
+              }
+            }
+
             aTab.closing = true;
             this._removingTabs.push(aTab);
             this._visibleTabs = null; // invalidate cache
 
             // Invalidate hovered tab state tracking for this closing tab.
             if (this.tabContainer._hoveredTab == aTab)
               aTab._mouseleave();
 
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -121,16 +121,18 @@ skip-if = e10s # Bug 1093153 - no about:
 [browser_autocomplete_a11y_label.js]
 skip-if = e10s # Bug 1101993 - times out for unknown reasons when run in the dir (works on its own)
 [browser_autocomplete_no_title.js]
 [browser_autocomplete_autoselect.js]
 [browser_autocomplete_oldschool_wrap.js]
 [browser_autocomplete_tag_star_visibility.js]
 [browser_backButtonFitts.js]
 skip-if = os != "win" || e10s # The Fitts Law back button is only supported on Windows (bug 571454) / e10s - Bug 1099154: test touches content (attempts to add an event listener directly to the contentWindow)
+[browser_beforeunload_duplicate_dialogs.js]
+skip-if = e10s # bug 967873 means permitUnload doesn't work in e10s mode
 [browser_blob-channelname.js]
 [browser_bookmark_titles.js]
 skip-if = buildapp == 'mulet' || toolkit == "windows" || e10s # Disabled on Windows due to frequent failures (bugs 825739, 841341) / e10s - Bug 1094205 - places doesn't return the right thing in e10s mode, for some reason
 [browser_bug304198.js]
 skip-if = e10s
 [browser_bug321000.js]
 skip-if = true # browser_bug321000.js is disabled because newline handling is shaky (bug 592528)
 [browser_bug329212.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js
@@ -0,0 +1,44 @@
+const TEST_PAGE = "http://mochi.test:8888/browser/browser/base/content/test/general/file_double_close_tab.html";
+
+let expectingDialog = false;
+function onTabModalDialogLoaded(node) {
+  ok(expectingDialog, "Should be expecting this dialog.");
+  expectingDialog = false;
+  // This accepts the dialog, closing it
+  node.Dialog.ui.button0.click();
+}
+
+
+// Listen for the dialog being created
+Services.obs.addObserver(onTabModalDialogLoaded, "tabmodal-dialog-loaded", false);
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("browser.tabs.warnOnClose");
+  Services.obs.removeObserver(onTabModalDialogLoaded, "tabmodal-dialog-loaded");
+});
+
+add_task(function* closeLastTabInWindow() {
+  let newWin = yield promiseOpenAndLoadWindow({}, true);
+  let firstTab = newWin.gBrowser.selectedTab;
+  yield promiseTabLoadEvent(firstTab, TEST_PAGE);
+  let windowClosedPromise = promiseWindowWillBeClosed(newWin);
+  expectingDialog = true;
+  // close tab:
+  document.getAnonymousElementByAttribute(firstTab, "anonid", "close-button").click();
+  yield windowClosedPromise;
+  ok(!expectingDialog, "There should have been a dialog.");
+  ok(newWin.closed, "Window should be closed.");
+});
+
+add_task(function* closeWindowWithMultipleTabsIncludingOneBeforeUnload() {
+  Services.prefs.setBoolPref("browser.tabs.warnOnClose", false);
+  let newWin = yield promiseOpenAndLoadWindow({}, true);
+  let firstTab = newWin.gBrowser.selectedTab;
+  yield promiseTabLoadEvent(firstTab, TEST_PAGE);
+  yield promiseTabLoadEvent(newWin.gBrowser.addTab(), "http://example.com/");
+  let windowClosedPromise = promiseWindowWillBeClosed(newWin);
+  expectingDialog = true;
+  newWin.BrowserTryToCloseWindow();
+  yield windowClosedPromise;
+  ok(!expectingDialog, "There should have been a dialog.");
+  ok(newWin.closed, "Window should be closed.");
+});
--- a/browser/base/content/test/general/file_double_close_tab.html
+++ b/browser/base/content/test/general/file_double_close_tab.html
@@ -1,13 +1,13 @@
 <!DOCTYPE html>
 <html>
   <head>
     <meta charset="utf-8">
-    <title>Test for bug 1050638 - clicking tab close button twice should close tab even in beforeunload case</title>
+    <title>Test page that blocks beforeunload. Used in tests for bug 1050638 and bug 305085</title>
   </head>
   <body>
     This page will block beforeunload. It should still be user-closable at all times.
     <script>
       window.onbeforeunload = function() {
         return "stop";
       };
     </script>
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -197,24 +197,31 @@ function resetBlocklist() {
 function whenNewWindowLoaded(aOptions, aCallback) {
   let win = OpenBrowserWindow(aOptions);
   win.addEventListener("load", function onLoad() {
     win.removeEventListener("load", onLoad, false);
     aCallback(win);
   }, false);
 }
 
+function promiseWindowWillBeClosed(win) {
+  return new Promise((resolve, reject) => {
+    Services.obs.addObserver(function observe(subject, topic) {
+      if (subject == win) {
+        Services.obs.removeObserver(observe, topic);
+        resolve();
+      }
+    }, "domwindowclosed", false);
+  });
+}
+
 function promiseWindowClosed(win) {
-  let deferred = Promise.defer();
-  win.addEventListener("unload", function onunload() {
-    win.removeEventListener("unload", onunload);
-    deferred.resolve();
-  });
+  let promise = promiseWindowWillBeClosed(win);
   win.close();
-  return deferred.promise;
+  return promise;
 }
 
 function promiseOpenAndLoadWindow(aOptions, aWaitForDelayedStartup=false) {
   let deferred = Promise.defer();
   let win = OpenBrowserWindow(aOptions);
   if (aWaitForDelayedStartup) {
     Services.obs.addObserver(function onDS(aSubject, aTopic, aData) {
       if (aSubject != win) {
--- a/browser/base/content/test/popupNotifications/browser_popupNotification_4.js
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_4.js
@@ -249,10 +249,25 @@ let tests = [
       PopupNotifications._update();
     },
     onShown: function (popup) {
       checkPopup(popup, this.notifyObj2);
       this.notification1.remove();
       this.notification2.remove();
     },
     onHidden: function(popup) { }
+  },
+  // The anchor icon should be shown for notifications in background windows.
+  { id: "Test#13",
+    run: function() {
+      let notifyObj = new BasicNotification(this.id);
+      notifyObj.options.dismissed = true;
+      let win = gBrowser.replaceTabWithWindow(gBrowser.addTab("about:blank"));
+      whenDelayedStartupFinished(win, function() {
+        showNotification(notifyObj);
+        let anchor = document.getElementById("default-notification-icon");
+        is(anchor.getAttribute("showing"), "true", "the anchor is shown");
+        win.close();
+        goNext();
+      });
+    }
   }
 ];
--- a/browser/branding/aurora/pref/firefox-branding.js
+++ b/browser/branding/aurora/pref/firefox-branding.js
@@ -21,18 +21,11 @@ pref("app.update.url.manual", "https://w
 // supplied in the "An update is available" page of the update wizard. 
 pref("app.update.url.details", "https://www.mozilla.org/firefox/aurora/");
 
 // The number of days a binary is permitted to be old
 // without checking for an update.  This assumes that
 // app.update.checkInstallTime is true.
 pref("app.update.checkInstallTime.days", 2);
 
-// code usage depends on contracts, please contact the Firefox module owner if you have questions
-pref("browser.search.param.yahoo-fr", "moz35");
-pref("browser.search.param.yahoo-fr-ja", "mozff");
-#ifdef MOZ_METRO
-pref("browser.search.param.yahoo-fr-metro", "");
-#endif
-
 // Number of usages of the web console or scratchpad.
 // If this is less than 5, then pasting code into the web console or scratchpad is disabled
 pref("devtools.selfxss.count", 5);
\ No newline at end of file
--- a/browser/branding/nightly/pref/firefox-branding.js
+++ b/browser/branding/nightly/pref/firefox-branding.js
@@ -19,18 +19,11 @@ pref("app.update.url.manual", "https://n
 // supplied in the "An update is available" page of the update wizard. 
 pref("app.update.url.details", "https://nightly.mozilla.org");
 
 // The number of days a binary is permitted to be old
 // without checking for an update.  This assumes that
 // app.update.checkInstallTime is true.
 pref("app.update.checkInstallTime.days", 2);
 
-// code usage depends on contracts, please contact the Firefox module owner if you have questions
-pref("browser.search.param.yahoo-fr", "moz35");
-pref("browser.search.param.yahoo-fr-ja", "mozff");
-#ifdef MOZ_METRO
-pref("browser.search.param.yahoo-fr-metro", "");
-#endif
-
 // Number of usages of the web console or scratchpad.
 // If this is less than 5, then pasting code into the web console or scratchpad is disabled
 pref("devtools.selfxss.count", 5);
\ No newline at end of file
--- a/browser/branding/official/pref/firefox-branding.js
+++ b/browser/branding/official/pref/firefox-branding.js
@@ -18,19 +18,11 @@ pref("app.update.url.manual", "https://w
 // supplied in the "An update is available" page of the update wizard. 
 pref("app.update.url.details", "https://www.mozilla.org/%LOCALE%/firefox/notes");
 
 // The number of days a binary is permitted to be old
 // without checking for an update.  This assumes that
 // app.update.checkInstallTime is true.
 pref("app.update.checkInstallTime.days", 63);
 
-// code usage depends on contracts, please contact the Firefox module owner if you have questions
-pref("browser.search.param.yahoo-fr", "moz35");
-pref("browser.search.param.yahoo-fr-ja", "mozff");
-#ifdef MOZ_METRO
-pref("browser.search.param.ms-pc-metro", "MOZW");
-pref("browser.search.param.yahoo-fr-metro", "mozilla_metro_search");
-#endif
-
 // Number of usages of the web console or scratchpad.
 // If this is less than 5, then pasting code into the web console or scratchpad is disabled
 pref("devtools.selfxss.count", 0);
\ No newline at end of file
--- a/browser/branding/unofficial/pref/firefox-branding.js
+++ b/browser/branding/unofficial/pref/firefox-branding.js
@@ -18,18 +18,11 @@ pref("app.update.url.manual", "https://n
 // supplied in the "An update is available" page of the update wizard. 
 pref("app.update.url.details", "https://nightly.mozilla.org");
 
 // The number of days a binary is permitted to be old
 // without checking for an update.  This assumes that
 // app.update.checkInstallTime is true.
 pref("app.update.checkInstallTime.days", 2);
 
-// code usage depends on contracts, please contact the Firefox module owner if you have questions
-pref("browser.search.param.yahoo-fr", "moz35");
-pref("browser.search.param.yahoo-fr-ja", "mozff");
-#ifdef MOZ_METRO
-pref("browser.search.param.yahoo-fr-metro", "");
-#endif
-
 // Number of usages of the web console or scratchpad.
 // If this is less than 5, then pasting code into the web console or scratchpad is disabled
 pref("devtools.selfxss.count", 0);
\ No newline at end of file
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -3964,18 +3964,22 @@ OverflowableToolbar.prototype = {
     this._panel.removeEventListener("dragover", this);
     this._panel.removeEventListener("dragend", this);
     let doc = aEvent.target.ownerDocument;
     let contextMenu = doc.getElementById(this._panel.getAttribute("context"));
     gELS.removeSystemEventListener(contextMenu, 'command', this, true);
   },
 
   onOverflow: function(aEvent) {
+    // The rangeParent check is here because of bug 1111986 and ensuring that
+    // overflow events from the bookmarks toolbar items or similar things that
+    // manage their own overflow don't trigger an overflow on the entire toolbar
     if (!this._enabled ||
-        (aEvent && aEvent.target != this._toolbar.customizationTarget))
+        (aEvent && aEvent.target != this._toolbar.customizationTarget) ||
+        (aEvent && aEvent.rangeParent))
       return;
 
     let child = this._target.lastChild;
 
     while (child && this._target.scrollLeftMax > 0) {
       let prevChild = child.previousSibling;
 
       if (child.getAttribute("overflows") != "false") {
--- a/browser/components/downloads/DownloadsCommon.jsm
+++ b/browser/components/downloads/DownloadsCommon.jsm
@@ -606,22 +606,18 @@ XPCOMUtils.defineLazyGetter(DownloadsCom
  *
  * Note that DownloadsData and PrivateDownloadsData are two equivalent singleton
  * objects, one accessing non-private downloads, and the other accessing private
  * ones.
  */
 function DownloadsDataCtor(aPrivate) {
   this._isPrivate = aPrivate;
 
-  // This Object contains all the available DownloadsDataItem objects, indexed by
-  // their globally unique identifier.  The identifiers of downloads that have
-  // been removed from the Download Manager data are still present, however the
-  // associated objects are replaced with the value "null".  This is required to
-  // prevent race conditions when populating the list asynchronously.
-  this.dataItems = {};
+  // Contains all the available DownloadsDataItem objects.
+  this.dataItems = new Set();
 
   // Array of view objects that should be notified when the available download
   // data changes.
   this._views = [];
 
   // Maps Download objects to DownloadDataItem objects.
   this._downloadToDataItemMap = new Map();
 }
@@ -639,18 +635,18 @@ DownloadsDataCtor.prototype = {
     }
   },
   _dataLinkInitialized: false,
 
   /**
    * True if there are finished downloads that can be removed from the list.
    */
   get canRemoveFinished() {
-    for (let [, dataItem] of Iterator(this.dataItems)) {
-      if (dataItem && !dataItem.inProgress) {
+    for (let dataItem of this.dataItems) {
+      if (!dataItem.inProgress) {
         return true;
       }
     }
     return false;
   },
 
   /**
    * Asks the back-end to remove finished downloads from the list.
@@ -663,17 +659,17 @@ DownloadsDataCtor.prototype = {
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Integration with the asynchronous Downloads back-end
 
   onDownloadAdded(aDownload) {
     let dataItem = new DownloadsDataItem(aDownload);
     this._downloadToDataItemMap.set(aDownload, dataItem);
-    this.dataItems[dataItem.downloadGuid] = dataItem;
+    this.dataItems.add(dataItem);
 
     for (let view of this._views) {
       view.onDataItemAdded(dataItem, true);
     }
 
     this._updateDataItemState(dataItem);
   },
 
@@ -690,17 +686,17 @@ DownloadsDataCtor.prototype = {
   onDownloadRemoved(aDownload) {
     let dataItem = this._downloadToDataItemMap.get(aDownload);
     if (!dataItem) {
       Cu.reportError("Download doesn't exist.");
       return;
     }
 
     this._downloadToDataItemMap.delete(aDownload);
-    this.dataItems[dataItem.downloadGuid] = null;
+    this.dataItems.delete(dataItem);
     for (let view of this._views) {
       view.onDataItemRemoved(dataItem);
     }
   },
 
   /**
    * Updates the given data item and sends related notifications.
    */
@@ -713,17 +709,17 @@ DownloadsDataCtor.prototype = {
 
     if (wasInProgress && !aDataItem.inProgress) {
       aDataItem.endTime = Date.now();
     }
 
     if (oldState != aDataItem.state) {
       for (let view of this._views) {
         try {
-          view.getViewItem(aDataItem).onStateChange(oldState);
+          view.onDataItemStateChanged(aDataItem, oldState);
         } catch (ex) {
           Cu.reportError(ex);
         }
       }
 
       // This state transition code should actually be located in a Downloads
       // API module (bug 941009).  Moreover, the fact that state is stored as
       // annotations should be ideally hidden behind methods of
@@ -751,17 +747,17 @@ DownloadsDataCtor.prototype = {
       this._notifyDownloadEvent("start");
     }
 
     if (!wasDone && aDataItem.done) {
       this._notifyDownloadEvent("finish");
     }
 
     for (let view of this._views) {
-      view.getViewItem(aDataItem).onProgressChange();
+      view.onDataItemChanged(aDataItem);
     }
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Registration of views
 
   /**
    * Adds an object to be notified when the available download data changes.
@@ -796,19 +792,17 @@ DownloadsDataCtor.prototype = {
    *        DownloadsView object to be initialized.
    */
   _updateView(aView) {
     // Indicate to the view that a batch loading operation is in progress.
     aView.onDataLoadStarting();
 
     // Sort backwards by start time, ensuring that the most recent
     // downloads are added first regardless of their state.
-    let loadedItemsArray = [dataItem
-                            for each (dataItem in this.dataItems)
-                            if (dataItem)];
+    let loadedItemsArray = [...this.dataItems];
     loadedItemsArray.sort((a, b) => b.startTime - a.startTime);
     loadedItemsArray.forEach(dataItem => aView.onDataItemAdded(dataItem, false));
 
     // Notify the view that all data is available.
     aView.onDataLoadCompleted();
   },
 
   //////////////////////////////////////////////////////////////////////////////
@@ -876,34 +870,26 @@ XPCOMUtils.defineLazyGetter(this, "Downl
  * The endTime property is initialized to the current date and time.
  *
  * @param aDownload
  *        The Download object with the current state.
  */
 function DownloadsDataItem(aDownload) {
   this._download = aDownload;
 
-  this.downloadGuid = "id:" + this._autoIncrementId;
   this.file = aDownload.target.path;
   this.target = OS.Path.basename(aDownload.target.path);
   this.uri = aDownload.source.url;
   this.endTime = Date.now();
 
   this.updateFromDownload();
 }
 
 DownloadsDataItem.prototype = {
   /**
-   * The JavaScript API does not need identifiers for Download objects, so they
-   * are generated sequentially for the corresponding DownloadDataItem.
-   */
-  get _autoIncrementId() ++DownloadsDataItem.prototype.__lastId,
-  __lastId: 0,
-
-  /**
    * Updates this object from the underlying Download object.
    */
   updateFromDownload() {
     // Collapse state using the correct priority.
     if (this._download.succeeded) {
       this.state = nsIDM.DOWNLOAD_FINISHED;
     } else if (this._download.error &&
                this._download.error.becauseBlockedByParentalControls) {
@@ -1245,26 +1231,36 @@ const DownloadsViewPrototype = {
    *
    * @note Subclasses should override this.
    */
   onDataItemRemoved(aDataItem) {
     throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
   },
 
   /**
-   * Returns the view item associated with the provided data item for this view.
+   * Called when the "state" property of a DownloadsDataItem has changed.
    *
-   * @param aDataItem
-   *        DownloadsDataItem object for which the view item is requested.
-   *
-   * @return Object that can be used to notify item status events.
+   * The onDataItemChanged notification will be sent afterwards.
    *
    * @note Subclasses should override this.
    */
-  getViewItem(aDataItem) {
+  onDataItemStateChanged(aDataItem) {
+    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
+  /**
+   * Called every time any state property of a DownloadsDataItem may have
+   * changed, including progress properties and the "state" property.
+   *
+   * Note that progress notification changes are throttled at the Downloads.jsm
+   * API level, and there is no throttling mechanism in the front-end.
+   *
+   * @note Subclasses should override this.
+   */
+  onDataItemChanged(aDataItem) {
     throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
   },
 
   /**
    * Private function used to refresh the internal properties being sent to
    * each registered view.
    *
    * @note Subclasses should override this.
@@ -1353,44 +1349,31 @@ DownloadsIndicatorDataCtor.prototype = {
    * @param aDataItem
    *        DownloadsDataItem object that is being removed.
    */
   onDataItemRemoved(aDataItem) {
     this._itemCount--;
     this._updateViews();
   },
 
-  /**
-   * Returns the view item associated with the provided data item for this view.
-   *
-   * @param aDataItem
-   *        DownloadsDataItem object for which the view item is requested.
-   *
-   * @return Object that can be used to notify item status events.
-   */
-  getViewItem(aDataItem) {
-    let data = this._isPrivate ? PrivateDownloadsIndicatorData
-                               : DownloadsIndicatorData;
-    return Object.freeze({
-      onStateChange(aOldState) {
-        if (aDataItem.state == nsIDM.DOWNLOAD_FINISHED ||
-            aDataItem.state == nsIDM.DOWNLOAD_FAILED) {
-          data.attention = true;
-        }
+  // DownloadsView
+  onDataItemStateChanged(aDataItem, aOldState) {
+    if (aDataItem.state == nsIDM.DOWNLOAD_FINISHED ||
+        aDataItem.state == nsIDM.DOWNLOAD_FAILED) {
+      this.attention = true;
+    }
 
-        // Since the state of a download changed, reset the estimated time left.
-        data._lastRawTimeLeft = -1;
-        data._lastTimeLeft = -1;
+    // Since the state of a download changed, reset the estimated time left.
+    this._lastRawTimeLeft = -1;
+    this._lastTimeLeft = -1;
+  },
 
-        data._updateViews();
-      },
-      onProgressChange() {
-        data._updateViews();
-      }
-    });
+  // DownloadsView
+  onDataItemChanged() {
+    this._updateViews();
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Propagation of properties to our views
 
   // The following properties are updated by _refreshProperties and are then
   // propagated to the views.  See _refreshProperties for details.
   _hasDownloads: false,
@@ -1475,17 +1458,17 @@ DownloadsIndicatorDataCtor.prototype = {
    * A generator function for the dataItems that this summary is currently
    * interested in. This generator is passed off to summarizeDownloads in order
    * to generate statistics about the dataItems we care about - in this case,
    * it's all dataItems for active downloads.
    */
   _activeDataItems() {
     let dataItems = this._isPrivate ? PrivateDownloadsData.dataItems
                                     : DownloadsData.dataItems;
-    for each (let dataItem in dataItems) {
+    for (let dataItem of dataItems) {
       if (dataItem && dataItem.inProgress) {
         yield dataItem;
       }
     }
   },
 
   /**
    * Computes aggregate values based on the current state of downloads.
@@ -1619,29 +1602,26 @@ DownloadsSummaryData.prototype = {
   },
 
   onDataItemRemoved(aDataItem) {
     let itemIndex = this._dataItems.indexOf(aDataItem);
     this._dataItems.splice(itemIndex, 1);
     this._updateViews();
   },
 
-  getViewItem(aDataItem) {
-    let self = this;
-    return Object.freeze({
-      onStateChange(aOldState) {
-        // Since the state of a download changed, reset the estimated time left.
-        self._lastRawTimeLeft = -1;
-        self._lastTimeLeft = -1;
-        self._updateViews();
-      },
-      onProgressChange() {
-        self._updateViews();
-      }
-    });
+  // DownloadsView
+  onDataItemStateChanged(aOldState) {
+    // Since the state of a download changed, reset the estimated time left.
+    this._lastRawTimeLeft = -1;
+    this._lastTimeLeft = -1;
+  },
+
+  // DownloadsView
+  onDataItemChanged() {
+    this._updateViews();
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Propagation of properties to our views
 
   /**
    * Computes aggregate values and propagates the changes to our views.
    */
--- a/browser/components/downloads/content/allDownloadsViewOverlay.js
+++ b/browser/components/downloads/content/allDownloadsViewOverlay.js
@@ -48,24 +48,20 @@ const NOT_AVAILABLE = Number.MAX_VALUE;
  *
  * Once initialized with either a data item or a places node, the created richlistitem
  * can be accessed through the |element| getter, and can then be inserted/removed from
  * a richlistbox.
  *
  * The shell doesn't take care of inserting the item, or removing it when it's no longer
  * valid. That's the caller (a DownloadsPlacesView object) responsibility.
  *
- * The caller is also responsible for "passing over" notification from both the
- * download-view and the places-result-observer, in the following manner:
- *  - The DownloadsPlacesView object implements getViewItem of the download-view
- *    pseudo interface.  It returns this object (therefore we implement
- *    onStateChangea and onProgressChange here).
- *  - The DownloadsPlacesView object adds itself as a places result observer and
- *    calls this object's placesNodeIconChanged, placesNodeTitleChanged and
- *    placeNodeAnnotationChanged from its callbacks.
+ * The caller is also responsible for "passing over" notifications. The
+ * DownloadsPlacesView object implements onDataItemStateChanged and
+ * onDataItemChanged of the DownloadsView pseudo interface, and registers as a
+ * Places result observer.
  *
  * @param [optional] aDataItem
  *        The data item of a the session download. Required if aPlacesNode is not set
  * @param [optional] aPlacesNode
  *        The places node for a past download. Required if aDataItem is not set.
  * @param [optional] aAnnotations
  *        Map containing annotations values, to speed up the initial loading.
  */
@@ -171,23 +167,20 @@ DownloadElementShell.prototype = {
 
   _getIcon() {
     let metaData = this.getDownloadMetaData();
     if ("filePath" in metaData) {
       return "moz-icon://" + metaData.filePath + "?size=32";
     }
 
     if (this._placesNode) {
-      // Try to extract an extension from the uri.
-      let ext = this._downloadURIObj.QueryInterface(Ci.nsIURL).fileExtension;
-      if (ext) {
-        return "moz-icon://." + ext + "?size=32";
-      }
-      return this._placesNode.icon || "moz-icon://.unknown?size=32";
+      return "moz-icon://.unknown?size=32";
     }
+
+    // Assert unreachable.
     if (this._dataItem) {
       throw new Error("Session-download items should always have a target file uri");
     }
 
     throw new Error("Unexpected download element state");
   },
 
   // Helper for getting a places annotation set for the download.
@@ -299,19 +292,19 @@ DownloadElementShell.prototype = {
    * - endTime: the end time of the download.
    * - filePath: the downloaded file path on the file system, when it
    *   was downloaded.  The file may not exist.  This is set for session
    *   downloads that have a local file set, and for history downloads done
    *   after the landing of bug 591289.
    * - fileName: the downloaded file name on the file system. Set if filePath
    *   is set.
    * - displayName: the user-facing label for the download.  This is always
-   *   set.  If available, it's set to the downloaded file name.  If not,
-   *   the places title for the download uri is used it's set.  As a last
-   *   resort, we fallback to the download uri.
+   *   set.  If available, it's set to the downloaded file name.  If not, this
+   *   means the download does not have Places metadata because it is very old,
+   *   and in this rare case the download uri is used.
    * - fileSize (only set for downloads which completed successfully):
    *   the downloaded file size.  For downloads done after the landing of
    *   bug 826991, this value is "static" - that is, it does not necessarily
    *   mean that the file is in place and has this size.
    */
   getDownloadMetaData() {
     if (!this._metaData) {
       if (this._dataItem) {
@@ -343,17 +336,17 @@ DownloadElementShell.prototype = {
         }
 
         try {
           let targetFileURI = this._getAnnotation(DESTINATION_FILE_URI_ANNO);
           [this._metaData.filePath, this._metaData.fileName] =
             this._extractFilePathAndNameFromFileURI(targetFileURI);
           this._metaData.displayName = this._metaData.fileName;
         } catch (ex) {
-          this._metaData.displayName = this._placesNode.title || this.downloadURI;
+          this._metaData.displayName = this.downloadURI;
         }
       }
     }
     return this._metaData;
   },
 
   _getStatusText() {
     let s = DownloadsCommon.strings;
@@ -484,119 +477,61 @@ DownloadElementShell.prototype = {
     // Dispatch the ValueChange event for accessibility, if possible.
     if (this._progressElement) {
       let event = document.createEvent("Events");
       event.initEvent("ValueChange", true, true);
       this._progressElement.dispatchEvent(event);
     }
   },
 
-  _updateDisplayNameAndIcon() {
-    let metaData = this.getDownloadMetaData();
-    this._element.setAttribute("displayName", metaData.displayName);
-    this._element.setAttribute("image", this._getIcon());
-  },
-
   _updateUI() {
     if (!this.active) {
       throw new Error("Trying to _updateUI on an inactive download shell");
     }
 
     this._metaData = null;
     this._targetFileInfoFetched = false;
 
-    this._updateDisplayNameAndIcon();
+    let metaData = this.getDownloadMetaData();
+    this._element.setAttribute("displayName", metaData.displayName);
+    this._element.setAttribute("image", this._getIcon());
 
     // For history downloads done in past releases, the downloads/metaData
     // annotation is not set, and therefore we cannot tell the download
     // state without the target file information.
     if (this._dataItem || this.getDownloadMetaData().state !== undefined) {
       this._updateDownloadStatusUI();
     } else {
       this._fetchTargetFileInfo(true);
     }
   },
 
-  placesNodeIconChanged() {
-    if (!this._dataItem) {
-      this._element.setAttribute("image", this._getIcon());
-    }
-  },
-
-  placesNodeTitleChanged() {
-    // If there's a file path, we use the leaf name for the title.
-    if (!this._dataItem && this.active &&
-        !this.getDownloadMetaData().filePath) {
-      this._metaData = null;
-      this._updateDisplayNameAndIcon();
-    }
-  },
-
-  placesNodeAnnotationChanged(aAnnoName) {
-    this._annotations.delete(aAnnoName);
-    if (!this._dataItem && this.active) {
-      if (aAnnoName == DOWNLOAD_META_DATA_ANNO) {
-        let metaData = this.getDownloadMetaData();
-        let annotatedMetaData = this._getAnnotatedMetaData();
-        metaData.endTime = annotatedMetaData.endTime;
-        if ("fileSize" in annotatedMetaData) {
-          metaData.fileSize = annotatedMetaData.fileSize;
-        } else {
-          delete metaData.fileSize;
-        }
-
-        if (metaData.state != annotatedMetaData.state) {
-          metaData.state = annotatedMetaData.state;
-          if (this._element.selected) {
-            goUpdateDownloadCommands();
-          }
-        }
-
-        this._updateDownloadStatusUI();
-      } else if (aAnnoName == DESTINATION_FILE_URI_ANNO) {
-        let metaData = this.getDownloadMetaData();
-        let targetFileURI = this._getAnnotation(DESTINATION_FILE_URI_ANNO);
-        [metaData.filePath, metaData.fileName] =
-            this._extractFilePathAndNameFromFileURI(targetFileURI);
-        metaData.displayName = metaData.fileName;
-        this._updateDisplayNameAndIcon();
-
-        if (this._targetFileInfoFetched) {
-          // This will also update the download commands if necessary.
-          this._targetFileInfoFetched = false;
-          this._fetchTargetFileInfo();
-        }
-      }
-    }
-  },
-
-  /* DownloadView */
-  onStateChange(aOldState) {
+  onStateChanged(aOldState) {
     let metaData = this.getDownloadMetaData();
     metaData.state = this.dataItem.state;
     if (aOldState != nsIDM.DOWNLOAD_FINISHED && aOldState != metaData.state) {
       // See comment in DVI_onStateChange in downloads.js (the panel-view)
       this._element.setAttribute("image", this._getIcon() + "&state=normal");
       metaData.fileSize = this._dataItem.maxBytes;
       if (this._targetFileInfoFetched) {
         this._targetFileInfoFetched = false;
         this._fetchTargetFileInfo();
       }
     }
 
     this._updateDownloadStatusUI();
+
     if (this._element.selected) {
       goUpdateDownloadCommands();
     } else {
       goUpdateCommand("downloadsCmd_clearDownloads");
     }
   },
 
-  /* DownloadView */
-  onProgressChange() {
+  onChanged() {
     this._updateDownloadStatusUI();
   },
 
   /* nsIController */
   isCommandEnabled(aCommand) {
     // The only valid command for inactive elements is cmd_delete.
     if (!this.active && aCommand != "cmd_delete") {
       return false;
@@ -832,25 +767,16 @@ DownloadsPlacesView.prototype = {
   get active() this._active,
   set active(val) {
     this._active = val;
     if (this._active)
       this._ensureVisibleElementsAreActive();
     return this._active;
   },
 
-  _forEachDownloadElementShellForURI(aURI, aCallback) {
-    if (this._downloadElementsShellsForURI.has(aURI)) {
-      let downloadElementShells = this._downloadElementsShellsForURI.get(aURI);
-      for (let des of downloadElementShells) {
-        aCallback(des);
-      }
-    }
-  },
-
   _getAnnotationsFor(aURI) {
     if (!this._cachedAnnotations) {
       this._cachedAnnotations = new Map();
       for (let name of [ DESTINATION_FILE_URI_ANNO,
                          DOWNLOAD_META_DATA_ANNO ]) {
         let results = PlacesUtils.annotations.getAnnotationsWithName(name);
         for (let result of results) {
           let url = result.uri.spec;
@@ -929,17 +855,17 @@ DownloadsPlacesView.prototype = {
     //    another shell for the download (so we have one shell for each data
     //    item).
     //
     // Note: If a cancelled session download is already in the list, and the
     // download is retired, onDataItemAdded is called again for the same
     // data item. Thus, we also check that we make sure we don't have a view item
     // already.
     if (!shouldCreateShell &&
-        aDataItem && this.getViewItem(aDataItem) == null) {
+        aDataItem && !this._viewItemsForDataItems.has(aDataItem)) {
       // If there's a past-download-only shell for this download-uri with no
       // associated data item, use it for the new data item. Otherwise, go ahead
       // and create another shell.
       shouldCreateShell = true;
       for (let shell of shellsForURI) {
         if (!shell.dataItem) {
           shouldCreateShell = false;
           shell.dataItem = aDataItem;
@@ -1046,17 +972,17 @@ DownloadsPlacesView.prototype = {
   },
 
   _removeSessionDownloadFromView(aDataItem) {
     let shells = this._downloadElementsShellsForURI.get(aDataItem.uri);
     if (shells.size == 0) {
       throw new Error("Should have had at leaat one shell for this uri");
     }
 
-    let shell = this.getViewItem(aDataItem);
+    let shell = this._viewItemsForDataItems.get(aDataItem);
     if (!shells.has(shell)) {
       throw new Error("Missing download element shell in shells list for url");
     }
 
     // If there's more than one item for this download uri, we can let the
     // view item for this this particular data item go away.
     // If there's only one item for this download uri, we should only
     // keep it if it is associated with a history download.
@@ -1272,31 +1198,19 @@ DownloadsPlacesView.prototype = {
   nodeInserted(aParent, aPlacesNode) {
     this._addDownloadData(null, aPlacesNode);
   },
 
   nodeRemoved(aParent, aPlacesNode, aOldIndex) {
     this._removeHistoryDownloadFromView(aPlacesNode);
   },
 
-  nodeIconChanged(aNode) {
-    this._forEachDownloadElementShellForURI(aNode.uri,
-                                            des => des.placesNodeIconChanged());
-  },
-
-  nodeAnnotationChanged(aNode, aAnnoName) {
-    this._forEachDownloadElementShellForURI(aNode.uri,
-                                            des => des.placesNodeAnnotationChanged(aAnnoName));
-  },
-
-  nodeTitleChanged(aNode, aNewTitle) {
-    this._forEachDownloadElementShellForURI(aNode.uri,
-                                            des => des.placesNodeTitleChanged());
-  },
-
+  nodeAnnotationChanged() {},
+  nodeIconChanged() {},
+  nodeTitleChanged() {},
   nodeKeywordChanged() {},
   nodeDateAddedChanged() {},
   nodeLastModifiedChanged() {},
   nodeHistoryDetailsChanged() {},
   nodeTagsChanged() {},
   sortingChanged() {},
   nodeMoved() {},
   nodeURIChanged() {},
@@ -1357,18 +1271,24 @@ DownloadsPlacesView.prototype = {
   onDataItemAdded(aDataItem, aNewest) {
     this._addDownloadData(aDataItem, null, aNewest);
   },
 
   onDataItemRemoved(aDataItem) {
     this._removeSessionDownloadFromView(aDataItem);
   },
 
-  getViewItem(aDataItem) {
-    return this._viewItemsForDataItems.get(aDataItem, null);
+  // DownloadsView
+  onDataItemStateChanged(aDataItem, aOldState) {
+    this._viewItemsForDataItems.get(aDataItem).onStateChanged(aOldState);
+  },
+
+  // DownloadsView
+  onDataItemChanged(aDataItem) {
+    this._viewItemsForDataItems.get(aDataItem).onChanged();
   },
 
   supportsCommand(aCommand) {
     if (DOWNLOAD_VIEW_SUPPORTED_COMMANDS.indexOf(aCommand) != -1) {
       // The clear-downloads command may be performed by the toolbar-button,
       // which can be focused on OS X.  Thus enable this command even if the
       // richlistbox is not focused.
       // For other commands, be prudent and disable them unless the richlistview
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -549,17 +549,17 @@ const DownloadsPanel = {
         DownloadsCommon.error("Downloads button cannot be found.");
         return;
       }
 
       // When the panel is opened, we check if the target files of visible items
       // still exist, and update the allowed items interactions accordingly.  We
       // do these checks on a background thread, and don't prevent the panel to
       // be displayed while these checks are being performed.
-      for each (let viewItem in DownloadsView._viewItems) {
+      for (let viewItem of DownloadsView._visibleViewItems.values()) {
         viewItem.verifyTargetExists();
       }
 
       DownloadsCommon.log("Opening downloads panel popup.");
       this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, null);
     });
   },
 };
@@ -668,21 +668,21 @@ const DownloadsView = {
    * Ordered array of all DownloadsDataItem objects.  We need to keep this array
    * because only a limited number of items are shown at once, and if an item
    * that is currently visible is removed from the list, we might need to take
    * another item from the array and make it appear at the bottom.
    */
   _dataItems: [],
 
   /**
-   * Object containing the available DownloadsViewItem objects, indexed by their
-   * numeric download identifier.  There is a limited number of view items in
-   * the panel at any given time.
+   * Associates the visible DownloadsDataItem objects with their corresponding
+   * DownloadsViewItem object.  There is a limited number of view items in the
+   * panel at any given time.
    */
-  _viewItems: {},
+  _visibleViewItems: new Map(),
 
   /**
    * Called when the number of items in the list changes.
    */
   _itemCountChanged() {
     DownloadsCommon.log("The downloads item count has changed - we are tracking",
                         this._dataItems.length, "downloads in total.");
     let count = this._dataItems.length;
@@ -809,73 +809,77 @@ const DownloadsView = {
         // Reinsert the next item into the panel.
         this._addViewItem(this._dataItems[this.kItemCountLimit - 1], false);
       }
     }
 
     this._itemCountChanged();
   },
 
-  /**
-   * Returns the view item associated with the provided data item for this view.
-   *
-   * @param aDataItem
-   *        DownloadsDataItem object for which the view item is requested.
-   *
-   * @return Object that can be used to notify item status events.
-   */
-  getViewItem(aDataItem) {
-    // If the item is visible, just return it, otherwise return a mock object
-    // that doesn't react to notifications.
-    if (aDataItem.downloadGuid in this._viewItems) {
-      return this._viewItems[aDataItem.downloadGuid];
+  // DownloadsView
+  onDataItemStateChanged(aDataItem, aOldState) {
+    let viewItem = this._visibleViewItems.get(aDataItem);
+    if (viewItem) {
+      viewItem.onStateChanged(aOldState);
     }
-    return this._invisibleViewItem;
+  },
+
+  // DownloadsView
+  onDataItemChanged(aDataItem) {
+    let viewItem = this._visibleViewItems.get(aDataItem);
+    if (viewItem) {
+      viewItem.onChanged();
+    }
   },
 
   /**
-   * Mock DownloadsDataItem object that doesn't react to notifications.
+   * Associates each richlistitem for a download with its corresponding
+   * DownloadsViewItemController object.
    */
-  _invisibleViewItem: Object.freeze({
-    onStateChange() {},
-    onProgressChange() {},
-  }),
+  _controllersForElements: new Map(),
+
+  controllerForElement(element) {
+    return this._controllersForElements.get(element);
+  },
 
   /**
    * Creates a new view item associated with the specified data item, and adds
    * it to the top or the bottom of the list.
    */
   _addViewItem(aDataItem, aNewest)
   {
     DownloadsCommon.log("Adding a new DownloadsViewItem to the downloads list.",
                         "aNewest =", aNewest);
 
     let element = document.createElement("richlistitem");
     let viewItem = new DownloadsViewItem(aDataItem, element);
-    this._viewItems[aDataItem.downloadGuid] = viewItem;
+    this._visibleViewItems.set(aDataItem, viewItem);
+    let viewItemController = new DownloadsViewItemController(aDataItem);
+    this._controllersForElements.set(element, viewItemController);
     if (aNewest) {
       this.richListBox.insertBefore(element, this.richListBox.firstChild);
     } else {
       this.richListBox.appendChild(element);
     }
   },
 
   /**
    * Removes the view item associated with the specified data item.
    */
   _removeViewItem(aDataItem) {
     DownloadsCommon.log("Removing a DownloadsViewItem from the downloads list.");
-    let element = this.getViewItem(aDataItem)._element;
+    let element = this._visibleViewItems.get(aDataItem)._element;
     let previousSelectedIndex = this.richListBox.selectedIndex;
     this.richListBox.removeChild(element);
     if (previousSelectedIndex != -1) {
       this.richListBox.selectedIndex = Math.min(previousSelectedIndex,
                                                 this.richListBox.itemCount - 1);
     }
-    delete this._viewItems[aDataItem.downloadGuid];
+    this._visibleViewItems.delete(aDataItem);
+    this._controllersForElements.delete(element);
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// User interface event functions
 
   /**
    * Helper function to do commands on a specific download item.
    *
@@ -886,17 +890,17 @@ const DownloadsView = {
    * @param aCommand
    *        The command to be performed.
    */
   onDownloadCommand(aEvent, aCommand) {
     let target = aEvent.target;
     while (target.nodeName != "richlistitem") {
       target = target.parentNode;
     }
-    new DownloadsViewItemController(target).doCommand(aCommand);
+    DownloadsView.controllerForElement(target).doCommand(aCommand);
   },
 
   onDownloadClick(aEvent) {
     // Handle primary clicks only, and exclude the action button.
     if (aEvent.button == 0 &&
         !aEvent.originalTarget.hasAttribute("oncommand")) {
       goDoCommand("downloadsCmd_open");
     }
@@ -960,18 +964,18 @@ const DownloadsView = {
   },
 
   onDownloadDragStart(aEvent) {
     let element = this.richListBox.selectedItem;
     if (!element) {
       return;
     }
 
-    let controller = new DownloadsViewItemController(element);
-    let localFile = controller.dataItem.localFile;
+    let localFile = DownloadsView.controllerForElement(element)
+                                 .dataItem.localFile;
     if (!localFile.exists()) {
       return;
     }
 
     let dataTransfer = aEvent.dataTransfer;
     dataTransfer.mozSetDataAt("application/x-moz-file", localFile, 0);
     dataTransfer.effectAllowed = "copyMove";
     var url = Services.io.newFileURI(localFile).spec;
@@ -1005,18 +1009,16 @@ function DownloadsViewItem(aDataItem, aE
   // as bug 239948 comment 12 is handled, the "file" property will be always a
   // file URL rather than a file name.  At that point we should remove the "//"
   // (double slash) from the icon URI specification (see test_moz_icon_uri.js).
   this.image = "moz-icon://" + this.dataItem.file + "?size=32";
 
   let attributes = {
     "type": "download",
     "class": "download-state",
-    "id": "downloadsItem_" + this.dataItem.downloadGuid,
-    "downloadGuid": this.dataItem.downloadGuid,
     "state": this.dataItem.state,
     "progress": this.dataItem.inProgress ? this.dataItem.percentComplete : 100,
     "target": this.dataItem.target,
     "image": this.image
   };
 
   for (let attributeName in attributes) {
     this._element.setAttribute(attributeName, attributes[attributeName]);
@@ -1047,17 +1049,17 @@ DownloadsViewItem.prototype = {
   //////////////////////////////////////////////////////////////////////////////
   //// Callback functions from DownloadsData
 
   /**
    * Called when the download state might have changed.  Sometimes the state of
    * the download might be the same as before, if the data layer received
    * multiple events for the same download.
    */
-  onStateChange(aOldState) {
+  onStateChanged(aOldState) {
     // If a download just finished successfully, it means that the target file
     // now exists and we can extract its specific icon.  To ensure that the icon
     // is reloaded, we must change the URI used by the XUL image element, for
     // example by adding a query parameter.  Since this URI has a "moz-icon"
     // scheme, this only works if we add one of the parameters explicitly
     // supported by the nsIMozIconURI interface.
     if (aOldState != Ci.nsIDownloadManager.DOWNLOAD_FINISHED &&
         aOldState != this.dataItem.state) {
@@ -1067,24 +1069,22 @@ DownloadsViewItem.prototype = {
       // successfully, without checking the condition in the background.  If the
       // panel is already open, this will take effect immediately.  If the panel
       // is opened later, a new background existence check will be performed.
       this._element.setAttribute("exists", "true");
     }
 
     // Update the user interface after switching states.
     this._element.setAttribute("state", this.dataItem.state);
-    this._updateProgress();
-    this._updateStatusLine();
   },
 
   /**
    * Called when the download progress has changed.
    */
-  onProgressChange() {
+  onChanged() {
     this._updateProgress();
     this._updateStatusLine();
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Functions for updating the user interface
 
   /**
@@ -1278,32 +1278,32 @@ const DownloadsViewController = {
   isCommandEnabled(aCommand) {
     // Handle commands that are not selection-specific.
     if (aCommand == "downloadsCmd_clearList") {
       return DownloadsCommon.getData(window).canRemoveFinished;
     }
 
     // Other commands are selection-specific.
     let element = DownloadsView.richListBox.selectedItem;
-    return element &&
-           new DownloadsViewItemController(element).isCommandEnabled(aCommand);
+    return element && DownloadsView.controllerForElement(element)
+                                   .isCommandEnabled(aCommand);
   },
 
   doCommand(aCommand) {
     // If this command is not selection-specific, execute it.
     if (aCommand in this.commands) {
       this.commands[aCommand].apply(this);
       return;
     }
 
     // Other commands are selection-specific.
     let element = DownloadsView.richListBox.selectedItem;
     if (element) {
       // The doCommand function also checks if the command is enabled.
-      new DownloadsViewItemController(element).doCommand(aCommand);
+      DownloadsView.controllerForElement(element).doCommand(aCommand);
     }
   },
 
   onEvent() {},
 
   //////////////////////////////////////////////////////////////////////////////
   //// Other functions
 
@@ -1329,19 +1329,18 @@ const DownloadsViewController = {
 
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadsViewItemController
 
 /**
  * Handles all the user interaction events, in particular the "commands",
  * related to a single item in the downloads list widgets.
  */
-function DownloadsViewItemController(aElement) {
-  let downloadGuid = aElement.getAttribute("downloadGuid");
-  this.dataItem = DownloadsCommon.getData(window).dataItems[downloadGuid];
+function DownloadsViewItemController(aDataItem) {
+  this.dataItem = aDataItem;
 }
 
 DownloadsViewItemController.prototype = {
   //////////////////////////////////////////////////////////////////////////////
   //// Command dispatching
 
   /**
    * The DownloadDataItem controlled by this object.
--- a/browser/components/downloads/test/browser/browser_basic_functionality.js
+++ b/browser/components/downloads/test/browser/browser_basic_functionality.js
@@ -44,12 +44,12 @@ add_task(function* test_basic_functional
   let richlistbox = document.getElementById("downloadsListBox");
   /* disabled for failing intermittently (bug 767828)
     is(richlistbox.children.length, DownloadData.length,
        "There is the correct number of richlistitems");
   */
   let itemCount = richlistbox.children.length;
   for (let i = 0; i < itemCount; i++) {
     let element = richlistbox.children[itemCount - i - 1];
-    let dataItem = new DownloadsViewItemController(element).dataItem;
+    let dataItem = DownloadsView.controllerForElement(element).dataItem;
     is(dataItem.state, DownloadData[i].state, "Download states match up");
   }
 });
--- a/browser/components/loop/MozLoopPushHandler.jsm
+++ b/browser/components/loop/MozLoopPushHandler.jsm
@@ -4,388 +4,866 @@
 
 "use strict";
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
-Cu.import("resource://gre/modules/Promise.jsm");
+
+const {MozLoopService} = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
+const consoleLog = MozLoopService.log;
 
 this.EXPORTED_SYMBOLS = ["MozLoopPushHandler"];
 
-XPCOMUtils.defineLazyModuleGetter(this, "console",
-                                  "resource://gre/modules/devtools/Console.jsm");
+const CONNECTION_STATE_CLOSED = 0;
+const CONNECTION_STATE_CONNECTING = 1;
+const CONNECTION_STATE_OPEN = 2;
+
+const SERVICE_STATE_OFFLINE = 0;
+const SERVICE_STATE_PENDING = 1;
+const SERVICE_STATE_ACTIVE = 2;
+
+function PushSocket(webSocket = null) {
+  this._websocket = webSocket;
+}
+
+PushSocket.prototype = {
+
+  /**
+   * Open push-notification websocket.
+   *
+   * @param {String} pushUri
+   * @param {Function} onMsg(aMsg) callback receives any incoming messages
+   *                   aMsg is constructed from the json payload; both
+   *                   text and binary message reception are mapped to this
+   *                   callback.
+   * @param {Function} onStart called when the socket is connected
+   * @param {Function} onClose(aCode, aReason) called when the socket closes;
+   *                   both near and far side close events map to this
+   *                   callback.
+   *                   aCode is any status code returned on close
+   *                   aReason is any string returned on close
+   */
+
+  connect: function(pushUri, onMsg, onStart, onClose) {
+    if (!pushUri || !onMsg || !onStart || !onClose) {
+      throw new Error("PushSocket: missing required parameter(s):"
+                      (pushUri ? "" : " pushUri") +
+                      (onMsg ? "" : " onMsg") +
+                      (onStart ? "" : " onStart") +
+                      (onClose ? "" : " onClose"));
+    }
+
+    this._onMsg = onMsg;
+    this._onStart = onStart;
+    this._onClose = onClose;
+
+    if (!this._websocket) {
+      this._websocket = Cc["@mozilla.org/network/protocol;1?name=wss"]
+                          .createInstance(Ci.nsIWebSocketChannel);
+    }
+
+    let uri = Services.io.newURI(pushUri, null, null);
+    this._websocket.protocol = "push-notification";
+    this._websocket.asyncOpen(uri, pushUri, this, null);
+  },
+
+  /**
+   * nsIWebSocketListener method, handles the start of the websocket stream.
+   *
+   * @param {nsISupports} aContext Not used
+   */
+  onStart: function() {
+    this._socketOpen = true;
+    this._onStart();
+  },
+
+  /**
+   * nsIWebSocketListener method, called when the websocket is closed locally.
+   *
+   * @param {nsISupports} aContext Not used
+   * @param {nsresult} aStatusCode
+   */
+  onStop: function(aContext, aStatusCode) {
+    this._socketOpen = false;
+    this._onClose(aStatusCode, "websocket onStop");
+  },
+
+  /**
+   * nsIWebSocketListener method, called when the websocket is closed
+   * by the far end.
+   *
+   * @param {nsISupports} aContext Not used
+   * @param {integer} aCode the websocket closing handshake close code
+   * @param {String} aReason the websocket closing handshake close reason
+   */
+  onServerClose: function(aContext, aCode, aReason) {
+    this._socketOpen = false;
+    this._onClose(aCode, aReason);
+  },
+
+  /**
+   * nsIWebSocketListener method, called when the websocket receives
+   * a text message (normally json encoded).
+   *
+   * @param {nsISupports} aContext Not used
+   * @param {String} aMsg The message data
+   */
+  onMessageAvailable: function(aContext, aMsg) {
+    consoleLog.log("PushSocket: Message received: ", aMsg);
+    if (!this._socketOpen) {
+      consoleLog.error("Message received in Winsocket closed state");
+      return;
+    }
+
+    try {
+      this._onMsg(JSON.parse(aMsg));
+    }
+    catch (error) {
+      consoleLog.error("PushSocket: error parsing message payload - ", error);
+    }
+  },
+
+  /**
+   * nsIWebSocketListener method, called when the websocket receives a binary message.
+   * This class assumes that it is connected to a SimplePushServer and therefore treats
+   * the message payload as json encoded.
+   *
+   * @param {nsISupports} aContext Not used
+   * @param {String} aMsg The message data
+   */
+  onBinaryMessageAvailable: function(aContext, aMsg) {
+    consoleLog.log("PushSocket: Binary message received: ", aMsg);
+    if (!this._socketOpen) {
+      consoleLog.error("PushSocket: message receive in Winsocket closed state");
+      return;
+    }
+
+    try {
+      this._onMsg(JSON.parse(aMsg));
+    }
+    catch (error) {
+      consoleLog.error("PushSocket: error parsing message payload - ", error);
+    }
+  },
+
+  /**
+   * Create a JSON encoded message payload and send via websocket.
+   *
+   * @param {Object} aMsg Message to send.
+   *
+   * @returns {Boolean} true if message has been sent, false otherwise
+   */
+  send: function(aMsg) {
+    if (!this._socketOpen) {
+      consoleLog.error("PushSocket: attempt to send before websocket is open");
+      return false;
+    }
+
+    let msg;
+    try {
+      msg = JSON.stringify(aMsg);
+    }
+    catch (error) {
+      consoleLog.error("PushSocket: JSON generation error - ", error);
+      return false;
+    }
+
+    try {
+      this._websocket.sendMsg(msg);
+      consoleLog.log("PushSocket: Message sent: ", msg);
+    }
+    // guard against the case that the websocket has closed before this call.
+    catch (e) {
+      consoleLog.warn("PushSocket: websocket send error", e);
+      return false;
+    }
+
+    return true;
+  },
+
+  /**
+   * Close the websocket.
+   */
+  close: function() {
+    if (!this._socketOpen) {
+      return;
+    }
+
+    this._socketOpen = false;
+    consoleLog.info("PushSocket: websocket closing");
+
+    // Do not pass through any callbacks after this point.
+    this._onStart = function() {};
+    this._onMsg = this._onStart;
+    this._onClose = this._onStart;
+
+    try {
+      this._websocket.close(this._websocket.CLOSE_NORMAL);
+    }
+    catch (e) {}
+  },
+};
+
+
+/**
+ * Create a RetryManager object. Class to handle retrying a UserAgent
+ * to PushServer request following a retry back-off scheme managed by
+ * this class. The current delay mechanism is to double the delay
+ * each time an operation to be retried until a maximum is met.
+ *
+ * @param {Integer} startDelay The initial delay interval in milliseconds.
+ * @param {Integer} maxDelay Maximum time delay value in milliseconds.
+ */
+function RetryManager (startDelay, maxDelay) {
+  if (!startDelay || !maxDelay) {
+    throw new Error("RetryManager: missing required parameters(s)" +
+                     (startDelay ? "" : " startDelay") +
+                     (maxDelay ? "" : " maxDelay"));
+  }
+
+  this._startDelay = startDelay;
+  // The maximum delay cannot be less than the starting delay.
+  this._maxDelay = maxDelay > startDelay ? maxDelay : startDelay;
+}
+
+RetryManager.prototype = {
+  /**
+   * Method to handle retrying a UserAgent to PushServer request.
+   *
+   * @param {Function} delayedOp Function to call after current delay is satisfied
+   */
+  retry: function(delayedOp) {
+    if (!this._timeoutID) {
+      this._retryDelay = this._startDelay;
+    } else {
+      clearTimeout(this._timeoutID);
+      let nextDelay = this._retryDelay * 2;
+      this._retryDelay = nextDelay > this._maxDelay ? this._maxDelay : nextDelay;
+    }
+
+    this._timeoutID = setTimeout(delayedOp, this._retryDelay);
+    consoleLog.log("PushHandler: retry delay set for ", this._retryDelay);
+  },
+
+  /**
+   * Method used to reset the delay back-off logic and clear any currently
+   * running delay timeout.
+   */
+  reset: function() {
+    if (this._timeoutID) {
+      clearTimeout(this._timeoutID);
+      this._timeoutID = null;
+    }
+  },
+};
+
+/**
+ * Create a PingMonitor object. An object instance will periodically execute
+ * a ping send function and if not reset, will then execute an error function.
+ *
+ * @param {Function} pingFunc Function that is called after a ping interval
+ *                   has expired without being restart.
+ * @param {Function} onTimeout Function that is called after a ping timeout
+ *                   interval has expired without restart being called.
+ * @param {Integer} interval Timeout value in milliseconds between successive
+ *                  pings or between the last restart call and a ping.
+ *                  When this interval expires, pingFunc is called and the
+ *                  timeout interval is started.
+ * @param {Integer} timeout Timeout value in milliseconds between a call to
+ *                  pingFunc and a call to onTimeout unless restart is called.
+ *                  Restart will begin the ping timeout interval again.
+ */
+function PingMonitor(pingFunc, onTimeout, interval, timeout) {
+  if (!pingFunc || !onTimeout || !interval || !timeout) {
+    throw new Error("PingMonitor: missing required parameters");
+  }
+  this._onTimeout = onTimeout;
+  this._pingFunc = pingFunc;
+  this._pingInterval = interval;
+  this._pingTimeout = timeout;
+}
+
+PingMonitor.prototype = {
+  /**
+   * Function to restart the ping timeout and cancel any current timeout operation.
+   */
+  restart: function () {
+    consoleLog.info("PushHandler: ping timeout restart");
+    this.stop();
+    this._pingTimerID = setTimeout(() => {this._pingSend()}, this._pingInterval);
+  },
+
+  /**
+   * Function to stop the PingMonitor.
+   */
+  stop: function() {
+    if (this._pingTimerID){
+      clearTimeout(this._pingTimerID);
+      this._pingTimerID = undefined;
+    }
+  },
+
+  _pingSend: function () {
+    consoleLog.info("PushHandler: ping sent");
+    this._pingTimerID = setTimeout(this._onTimeout, this._pingTimeout);
+    this._pingFunc();
+  },
+};
+
 
 /**
  * We don't have push notifications on desktop currently, so this is a
  * workaround to get them going for us.
  */
 let MozLoopPushHandler = {
   // This is the uri of the push server.
   pushServerUri: undefined,
   // Records containing the registration and notification callbacks indexed by channelID.
   // Each channel will be registered with the PushServer.
-  channels: {},
+  channels: new Map(),
   // This is the UserAgent UUID assigned by the PushServer
   uaID: undefined,
   // Each successfully registered channelID is used as a key to hold its pushEndpoint URL.
   registeredChannels: {},
-
-  _channelsToRegister: {},
+  // Push protocol state variable
+  serviceState: SERVICE_STATE_OFFLINE,
+  // Websocket connection state variable
+  connectionState: CONNECTION_STATE_CLOSED,
+  // Contains channels that need to be registered with the PushServer
+  _channelsToRegister: [],
 
-  _minRetryDelay_ms: (() => {
+  get _startRetryDelay_ms() {
     try {
-      return Services.prefs.getIntPref("loop.retry_delay.start")
+      return Services.prefs.getIntPref("loop.retry_delay.start");
+    }
+    catch (e) {
+      return 60000; // 1 minute
+    }
+  },
+
+  get _maxRetryDelay_ms() {
+    try {
+      return Services.prefs.getIntPref("loop.retry_delay.limit");
     }
     catch (e) {
-      return 60000 // 1 minute
+      return 300000; // 5 minutes
     }
-  })(),
+  },
 
-  _maxRetryDelay_ms: (() => {
+  get _pingInterval_ms() {
     try {
-      return Services.prefs.getIntPref("loop.retry_delay.limit")
+      return Services.prefs.getIntPref("loop.ping.interval");
     }
     catch (e) {
-      return 300000 // 5 minutes
+      return 18000000; // 30 minutes
     }
-  })(),
+  },
+
+  get _pingTimeout_ms() {
+    try {
+      return Services.prefs.getIntPref("loop.ping.timeout");
+    }
+    catch (e) {
+      return 10000; // 10 seconds
+    }
+  },
 
    /**
     * Inializes the PushHandler and opens a socket with the PushServer.
     * It will automatically say hello and register any channels
     * that are found in the work queue at that point.
     *
     * @param {Object} options Set of configuration options. Currently,
     *                 the only option is mocketWebSocket which will be
     *                 used for testing.
     */
   initialize: function(options = {}) {
+    consoleLog.info("PushHandler: initialize options = ", options);
     if (Services.io.offline) {
-      console.warn("MozLoopPushHandler - IO offline");
+      consoleLog.warn("PushHandler: IO offline");
       return false;
     }
 
     if (this._initDone) {
       return true;
     }
 
     this._initDone = true;
+    this._retryManager = new RetryManager(this._startRetryDelay_ms,
+                                          this._maxRetryDelay_ms);
+    // Send an empty json payload as a ping.
+    // Close the websocket and re-open if a timeout occurs.
+    this._pingMonitor = new PingMonitor(() => this._pushSocket.send({}),
+                                        () => this._restartConnection(),
+                                        this._pingInterval_ms,
+                                        this._pingTimeout_ms);
 
     if ("mockWebSocket" in options) {
       this._mockWebSocket = options.mockWebSocket;
     }
 
     this._openSocket();
     return true;
   },
 
+  /**
+   * Reset and clear PushServer connection.
+   * Returns MozLoopPushHandler to pre-initialized state.
+   */
+  shutdown: function() {
+    consoleLog.info("PushHandler: shutdown");
+    if (!this._initDone) {
+      return;
+    }
+
+    this._initDone = false;
+    this._retryManager.reset();
+    this._pingMonitor.stop();
+
+    // Un-register each active notification channel
+    if (this.connectionState === CONNECTION_STATE_OPEN) {
+      Object.keys(this.registeredChannels).forEach((id) => {
+        let unRegMsg = {messageType: "unregister",
+                        channelID: id};
+        this._pushSocket.send(unRegMsg);
+      });
+      this.registeredChannels = {};
+    }
+
+    this.connectionState = CONNECTION_STATE_CLOSED;
+    this.serviceState = SERVICE_STATE_OFFLINE;
+    this._pushSocket.close();
+    this._pushSocket = undefined;
+    // NOTE: this PushSocket instance will not be released until at least
+    // the websocket referencing it as an nsIWebSocketListener is released.
+    this.channels.clear();
+    this.uaID = undefined;
+    this.pushUrl = undefined;
+    this.pushServerUri = undefined;
+  },
+
    /**
-    * Start registration of a PushServer notification channel.
-    * connection, it will automatically say hello and register the channel
-    * id with the server.
+    * Assign a channel to be registered with the PushServer
+    * This channel will be registered when a connection to the PushServer
+    * has been established or re-registered after a connection has been lost
+    * and re-established. Calling this more than once for the same channel
+    * has no additional effect.
     *
     * onRegistered callback parameters:
     * - {String|null} err: Encountered error, if any
     * - {String} url: The push url obtained from the server
+    * - {String} channelID The channelID on which the notification was sent.
     *
     * onNotification parameters:
     * - {String} version The version string received from the push server for
     *                    the notification.
     * - {String} channelID The channelID on which the notification was sent.
     *
     * @param {String} channelID Channel ID to use in registration.
     *
     * @param {Function} onRegistered Callback to be called once we are
-    *                     registered.
+    *                   registered.
+    *                   NOTE: This function can be called multiple times if
+    *                   the PushServer generates new pushURLs due to
+    *                   re-registration due to network loss or PushServer
+    *                   initiated re-assignment.
     * @param {Function} onNotification Callback to be called when a
-    *                     push notification is received (may be called multiple
-    *                     times).
+    *                   push notification is received (may be called multiple
+    *                   times).
     */
   register: function(channelID, onRegistered, onNotification) {
     if (!channelID || !onRegistered || !onNotification) {
-      throw new Error("missing required parameter(s):"
-                      + (channelID ? "" : " channelID")
-                      + (onRegistered ? "" : " onRegistered")
-                      + (onNotification ? "" : " onNotification"));
+      throw new Error("missing required parameter(s):" +
+                      (channelID ? "" : " channelID") +
+                      (onRegistered ? "" : " onRegistered") +
+                      (onNotification ? "" : " onNotification"));
     }
 
-    // If the channel is already registered, callback with an error immediately
-    // so we don't leave code hanging waiting for an onRegistered callback.
-    if (channelID in this.channels) {
-      onRegistered("error: channel already registered: " + channelID);
+    consoleLog.info("PushHandler: channel registration: ", channelID);
+    if (this.channels.has(channelID)) {
+      // If this channel has an active registration with the PushServer
+      // call the onRegister callback with the URL.
+      if (this.registeredChannels[channelID]) {
+        onRegistered(null, this.registeredChannels[channelID], channelID);
+      }
+      // Update the channel record.
+      this.channels.set(channelID, {onRegistered: onRegistered,
+                        onNotification: onNotification});
       return;
     }
 
-    this.channels[channelID] = {
-      onRegistered: onRegistered,
-      onNotification: onNotification
-    };
+    this.channels.set(channelID, {onRegistered: onRegistered,
+                                  onNotification: onNotification});
+    this._channelsToRegister.push(channelID);
+    this._registerChannels();
+  },
+  
+  /**
+   * Un-register a notification channel.
+   *
+   * @param {String} channelID Notification channel ID.
+   */
+  unregister: function(channelID) {
+    consoleLog.info("MozLoopPushHandler: un-register channel ", channelID);
+    if (!this.channels.has(channelID)) {
+      return;
+    }
 
-    // If registration is in progress, simply add to the work list.
-    // Else, re-start a registration cycle.
-    if (this._registrationID) {
-      this._channelsToRegister.push(channelID);
-    } else {
-      this._registerChannels();
+    this.channels.delete(channelID);
+
+    if (this.registeredChannels[channelID]) {
+      delete this.registeredChannels[channelID];
+      if (this.connectionState === CONNECTION_STATE_OPEN) {
+        this._pushSocket.send({messageType: "unregister",
+                               channelID: channelID});
+      }
     }
   },
 
   /**
-   * Listener method, handles the start of the websocket stream.
+   * Handles the start of the websocket stream.
    * Sends a hello message to the server.
    *
-   * @param {nsISupports} aContext Not used
    */
-  onStart: function() {
-    this._retryEnd();
-    // If a uaID has already been assigned, assume this is a re-connect
-    // and send the uaID in order to re-synch with the
-    // PushServer. If a registration has been completed, send the channelID.
+  _onStart: function() {
+    consoleLog.info("PushHandler: websocket open, sending 'hello' to PushServer");
+    this.connectionState = CONNECTION_STATE_OPEN;
+    // If a uaID has already been assigned, assume this is a re-connect;
+    // send the uaID and channelIDs in order to re-synch with the
+    // PushServer. The PushServer does not need to accept the existing channelIDs
+    // and may issue new channelIDs along with new pushURLs.
+    this.serviceState = SERVICE_STATE_PENDING;
     let helloMsg = {
-          messageType: "hello",
-          uaid: this.uaID || "",
-          channelIDs: Object.keys(this.registeredChannels)};
-
-    this._retryOperation(() => this.onStart(), this._maxRetryDelay_ms);
-    try { // in case websocket has closed before this handler is run
-      this._websocket.sendMsg(JSON.stringify(helloMsg));
-    }
-    catch (e) {console.warn("MozLoopPushHandler::onStart websocket.sendMsg() failure");}
+      messageType: "hello",
+      uaid: this.uaID || "",
+      channelIDs: this.uaID ? Object.keys(this.registeredChannels) : []
+    };
+    // The Simple PushServer spec does not allow a retry of the Hello handshake but requires that the socket
+    // be closed and another socket openned in order to re-attempt the handshake.
+    // Here, the retryManager is not set up to retry the sending another 'hello' message: the timeout will
+    // trigger closing the websocket and starting the connection again from the start.
+    this._retryManager.reset();
+    this._retryManager.retry(() => this._restartConnection());
+    this._pushSocket.send(helloMsg);
   },
 
   /**
-   * Listener method, called when the websocket is closed.
+   * Handles websocket close callbacks.
    *
-   * @param {nsISupports} aContext Not used
-   * @param {nsresult} aStatusCode Reason for stopping (NS_OK = successful)
+   * This method will continually try to re-establish a connection
+   * to the PushServer unless shutdown has been called.
    */
-  onStop: function(aContext, aStatusCode) {
-    Cu.reportError("Loop Push server web socket closed! Code: " + aStatusCode);
-    this._retryOperation(() => this._openSocket());
-  },
+  _onClose: function(aCode, aReason) {
+    this._pingMonitor.stop();
 
-  /**
-   * Listener method, called when the websocket is closed by the server.
-   * If there are errors, onStop may be called without ever calling this
-   * method.
-   *
-   * @param {nsISupports} aContext Not used
-   * @param {integer} aCode the websocket closing handshake close code
-   * @param {String} aReason the websocket closing handshake close reason
-   */
-  onServerClose: function(aContext, aCode) {
-    Cu.reportError("Loop Push server web socket closed (server)! Code: " + aCode);
-    this._retryOperation(() => this._openSocket());
-  },
+    switch (this.connectionState) {
+    case CONNECTION_STATE_OPEN:
+        this.connectionState = CONNECTION_STATE_CLOSED;
+        consoleLog.info("PushHandler: websocket closed: begin reconnect - ", aCode);
+        // The first retry is immediate
+        this._retryManager.reset();
+        this._openSocket();
+        break;
+
+      case CONNECTION_STATE_CONNECTING:
+        // Wait before re-attempting to open the websocket.
+        consoleLog.info("PushHandler: websocket closed: delay and retry - ", aCode);
+        this._retryManager.retry(() => this._openSocket());
+        break;
+     }
+   },
 
   /**
    * Listener method, called when the websocket receives a message.
    *
-   * @param {nsISupports} aContext Not used
-   * @param {String} aMsg The message data
+   * @param {Object} aMsg The message data
    */
-  onMessageAvailable: function(aContext, aMsg) {
-    let msg = JSON.parse(aMsg);
+  _onMsg: function(aMsg) {
+    // If an error property exists in the message object ignore the other
+    // properties.
+    if (aMsg.error) {
+      consoleLog.error("PushHandler: received error response msg: ", aMsg.error);
+      return;
+    }
 
-    switch(msg.messageType) {
+    // The recommended response to a ping message when the push server has nothing
+    // else to send is a blank JSON message body: {}
+    if (!aMsg.messageType && this.serviceState === SERVICE_STATE_ACTIVE) {
+      // Treat this as a ping response
+      this._pingMonitor.restart();
+      return;
+    }
+
+    switch(aMsg.messageType) {
       case "hello":
-        this._retryEnd();
-        this._isConnected = true;
-        if (this.uaID !== msg.uaid) {
-          this.uaID = msg.uaid;
-          this.registeredChannels = {};
-          this._registerChannels();
-        }
+        this._onHello(aMsg);
         break;
 
       case "register":
-        this._onRegister(msg);
+        this._onRegister(aMsg);
         break;
 
       case "notification":
-        msg.updates.forEach((update) => {
-          if (update.channelID in this.registeredChannels) {
-            this.channels[update.channelID].onNotification(update.version, update.channelID);
-          }
-        });
+        this._onNotification(aMsg);
+        break;
+
+      default:
+        consoleLog.warn("PushHandler: unknown message type = ", aMsg.messageType);
+        if (this.serviceState === SERVICE_STATE_ACTIVE) {
+          // Treat this as a ping response
+          this._pingMonitor.restart();
+        }
         break;
+     }
+   },
+
+  /**
+   * Handles hello message.
+   *
+   * This method will parse the hello response from the PushServer
+   * and determine whether registration is necessary.
+   *
+   * @param {aMsg} hello message body
+   */
+  _onHello: function(aMsg) {
+    if (this.serviceState !== SERVICE_STATE_PENDING) {
+      consoleLog.error("PushHandler: extra 'hello' response received from PushServer");
+      return;
     }
+
+    // Clear any pending timeout that will restart the connection.
+    this._retryManager.reset();
+    this.serviceState = SERVICE_STATE_ACTIVE;
+    consoleLog.info("PushHandler: 'hello' handshake complete");
+    // Start the PushServer ping monitor
+    this._pingMonitor.restart();
+    // If a new uaID is received, then any previous channel registrations
+    // are no longer valid and a Registration request is generated.
+    if (this.uaID !== aMsg.uaid) {
+      consoleLog.log("PushHandler: registering all channels");
+      this.uaID = aMsg.uaid;
+      // Re-register all channels.
+      this._channelsToRegister = [...this.channels.keys()];
+      this.registeredChannels = {};
+    }
+    // Allow queued registrations to start (or all if cleared above).
+    this._registerChannels();
   },
 
   /**
+   * Handles notification message.
+   *
+   * This method will parse the Array of updates and trigger
+   * the callback of any registered channel.
+   * This method will construct an ack message containing
+   * a set of channel version update notifications.
+   *
+   * @param {aMsg} notification message body
+   */
+  _onNotification: function(aMsg) {
+    if (this.serviceState !== SERVICE_STATE_ACTIVE ||
+       this.registeredChannels.length === 0) {
+      // Treat reception of a notification before handshake and registration
+      // are complete as a fatal error.
+      consoleLog.error("PushHandler: protocol error - notification received in wrong state");
+      this._restartConnection();
+      return;
+    }
+
+    this._pingMonitor.restart();
+    if (Array.isArray(aMsg.updates) && aMsg.updates.length > 0) {
+      let ackChannels = [];
+      aMsg.updates.forEach(update => {
+        if (update.channelID in this.registeredChannels) {
+          consoleLog.log("PushHandler: notification: version = ", update.version,
+                         ", channelID = ", update.channelID);
+          this.channels.get(update.channelID)
+            .onNotification(update.version, update.channelID);
+          ackChannels.push(update);
+        } else {
+          consoleLog.error("PushHandler: notification received for unknown channelID: ",
+                           update.channelID);
+        }
+      });
+
+      consoleLog.log("PushHandler: PusherServer 'ack': ", ackChannels);
+      this._pushSocket.send({messageType: "ack",
+                             updates: ackChannels});
+     }
+   },
+
+  /**
    * Handles the PushServer registration response.
    *
    * @param {Object} msg PushServer to UserAgent registration response (parsed from JSON).
    */
   _onRegister: function(msg) {
-    let registerNext = () => {
-      this._registrationID = this._channelsToRegister.shift();
-      this._sendRegistration(this._registrationID);
+    if (this.serviceState !== SERVICE_STATE_ACTIVE ||
+        msg.channelID != this._pendingChannelID) {
+      // Treat reception of a register response outside of a completed handshake
+      // or for a channelID not currently pending a response
+      // as an indication that the connections should be reset.
+      consoleLog.error("PushHandler: registration protocol error");
+      this._restartConnection();
+      return;
     }
 
+    this._retryManager.reset();
+    this._pingMonitor.restart();
+
     switch (msg.status) {
       case 200:
-        if (msg.channelID == this._registrationID) {
-          this._retryEnd(); // reset retry mechanism
-          this.registeredChannels[msg.channelID] = msg.pushEndpoint;
-          this.channels[msg.channelID].onRegistered(null, msg.pushEndpoint, msg.channelID);
-          registerNext();
-        }
+        consoleLog.info("PushHandler: channel registered: ", msg.channelID);
+        this.registeredChannels[msg.channelID] = msg.pushEndpoint;
+        this.channels.get(msg.channelID)
+          .onRegistered(null, msg.pushEndpoint, msg.channelID);
+        this._registerNext();
         break;
 
       case 500:
+        consoleLog.info("PushHandler: eeceived a 500 retry response from the PushServer: ",
+                        msg.channelID);
         // retry the registration request after a suitable delay
-        this._retryOperation(() => this._sendRegistration(msg.channelID));
+        this._retryManager.retry(() => this._sendRegistration(msg.channelID));
         break;
 
       case 409:
-        this.channels[this._registrationID].onRegistered(
-          "error: PushServer ChannelID already in use: " + msg.channelID);
-        registerNext();
+        consoleLog.error("PushHandler: received a 409 response from the PushServer: ",
+                         msg.channelID);
+        this.channels.get(this._pendingChannelID).onRegistered("409");
+        // Remove this channel from the channel list.
+        this.channels.delete(this._pendingChannelID);
+        this._registerNext();
         break;
 
       default:
-        let id = this._channelsToRegister.shift();
-        this.channels[this._registrationID].onRegistered(
-          "error: PushServer registration failure, status = " + msg.status);
-        registerNext();
+        consoleLog.error("PushHandler: received error ", msg.status,
+                         " from the PushServer: ", msg.channelID);
+        this.channels.get(this._pendingChannelID).onRegistered(msg.status);
+        this.channels.delete(this._pendingChannelID);
+        this._registerNext();
         break;
     }
   },
 
   /**
    * Attempts to open a websocket.
    *
    * A new websocket interface is used each time. If an onStop callback
    * was received, calling asyncOpen() on the same interface will
-   * trigger a "alreay open socket" exception even though the channel
+   * trigger an "already open socket" exception even though the channel
    * is logically closed.
    */
   _openSocket: function() {
-    this._isConnected = false;
-
-    if (this._mockWebSocket) {
-      // For tests, use the mock instance.
-      this._websocket = this._mockWebSocket;
-    } else {
-      this._websocket = Cc["@mozilla.org/network/protocol;1?name=wss"]
-                        .createInstance(Ci.nsIWebSocketChannel);
-    }
-
-    this._websocket.protocol = "push-notification";
+    this.connectionState = CONNECTION_STATE_CONNECTING;
+    // For tests, use the mock instance.
+    this._pushSocket = new PushSocket(this._mockWebSocket);
 
     let performOpen = () => {
-      let uri = Services.io.newURI(this.pushServerUri, null, null);
-      this._websocket.asyncOpen(uri, this.pushServerUri, this, null);
+      consoleLog.info("PushHandler: attempt to open websocket to PushServer: ", this.pushServerUri);
+      this._pushSocket.connect(this.pushServerUri,
+                               (aMsg) => this._onMsg(aMsg),
+                               () => this._onStart(),
+                               (aCode, aReason) => this._onClose(aCode, aReason));
     }
 
     let pushServerURLFetchError = () => {
-      console.warn("MozLoopPushHandler - Could not retrieve push server URL from Loop server, will retry");
-      this._retryOperation(() => this._openSocket());
+      consoleLog.warn("PushHandler: Could not retrieve push server URL from Loop server, will retry");
+      this._pushSocket = undefined;
+      this._retryManager.retry(() => this._openSocket());
       return;
     }
 
+    try {
+      this.pushServerUri = Services.prefs.getCharPref("loop.debug.pushserver");
+    }
+    catch (e) {}
+
     if (!this.pushServerUri) {
       // Get push server to use from the Loop server
       let pushUrlEndpoint = Services.prefs.getCharPref("loop.server") + "/push-server-config";
       let req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
-                createInstance(Ci.nsIXMLHttpRequest);
+                  createInstance(Ci.nsIXMLHttpRequest);
       req.open("GET", pushUrlEndpoint);
       req.onload = () => {
         if (req.status >= 200 && req.status < 300) {
           let pushServerConfig;
           try {
             pushServerConfig = JSON.parse(req.responseText);
           } catch (e) {
-            console.warn("MozLoopPushHandler - Error parsing JSON response for push server URL");
+            consoleLog.warn("PushHandler: Error parsing JSON response for push server URL");
             pushServerURLFetchError();
           }
           if (pushServerConfig.pushServerURI) {
+            this._retryManager.reset();
             this.pushServerUri = pushServerConfig.pushServerURI;
-            this._retryEnd();
             performOpen();
           } else {
-            console.warn("MozLoopPushHandler - push server URL config lacks pushServerURI parameter");
+            consoleLog.warn("PushHandler: push server URL config lacks pushServerURI parameter");
             pushServerURLFetchError();
           }
         } else {
-          console.warn("MozLoopPushHandler - push server URL retrieve error: " + req.status);
+          consoleLog.warn("PushHandler: push server URL retrieve error: " + req.status);
           pushServerURLFetchError();
         }
       };
       req.onerror = pushServerURLFetchError;
       req.send();
     } else {
       // this.pushServerUri already set -- just open the channel
       performOpen();
     }
   },
 
   /**
+    * Closes websocket and begins re-establishing a connection with the PushServer
+    */
+  _restartConnection: function() {
+    this._retryManager.reset();
+    this._pingMonitor.stop();
+    this.serviceState = SERVICE_STATE_OFFLINE;
+    this._pendingChannelID = null;
+
+    if (this.connectionState === CONNECTION_STATE_OPEN) {
+      // Close the current PushSocket and start the operation to open a new one.
+      this.connectionState = CONNECTION_STATE_CLOSED;
+      this._pushSocket.close();
+      consoleLog.warn("PushHandler: connection error: re-establishing connection to PushServer");
+      this._openSocket();
+    }
+  },
+
+  /**
    * Begins registering the channelIDs with the PushServer
    */
   _registerChannels: function() {
     // Hold off registration operation until handshake is complete.
-    if (!this._isConnected) {
+    // If a registration cycle is in progress, do nothing.
+    if (this.serviceState !== SERVICE_STATE_ACTIVE ||
+       this._pendingChannelID) {
       return;
     }
+    this._registerNext();
+  },
 
-    // If a registration is pending, do not generate a work list.
-    // Assume registration is in progress.
-    if (!this._registrationID) {
-      // Generate a list of channelIDs that have not yet been registered.
-      this._channelsToRegister = Object.keys(this.channels).filter((id) => {
-        return !(id in this.registeredChannels);
-      });
-      this._registrationID = this._channelsToRegister.shift();
-      this._sendRegistration(this._registrationID);
-    }
+  /**
+   * Gets the next channel to register from the worklist and kicks of its registration
+   */
+  _registerNext: function() {
+    this._pendingChannelID = this._channelsToRegister.pop();
+    this._sendRegistration(this._pendingChannelID);
   },
 
   /**
    * Handles registering a service
    *
    * @param {string} channelID - identification token to use in registration for this channel.
    */
   _sendRegistration: function(channelID) {
     if (channelID) {
-      try { // in case websocket has closed
-        this._websocket.sendMsg(JSON.stringify({messageType: "register",
-                                                channelID: channelID}));
-      }
-      catch (e) {console.warn("MozLoopPushHandler::_registerChannel websocket.sendMsg() failure");}
+      this._pushSocket.send({messageType: "register",
+                             channelID: channelID});
     }
   },
-
-  /**
-   * Method to handle retrying UserAgent to PushServer request following
-   * a retry back-off scheme managed by this function.
-   *
-   * @param {function} delayedOp Function to call after current delay is satisfied
-   *
-   * @param {number} [optional] retryDelay This parameter will be used as the initial delay
-   */
-  _retryOperation: function(delayedOp, retryDelay) {
-    if (!this._retryCount) {
-      this._retryDelay = retryDelay || this._minRetryDelay_ms;
-      this._retryCount = 1;
-    } else {
-      let nextDelay = this._retryDelay * 2;
-      this._retryDelay = nextDelay > this._maxRetryDelay_ms ? this._maxRetryDelay_ms : nextDelay;
-      this._retryCount += 1;
-    }
-    this._timeoutID = setTimeout(delayedOp, this._retryDelay);
-  },
-
-  /**
-   * Method used to reset the retry delay back-off logic.
-   *
-   */
-  _retryEnd: function() {
-    if (this._retryCount) {
-      clearTimeout(this._timeoutID);
-      this._retryCount = 0;
-    }
-  }
-};
+}
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -119,20 +119,20 @@ let gConversationWindowData = new Map();
  * Internal helper methods and state
  *
  * The registration is a two-part process. First we need to connect to
  * and register with the push server. Then we need to take the result of that
  * and register with the Loop server.
  */
 let MozLoopServiceInternal = {
   conversationContexts: new Map(),
+  pushURLs: new Map(),
 
   mocks: {
     pushHandler: undefined,
-    webSocket: undefined,
   },
 
   /**
    * The current deferreds for the registration processes. This is set if in progress
    * or the registration was successful. This is null if a registration attempt was
    * unsuccessful.
    */
   deferredRegistrations: new Map(),
@@ -320,111 +320,209 @@ let MozLoopServiceInternal = {
     }
   },
 
   get errors() {
     return gErrors;
   },
 
   /**
-   * Get endpoints with the push server and register for notifications.
-   * This should only be called from promiseRegisteredWithServers to prevent reentrancy.
+   * Create a notification channel between the LoopServer and this client
+   * via a PushServer. Once created, any subsequent changes in the pushURL
+   * assigned by the PushServer will be communicated to the LoopServer.
+   * with the Loop server. It will return early if already registered.
    *
+   * @param {String} channelID Unique identifier for the notification channel
+   *                 registered with the PushServer.
    * @param {LOOP_SESSION_TYPE} sessionType
-   * @return {Promise} resolves with all push endpoints
-   *                   rejects if any of the push registrations failed
+   * @param {String} serviceType Either 'calls' or 'rooms'.
+   * @param {Function} onNotification Callback function that will be associated
+   *                   with this channel from the PushServer.
+   * @returns {Promise} A promise that is resolved with no params on completion, or
+   *                    rejected with an error code or string.
    */
-  promiseRegisteredWithPushServer: function(sessionType) {
-    if (!this.deferredRegistrations.has(sessionType)) {
-      return Promise.reject(new Error("promiseRegisteredWithPushServer must be called while there is a " +
-                            "deferred in deferredRegistrations in order to prevent reentrancy"));
-    }
-    // Wrap push notification registration call-back in a Promise.
-    function registerForNotification(channelID, onNotification) {
-      log.debug("registerForNotification", channelID);
-      return new Promise((resolve, reject) => {
-        function onRegistered(error, pushUrl) {
-          log.debug("registerForNotification onRegistered:", error, pushUrl);
-          if (error) {
-            reject(Error(error));
-          } else {
-            resolve(pushUrl);
-          }
+  createNotificationChannel: function(channelID, sessionType, serviceType, onNotification) {
+    log.debug("createNotificationChannel", channelID, sessionType, serviceType);
+    // Wrap the push notification registration callback in a Promise.
+    return new Promise((resolve, reject) => {
+      let onRegistered = (error, pushURL, channelID) => {
+        log.debug("createNotificationChannel onRegistered:", error, pushURL, channelID);
+        if (error) {
+          reject(Error(error));
+        } else {
+          resolve(this.registerWithLoopServer(sessionType, serviceType, pushURL));
         }
+      }
 
-        // If we're already registered, resolve with the existing push URL
-        let pushURL = MozLoopServiceInternal.pushHandler.registeredChannels[channelID];
-        if (pushURL) {
-          log.debug("Using the existing push endpoint for channelID:", channelID);
-          resolve(pushURL);
-          return;
-        }
-
-        MozLoopServiceInternal.pushHandler.register(channelID, onRegistered, onNotification);
-      });
-    }
-
-    let options = this.mocks.webSocket ? { mockWebSocket: this.mocks.webSocket } : {};
-    this.pushHandler.initialize(options);
-
-    if (sessionType == LOOP_SESSION_TYPE.GUEST) {
-      let callsRegGuest = registerForNotification(MozLoopService.channelIDs.callsGuest,
-                                                  LoopCalls.onNotification);
-
-      let roomsRegGuest = registerForNotification(MozLoopService.channelIDs.roomsGuest,
-                                                  roomsPushNotification);
-      return Promise.all([callsRegGuest, roomsRegGuest]);
-    } else if (sessionType == LOOP_SESSION_TYPE.FXA) {
-      let callsRegFxA = registerForNotification(MozLoopService.channelIDs.callsFxA,
-                                                LoopCalls.onNotification);
-
-      let roomsRegFxA = registerForNotification(MozLoopService.channelIDs.roomsFxA,
-                                                roomsPushNotification);
-      return Promise.all([callsRegFxA, roomsRegFxA]);
-    }
-
-    return Promise.reject(new Error("promiseRegisteredWithPushServer: Invalid sessionType"));
+      this.pushHandler.register(channelID, onRegistered, onNotification);
+    });
   },
 
   /**
-   * Starts registration of Loop with the push server, and then will register
-   * with the Loop server. It will return early if already registered.
+   * Starts registration of Loop with the PushServer and the LoopServer.
+   * Successful PushServer registration will automatically trigger the registration
+   * of the PushURL returned by the PushServer with the LoopServer. If the registration
+   * chain has already been set up, this function will simply resolve.
    *
    * @param {LOOP_SESSION_TYPE} sessionType
    * @returns {Promise} a promise that is resolved with no params on completion, or
    *          rejected with an error code or string.
    */
   promiseRegisteredWithServers: function(sessionType = LOOP_SESSION_TYPE.GUEST) {
+    if (sessionType !== LOOP_SESSION_TYPE.GUEST && sessionType !== LOOP_SESSION_TYPE.FXA) {
+      return Promise.reject(new Error("promiseRegisteredWithServers: Invalid sessionType"));
+    }
+
     if (this.deferredRegistrations.has(sessionType)) {
-      log.debug("promiseRegisteredWithServers: registration already completed or in progress:", sessionType);
-      return this.deferredRegistrations.get(sessionType).promise;
+      log.debug("promiseRegisteredWithServers: registration already completed or in progress:",
+                sessionType);
+      return this.deferredRegistrations.get(sessionType);
     }
 
-    let result = null;
-    let deferred = Promise.defer();
-    log.debug("assigning to deferredRegistrations for sessionType:", sessionType);
-    this.deferredRegistrations.set(sessionType, deferred);
+    let options = this.mocks.webSocket ? { mockWebSocket: this.mocks.webSocket } : {};
+    this.pushHandler.initialize(options); // This can be called more than once.
 
-    // We grab the promise early in case one of the callers below delete it from the map.
-    result = deferred.promise;
+    let callsID = sessionType == LOOP_SESSION_TYPE.GUEST ?
+          MozLoopService.channelIDs.callsGuest :
+          MozLoopService.channelIDs.callsFxA,
+        roomsID = sessionType == LOOP_SESSION_TYPE.GUEST ?
+          MozLoopService.channelIDs.roomsGuest :
+          MozLoopService.channelIDs.roomsFxA;
 
-    this.promiseRegisteredWithPushServer(sessionType).then(() => {
-      return this.registerWithLoopServer(sessionType);
-    }).then(() => {
-      deferred.resolve("registered to status:" + sessionType);
-      // No need to clear the promise here, everything was good, so we don't need
-      // to re-register.
-    }, error => {
-      log.error("Failed to register with Loop server with sessionType " + sessionType, error);
-      deferred.reject(error);
+    let regPromise = this.createNotificationChannel(
+      callsID, sessionType, "calls", LoopCalls.onNotification).then(() => {
+        return this.createNotificationChannel(
+          roomsID, sessionType, "rooms", roomsPushNotification)});
+
+    log.debug("assigning to deferredRegistrations for sessionType:", sessionType);
+    this.deferredRegistrations.set(sessionType, regPromise);
+
+    // Do not return the new Promise generated by this catch() invocation.
+    // This will be called along with any other onReject function attached to regPromise.
+    regPromise.catch((error) => {
+      log.error("Failed to register with Loop server with sessionType ", sessionType, error);
       this.deferredRegistrations.delete(sessionType);
       log.debug("Cleared deferredRegistration for sessionType:", sessionType);
     });
 
-    return result;
+    return regPromise;
+  },
+
+  /**
+   * Registers with the Loop server either as a guest or a FxA user.
+   *
+   * @private
+   * @param {LOOP_SESSION_TYPE} sessionType The type of session e.g. guest or FxA
+   * @param {String} serviceType: "rooms" or "calls"
+   * @param {Boolean} [retry=true] Whether to retry if authentication fails.
+   * @return {Promise} resolves to pushURL or rejects with an Error
+   */
+  registerWithLoopServer: function(sessionType, serviceType, pushURL, retry = true) {
+    log.debug("registerWithLoopServer with sessionType:", sessionType, serviceType, retry);
+    if (!pushURL || !sessionType || !serviceType) {
+      return Promise.reject(new Error("Invalid or missing parameters for registerWithLoopServer"));
+    }
+
+    let pushURLs = this.pushURLs.get(sessionType);
+
+    // Create a blank URL record set if none exists for this sessionType.
+    if (!pushURLs) {
+      pushURLs = {calls: undefined, rooms: undefined};
+      this.pushURLs.set(sessionType, pushURLs);
+    }
+
+    if (pushURLs[serviceType] == pushURL) {
+      return Promise.resolve(pushURL);
+    }
+
+    let newURLs = {calls: pushURLs.calls,
+                   rooms: pushURLs.rooms};
+    newURLs[serviceType] = pushURL;
+
+    return this.hawkRequestInternal(sessionType, "/registration", "POST",
+                                    {simplePushURLs: newURLs}).then(
+      (response) => {
+        // If this failed we got an invalid token.
+        if (!this.storeSessionToken(sessionType, response.headers)) {
+          throw new Error("session-token-wrong-size");
+        }
+
+        // Record the new push URL
+        pushURLs[serviceType] = pushURL;
+        log.debug("Successfully registered with server for sessionType", sessionType);
+        this.clearError("registration");
+        return pushURL;
+      }, (error) => {
+        // There's other errors than invalid auth token, but we should only do the reset
+        // as a last resort.
+        if (error.code === 401) {
+          // Authorization failed, invalid token, we need to try again with a new token.
+          // XXX (pkerr) - Why is there a retry here? This will not clear up a hawk session
+          // token problem at this level.
+          if (retry) {
+            return this.registerWithLoopServer(sessionType, serviceType, pushURL, false);
+          }
+        }
+
+        log.error("Failed to register with the loop server. Error: ", error);
+        throw error;
+      }
+    );
+  },
+
+  /**
+   * Unregisters from the Loop server either as a guest or a FxA user.
+   *
+   * This is normally only wanted for FxA users as we normally want to keep the
+   * guest session with the device.
+   *
+   * NOTE: It is the responsibiliy of the caller the clear the session token
+   * after all of the notification classes: calls and rooms, for either
+   * Guest or FxA have been unregistered with the LoopServer.
+   *
+   * @param {LOOP_SESSION_TYPE} sessionType The type of session e.g. guest or FxA
+   * @return {Promise} resolving when the unregistration request finishes
+   */
+  unregisterFromLoopServer: function(sessionType) {
+    let prefType = Services.prefs.getPrefType(this.getSessionTokenPrefName(sessionType));
+    if (prefType == Services.prefs.PREF_INVALID) {
+      log.debug("already unregistered from LoopServer", sessionType);
+      return Promise.resolve("already unregistered");
+    }
+
+    let error,
+        pushURLs = this.pushURLs.get(sessionType),
+        callsPushURL = pushURLs ? pushURLs.calls : null,
+        roomsPushURL = pushURLs ? pushURLs.rooms : null;
+    this.pushURLs.delete(sessionType);
+
+    let unregister = (sessionType, pushURL) => {
+      if (!pushURL) {
+        return Promise.resolve("no pushURL of this type to unregister");
+      }
+
+      let unregisterURL = "/registration?simplePushURL=" + encodeURIComponent(pushURL);
+      return this.hawkRequestInternal(sessionType, unregisterURL, "DELETE").then(
+        () => {
+          log.debug("Successfully unregistered from server for sessionType = ", sessionType);
+          return "unregistered sessionType " + sessionType;
+        },
+        error => {
+          if (error.code === 401) {
+            // Authorization failed, invalid token. This is fine since it may mean we already logged out.
+            log.debug("already unregistered - invalid token", sessionType);
+            return "already unregistered, sessionType = " + sessionType;
+          }
+
+          log.error("Failed to unregister with the loop server. Error: ", error);
+          throw error;
+        });
+    }
+
+    return Promise.all([unregister(sessionType, callsPushURL), unregister(sessionType, roomsPushURL)]);
   },
 
   /**
    * Performs a hawk based request to the loop server - there is no pre-registration
    * for this request, if this is required, use hawkRequest.
    *
    * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
    *                                        This is one of the LOOP_SESSION_TYPE members.
@@ -435,16 +533,17 @@ let MozLoopServiceInternal = {
    * @param {Boolean} [retryOn401=true] Whether to retry if authentication fails.
    * @returns {Promise}
    *        Returns a promise that resolves to the response of the API call,
    *        or is rejected with an error.  If the server response can be parsed
    *        as JSON and contains an 'error' property, the promise will be
    *        rejected with this JSON-parsed response.
    */
   hawkRequestInternal: function(sessionType, path, method, payloadObj, retryOn401 = true) {
+    log.debug("hawkRequestInternal: ", sessionType, path, method);
     if (!gHawkClient) {
       gHawkClient = new HawkClient(this.loopServerUri);
     }
 
     let sessionToken, credentials;
     try {
       sessionToken = Services.prefs.getCharPref(this.getSessionTokenPrefName(sessionType));
     } catch (x) {
@@ -468,25 +567,27 @@ let MozLoopServiceInternal = {
           newPayloadObj[property] = payloadObj[property];
         }
       };
       payloadObj = newPayloadObj;
     }
 
     let handle401Error = (error) => {
       if (sessionType === LOOP_SESSION_TYPE.FXA) {
-        MozLoopService.logOutFromFxA().then(() => {
+        return MozLoopService.logOutFromFxA().then(() => {
           // Set a user-visible error after logOutFromFxA clears existing ones.
           this.setError("login", error);
+          throw error;
         });
       } else if (this.urlExpiryTimeIsInFuture()) {
         // If there are no Guest URLs in the future, don't use setError to notify the user since
         // there isn't a need for a Guest registration at this time.
         this.setError("registration", error);
       }
+      throw error;
     };
 
     return gHawkClient.request(path, method, credentials, payloadObj).then(
       (result) => {
         this.clearError("network");
         return result;
       },
       (error) => {
@@ -494,22 +595,21 @@ let MozLoopServiceInternal = {
         this.clearSessionToken(sessionType);
         if (retryOn401 && sessionType === LOOP_SESSION_TYPE.GUEST) {
           log.info("401 and INVALID_AUTH_TOKEN - retry registration");
           return this.registerWithLoopServer(sessionType, false).then(
             () => {
               return this.hawkRequestInternal(sessionType, path, method, payloadObj, false);
             },
             () => {
-              handle401Error(error); //Process the original error that triggered the retry.
-              throw error;
+              return handle401Error(error); //Process the original error that triggered the retry.
             }
           );
         }
-        handle401Error(error);
+        return handle401Error(error);
       }
       throw error;
     });
   },
 
   /**
    * Performs a hawk based request to the loop server, registering if necessary.
    *
@@ -554,17 +654,16 @@ let MozLoopServiceInternal = {
       case LOOP_SESSION_TYPE.GUEST:
         suffix = "";
         break;
       case LOOP_SESSION_TYPE.FXA:
         suffix = ".fxa";
         break;
       default:
         throw new Error("Unknown LOOP_SESSION_TYPE");
-        break;
     }
     return "loop.hawk-session-token" + suffix;
   },
 
   /**
    * Used to store a session token from a request if it exists in the headers.
    *
    * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
@@ -599,122 +698,16 @@ let MozLoopServiceInternal = {
    *                                        One of the LOOP_SESSION_TYPE members.
    */
   clearSessionToken: function(sessionType) {
     Services.prefs.clearUserPref(this.getSessionTokenPrefName(sessionType));
     log.debug("Cleared hawk session token for sessionType", sessionType);
   },
 
   /**
-   * Registers with the Loop server either as a guest or a FxA user. This method should only be
-   * called by promiseRegisteredWithServers since it prevents calling this while a registration is
-   * already in progress.
-   *
-   * @private
-   * @param {LOOP_SESSION_TYPE} sessionType The type of session e.g. guest or FxA
-   * @param {Boolean} [retry=true] Whether to retry if authentication fails.
-   * @return {Promise}
-   */
-  registerWithLoopServer: function(sessionType, retry = true) {
-    log.debug("registerWithLoopServer with sessionType:", sessionType);
-
-    let callsPushURL, roomsPushURL;
-    if (sessionType == LOOP_SESSION_TYPE.FXA) {
-      callsPushURL = this.pushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA];
-      roomsPushURL = this.pushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA];
-    } else if (sessionType == LOOP_SESSION_TYPE.GUEST) {
-      callsPushURL = this.pushHandler.registeredChannels[MozLoopService.channelIDs.callsGuest];
-      roomsPushURL = this.pushHandler.registeredChannels[MozLoopService.channelIDs.roomsGuest];
-    }
-
-    if (!callsPushURL || !roomsPushURL) {
-      return Promise.reject(new Error("Invalid sessionType or missing push URLs for registerWithLoopServer: " + sessionType));
-    }
-
-    // create a registration payload with a backwards compatible attribute (simplePushURL)
-    // that will register only the calls notification.
-    let msg = {
-        simplePushURL: callsPushURL,
-        simplePushURLs: {
-          calls: callsPushURL,
-          rooms: roomsPushURL,
-        },
-    };
-    return this.hawkRequestInternal(sessionType, "/registration", "POST", msg, false)
-      .then((response) => {
-        // If this failed we got an invalid token.
-        if (!this.storeSessionToken(sessionType, response.headers)) {
-          return Promise.reject(new Error("session-token-wrong-size"));
-        }
-
-        log.debug("Successfully registered with server for sessionType", sessionType);
-        this.clearError("registration");
-        return undefined;
-      }, (error) => {
-        // There's other errors than invalid auth token, but we should only do the reset
-        // as a last resort.
-        if (error.code === 401) {
-          // Authorization failed, invalid token, we need to try again with a new token.
-          if (retry) {
-            return this.registerWithLoopServer(sessionType, false);
-          }
-        }
-
-        log.error("Failed to register with the loop server. Error: ", error);
-        let deferred = Promise.defer();
-        deferred.promise.then(() => {
-          log.debug("registration retry succeeded");
-        },
-        error => {
-          log.debug("registration retry failed");
-        });
-        this.setError("registration", error, () => MozLoopService.delayedInitialize(deferred));
-        throw error;
-      }
-    );
-  },
-
-  /**
-   * Unregisters from the Loop server either as a guest or a FxA user.
-   *
-   * This is normally only wanted for FxA users as we normally want to keep the
-   * guest session with the device.
-   *
-   * NOTE: It is the responsibiliy of the caller the clear the session token
-   * after all of the notification classes: calls and rooms, for either
-   * Guest or FxA have been unregistered with the LoopServer.
-   *
-   * @param {LOOP_SESSION_TYPE} sessionType The type of session e.g. guest or FxA
-   * @param {String} pushURL The push URL previously given by the push server.
-   *                         This may not be necessary to unregister in the future.
-   * @return {Promise} resolving when the unregistration request finishes
-   */
-  unregisterFromLoopServer: function(sessionType, pushURL) {
-    let prefType = Services.prefs.getPrefType(this.getSessionTokenPrefName(sessionType));
-    if (prefType == Services.prefs.PREF_INVALID) {
-      return Promise.resolve("already unregistered");
-    }
-
-    let unregisterURL = "/registration?simplePushURL=" + encodeURIComponent(pushURL);
-    return this.hawkRequestInternal(sessionType, unregisterURL, "DELETE")
-      .then(() => {
-        log.debug("Successfully unregistered from server for sessionType", sessionType);
-      },
-      error => {
-        if (error.code === 401) {
-          // Authorization failed, invalid token. This is fine since it may mean we already logged out.
-          return;
-        }
-
-        log.error("Failed to unregister with the loop server. Error: ", error);
-        throw error;
-      });
-  },
-
-  /**
    * A getter to obtain and store the strings for loop. This is structured
    * for use by l10n.js.
    *
    * @returns {Map} a map of element ids with localized string values
    */
   get localizedStrings() {
     if (gLocalizedStrings.size)
       return gLocalizedStrings;
@@ -1286,31 +1279,16 @@ this.MozLoopService = {
   /**
    * Returns a new GUID (UUID) in curly braces format.
    */
   generateUUID: function() {
     return uuidgen.generateUUID().toString();
   },
 
   /**
-   * Returns a new non-global id
-   *
-   * @param {Function} notUnique [optional] This function will be
-   *                   applied to test the generated id for uniqueness
-   *                   in the callers domain.
-   */
-  generateLocalID: function(notUnique = ((id) => {return false})) {
-    do {
-      var id = Date.now().toString(36) + Math.floor((Math.random() * 4096)).toString(16);
-    }
-    while (notUnique(id));
-    return id;
-  },
-
-  /**
    * Retrieves MozLoopService "do not disturb" value.
    *
    * @return {Boolean}
    */
   get doNotDisturb() {
     return MozLoopServiceInternal.doNotDisturb;
   },
 
@@ -1489,34 +1467,31 @@ this.MozLoopService = {
    * Logs the user out from FxA.
    *
    * Gracefully handles if the user is already logged out.
    *
    * @return {Promise} that resolves when the FxA logout flow is complete.
    */
   logOutFromFxA: Task.async(function*() {
     log.debug("logOutFromFxA");
-    let pushHandler = MozLoopServiceInternal.pushHandler;
-    let callsPushUrl = pushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA];
-    let roomsPushUrl = pushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA];
     try {
-      if (callsPushUrl) {
-        yield MozLoopServiceInternal.unregisterFromLoopServer(LOOP_SESSION_TYPE.FXA, callsPushUrl);
-      }
-      if (roomsPushUrl) {
-        yield MozLoopServiceInternal.unregisterFromLoopServer(LOOP_SESSION_TYPE.FXA, roomsPushUrl);
-      }
-    } catch (error) {
-      throw error;
-    } finally {
+      yield MozLoopServiceInternal.unregisterFromLoopServer(LOOP_SESSION_TYPE.FXA);
+    }
+    catch (err) {
+      throw err
+    }
+    finally {
       MozLoopServiceInternal.clearSessionToken(LOOP_SESSION_TYPE.FXA);
-
       MozLoopServiceInternal.fxAOAuthTokenData = null;
       MozLoopServiceInternal.fxAOAuthProfile = null;
       MozLoopServiceInternal.deferredRegistrations.delete(LOOP_SESSION_TYPE.FXA);
+      // Unregister with PushHandler so these push channels will not get re-registered
+      // if the connection is re-established by the PushHandler.
+      MozLoopServiceInternal.pushHandler.unregister(MozLoopService.channelIDs.callsFxA);
+      MozLoopServiceInternal.pushHandler.unregister(MozLoopService.channelIDs.roomsFxA);
 
       // Reset the client since the initial promiseFxAOAuthParameters() call is
       // what creates a new session.
       gFxAOAuthClient = null;
       gFxAOAuthClientPromise = null;
 
       // clearError calls notifyStatusChanged so should be done last when the
       // state is clean.
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -766,17 +766,17 @@ loop.conversationViews = (function(mozL1
       return React.createElement("p", {className: "error"}, mozL10n.get("unable_retrieve_url"));
     },
 
     _getTitleMessage: function() {
       var callStateReason =
         this.props.store.getStoreState("callStateReason");
 
       if (callStateReason === "reject" || callStateReason === "busy" ||
-          callStateReason === "setup") {
+          callStateReason === "user-unknown") {
         var contactDisplayName = _getContactDisplayName(this.props.contact);
         if (contactDisplayName.length) {
           return mozL10n.get(
             "contact_unavailable_title",
             {"contactName": contactDisplayName});
         }
 
         return mozL10n.get("generic_contact_unavailable_title");
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -766,17 +766,17 @@ loop.conversationViews = (function(mozL1
       return <p className="error">{mozL10n.get("unable_retrieve_url")}</p>;
     },
 
     _getTitleMessage: function() {
       var callStateReason =
         this.props.store.getStoreState("callStateReason");
 
       if (callStateReason === "reject" || callStateReason === "busy" ||
-          callStateReason === "setup") {
+          callStateReason === "user-unknown") {
         var contactDisplayName = _getContactDisplayName(this.props.contact);
         if (contactDisplayName.length) {
           return mozL10n.get(
             "contact_unavailable_title",
             {"contactName": contactDisplayName});
         }
 
         return mozL10n.get("generic_contact_unavailable_title");
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -274,16 +274,18 @@ loop.store.ActiveRoomStore = (function()
       this.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
     },
 
     /**
      * Handles the action that signifies when media permission has been
      * granted and starts joining the room.
      */
     gotMediaPermission: function() {
+      this.setStoreState({roomState: ROOM_STATES.JOINING});
+
       this._mozLoop.rooms.join(this._storeState.roomToken,
         function(error, responseData) {
           if (error) {
             this.dispatchAction(new sharedActions.RoomFailure({error: error}));
             return;
           }
 
           this.dispatchAction(new sharedActions.JoinedRoom({
@@ -456,17 +458,18 @@ loop.store.ActiveRoomStore = (function()
 
       this._sdkDriver.disconnectSession();
 
       if (this._timeout) {
         clearTimeout(this._timeout);
         delete this._timeout;
       }
 
-      if (this._storeState.roomState === ROOM_STATES.JOINED ||
+      if (this._storeState.roomState === ROOM_STATES.JOINING ||
+          this._storeState.roomState === ROOM_STATES.JOINED ||
           this._storeState.roomState === ROOM_STATES.SESSION_CONNECTED ||
           this._storeState.roomState === ROOM_STATES.HAS_PARTICIPANTS) {
         this._mozLoop.rooms.leave(this._storeState.roomToken,
           this._storeState.sessionToken);
       }
 
       this.setStoreState({roomState: nextState || ROOM_STATES.ENDED});
     },
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -369,18 +369,22 @@ loop.store = loop.store || {};
       appendContactValues("email");
       appendContactValues("tel", true);
 
       this.client.setupOutgoingCall(contactAddresses,
         this.getStoreState("callType"),
         function(err, result) {
           if (err) {
             console.error("Failed to get outgoing call data", err);
+            var failureReason = "setup";
+            if (err.errno == 122) {
+              failureReason = "user-unknown";
+            }
             this.dispatcher.dispatch(
-              new sharedActions.ConnectionFailure({reason: "setup"}));
+              new sharedActions.ConnectionFailure({reason: failureReason}));
             return;
           }
 
           // Success, dispatch a new action.
           this.dispatcher.dispatch(
             new sharedActions.ConnectCall({sessionData: result}));
         }.bind(this)
       );
--- a/browser/components/loop/content/shared/js/feedbackViews.js
+++ b/browser/components/loop/content/shared/js/feedbackViews.js
@@ -70,17 +70,17 @@ loop.shared.views.FeedbackView = (functi
       return {pending: false};
     },
 
     _getCategories: function() {
       return {
         audio_quality: l10n.get("feedback_category_audio_quality"),
         video_quality: l10n.get("feedback_category_video_quality"),
         disconnected : l10n.get("feedback_category_was_disconnected"),
-        confusing:     l10n.get("feedback_category_confusing"),
+        confusing:     l10n.get("feedback_category_confusing2"),
         other:         l10n.get("feedback_category_other2")
       };
     },
 
     _getCategoryFields: function() {
       var categories = this._getCategories();
       return Object.keys(categories).map(function(category, key) {
         return (
@@ -137,17 +137,17 @@ loop.shared.views.FeedbackView = (functi
         happy: false,
         category: this.state.category,
         description: this.state.description
       }));
     },
 
     render: function() {
       return (
-        React.createElement(FeedbackLayout, {title: l10n.get("feedback_what_makes_you_sad"), 
+        React.createElement(FeedbackLayout, {title: l10n.get("feedback_category_list_heading"), 
                         reset: this.props.reset}, 
           React.createElement("form", {onSubmit: this.handleFormSubmit}, 
             this._getCategoryFields(), 
             React.createElement("p", null, 
               React.createElement("input", {type: "text", ref: "description", name: "description", 
                 className: "feedback-description", 
                 onChange: this.handleDescriptionFieldChange, 
                 value: this.state.description, 
--- a/browser/components/loop/content/shared/js/feedbackViews.jsx
+++ b/browser/components/loop/content/shared/js/feedbackViews.jsx
@@ -70,17 +70,17 @@ loop.shared.views.FeedbackView = (functi
       return {pending: false};
     },
 
     _getCategories: function() {
       return {
         audio_quality: l10n.get("feedback_category_audio_quality"),
         video_quality: l10n.get("feedback_category_video_quality"),
         disconnected : l10n.get("feedback_category_was_disconnected"),
-        confusing:     l10n.get("feedback_category_confusing"),
+        confusing:     l10n.get("feedback_category_confusing2"),
         other:         l10n.get("feedback_category_other2")
       };
     },
 
     _getCategoryFields: function() {
       var categories = this._getCategories();
       return Object.keys(categories).map(function(category, key) {
         return (
@@ -137,17 +137,17 @@ loop.shared.views.FeedbackView = (functi
         happy: false,
         category: this.state.category,
         description: this.state.description
       }));
     },
 
     render: function() {
       return (
-        <FeedbackLayout title={l10n.get("feedback_what_makes_you_sad")}
+        <FeedbackLayout title={l10n.get("feedback_category_list_heading")}
                         reset={this.props.reset}>
           <form onSubmit={this.handleFormSubmit}>
             {this._getCategoryFields()}
             <p>
               <input type="text" ref="description" name="description"
                 className="feedback-description"
                 onChange={this.handleDescriptionFieldChange}
                 value={this.state.description}
--- a/browser/components/loop/content/shared/js/roomStates.js
+++ b/browser/components/loop/content/shared/js/roomStates.js
@@ -11,16 +11,18 @@ loop.store.ROOM_STATES = {
     // The initial state of the room
     INIT: "room-init",
     // The store is gathering the room data
     GATHER: "room-gather",
     // The store has got the room data
     READY: "room-ready",
     // Obtaining media from the user
     MEDIA_WAIT: "room-media-wait",
+    // Joining the room is taking place
+    JOINING: "room-joining",
     // The room is known to be joined on the loop-server
     JOINED: "room-joined",
     // The room is connected to the sdk server.
     SESSION_CONNECTED: "room-session-connected",
     // There are participants in the room.
     HAS_PARTICIPANTS: "room-has-participants",
     // There was an issue with the room
     FAILED: "room-failed",
--- a/browser/components/loop/content/shared/js/websocket.js
+++ b/browser/components/loop/content/shared/js/websocket.js
@@ -82,17 +82,19 @@ loop.CallConnectionWebSocket = (function
 
     /**
      * Closes the websocket. This shouldn't be the normal action as the server
      * will normally close the socket. Only in bad error cases, or where we need
      * to close the socket just before closing the window (to avoid an error)
      * should we call this.
      */
     close: function() {
-      this.socket.close();
+      if (this.socket) {
+        this.socket.close();
+      }
     },
 
     _clearConnectionFlags: function() {
       clearTimeout(this.connectDetails.timeout);
       delete this.connectDetails;
     },
 
     /**
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -89,16 +89,17 @@ loop.standaloneRoomViews = (function(moz
           return (
             React.createElement("div", {className: "room-inner-info-area"}, 
               React.createElement("p", {className: "prompt-media-message"}, 
                 msg
               )
             )
           );
         }
+        case ROOM_STATES.JOINING:
         case ROOM_STATES.JOINED:
         case ROOM_STATES.SESSION_CONNECTED: {
           return (
             React.createElement("div", {className: "room-inner-info-area"}, 
               React.createElement("p", {className: "empty-room-message"}, 
                 mozL10n.get("rooms_only_occupant_label")
               )
             )
@@ -289,17 +290,17 @@ loop.standaloneRoomViews = (function(moz
       document.body.classList.add("is-standalone-room");
     },
 
     componentWillUnmount: function() {
       this.stopListening(this.props.activeRoomStore);
     },
 
     /**
-     * Watches for when we transition to JOINED room state, so we can request
+     * Watches for when we transition to MEDIA_WAIT room state, so we can request
      * user media access.
      *
      * @param  {Object} nextProps (Unused)
      * @param  {Object} nextState Next state object.
      */
     componentWillUpdate: function(nextProps, nextState) {
       if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
           nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -89,16 +89,17 @@ loop.standaloneRoomViews = (function(moz
           return (
             <div className="room-inner-info-area">
               <p className="prompt-media-message">
                 {msg}
               </p>
             </div>
           );
         }
+        case ROOM_STATES.JOINING:
         case ROOM_STATES.JOINED:
         case ROOM_STATES.SESSION_CONNECTED: {
           return (
             <div className="room-inner-info-area">
               <p className="empty-room-message">
                 {mozL10n.get("rooms_only_occupant_label")}
               </p>
             </div>
@@ -289,17 +290,17 @@ loop.standaloneRoomViews = (function(moz
       document.body.classList.add("is-standalone-room");
     },
 
     componentWillUnmount: function() {
       this.stopListening(this.props.activeRoomStore);
     },
 
     /**
-     * Watches for when we transition to JOINED room state, so we can request
+     * Watches for when we transition to MEDIA_WAIT room state, so we can request
      * user media access.
      *
      * @param  {Object} nextProps (Unused)
      * @param  {Object} nextState Next state object.
      */
     componentWillUpdate: function(nextProps, nextState) {
       if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
           nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
--- a/browser/components/loop/standalone/content/l10n/en-US/loop.properties
+++ b/browser/components/loop/standalone/content/l10n/en-US/loop.properties
@@ -63,22 +63,22 @@ vendor_alttext={{vendorShortname}} logo
 call_url_creation_date_label=(from {{call_url_creation_date}})
 call_progress_getting_media_description={{clientShortname}} requires access to your camera and microphone.
 call_progress_getting_media_title=Waiting for media…
 call_progress_connecting_description=Connecting…
 call_progress_ringing_description=Ringing…
 fxos_app_needed=Please install the {{fxosAppName}} app from the Firefox Marketplace.
 
 feedback_call_experience_heading2=How was your conversation?
-feedback_what_makes_you_sad=What makes you sad?
 feedback_thank_you_heading=Thank you for your feedback!
+feedback_category_list_heading=What made you sad?
 feedback_category_audio_quality=Audio quality
 feedback_category_video_quality=Video quality
 feedback_category_was_disconnected=Was disconnected
-feedback_category_confusing=Confusing
+feedback_category_confusing2=Confusing controls
 feedback_category_other2=Other
 feedback_custom_category_text_placeholder=What went wrong?
 feedback_submit_button=Submit
 feedback_back_button=Back
 ## LOCALIZATION NOTE (feedback_window_will_close_in2):
 ## Gaia l10n format; see https://github.com/mozilla-b2g/gaia/blob/f108c706fae43cd61628babdd9463e7695b2496e/apps/email/locales/email.en-US.properties#L387
 ## In this item, don't translate the part between {{..}}
 feedback_window_will_close_in2={[ plural(countdown) ]}
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -439,23 +439,33 @@ describe("loop.conversationViews", funct
 
         view = mountTestComponent({contact: contact});
 
         sinon.assert.calledWithExactly(document.mozL10n.get,
           "contact_unavailable_title",
           {contactName: loop.conversationViews._getContactDisplayName(contact)});
       });
 
-    it("should show 'contact unavailable' when the reason is 'setup'",
+    it("should show 'something went wrong' when the reason is 'setup'",
       function () {
         store.setStoreState({callStateReason: "setup"});
 
         view = mountTestComponent({contact: contact});
 
         sinon.assert.calledWithExactly(document.mozL10n.get,
+          "generic_failure_title");
+      });
+
+    it("should show 'contact unavailable' when the reason is 'user-unknown'",
+      function () {
+        store.setStoreState({callStateReason: "user-unknown"});
+
+        view = mountTestComponent({contact: contact});
+
+        sinon.assert.calledWithExactly(document.mozL10n.get,
           "contact_unavailable_title",
           {contactName: loop.conversationViews._getContactDisplayName(contact)});
       });
 
     it("should display a generic contact unavailable msg when the reason is" +
        " 'busy' and no display name is available", function() {
         store.setStoreState({callStateReason: "busy"});
         var phoneOnlyContact = {
--- a/browser/components/loop/test/functional/test_1_browser_call.py
+++ b/browser/components/loop/test/functional/test_1_browser_call.py
@@ -48,17 +48,17 @@ class Test1BrowserCall(MarionetteTestCas
 
     def switch_to_panel(self):
         button = self.marionette.find_element(By.ID, "loop-button")
 
         # click the element
         button.click()
 
         # switch to the frame
-        frame = self.marionette.find_element(By.ID, "loop")
+        frame = self.marionette.find_element(By.ID, "loop-panel-iframe")
         self.marionette.switch_to_frame(frame)
 
     def load_and_verify_standalone_ui(self, url):
         self.marionette.set_context("content")
         self.marionette.navigate(url)
 
     def start_a_conversation(self):
         # TODO: wait for react elements
--- a/browser/components/loop/test/mochitest/browser_fxa_login.js
+++ b/browser/components/loop/test/mochitest/browser_fxa_login.js
@@ -19,17 +19,17 @@ function* checkFxA401() {
   ise(err.friendlyDetailsButtonLabel, getLoopString("retry_button"),
       "Check friendlyDetailsButtonLabel");
   let loopButton = document.getElementById("loop-button");
   is(loopButton.getAttribute("state"), "error",
      "state of loop button should be error after a 401 with login");
 
   let loopPanel = document.getElementById("loop-notification-panel");
   yield loadLoopPanel();
-  let loopDoc = document.getElementById("loop").contentDocument;
+  let loopDoc = document.getElementById("loop-panel-iframe").contentDocument;
   is(loopDoc.querySelector(".alert-error .message").textContent,
      getLoopString("could_not_authenticate"),
      "Check error bar message");
   is(loopDoc.querySelector(".details-error .details").textContent,
      getLoopString("password_changed_question"),
      "Check error bar details message");
   is(loopDoc.querySelector(".details-error .detailsButton").textContent,
      getLoopString("retry_button"),
@@ -37,26 +37,22 @@ function* checkFxA401() {
   loopPanel.hidePopup();
 }
 
 add_task(function* setup() {
   Services.prefs.setBoolPref("loop.gettingStarted.seen", true);
   MozLoopServiceInternal.mocks.pushHandler = mockPushHandler;
   // Normally the same pushUrl would be registered but we change it in the test
   // to be able to check for success on the second registration.
-  mockPushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA] = "https://localhost/pushUrl/fxa-calls";
-  mockPushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA] = "https://localhost/pushUrl/fxa-rooms";
 
   registerCleanupFunction(function* () {
     info("cleanup time");
     yield promiseDeletedOAuthParams(BASE_URL);
     Services.prefs.clearUserPref("loop.gettingStarted.seen");
     MozLoopServiceInternal.mocks.pushHandler = undefined;
-    delete mockPushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA];
-    delete mockPushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA];
 
     yield resetFxA();
     Services.prefs.clearUserPref(MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.GUEST));
   });
 });
 
 add_task(function* checkOAuthParams() {
   let params = {
@@ -164,17 +160,16 @@ add_task(function* invalidState() {
   yield loginPromise.catch((error) => {
     ok(error, "The login promise should be rejected due to invalid state");
   });
 });
 
 add_task(function* basicRegistrationWithoutSession() {
   yield resetFxA();
   yield promiseDeletedOAuthParams(BASE_URL);
-
   let caught = false;
   yield MozLoopServiceInternal.promiseFxAOAuthToken("code1", "state").catch((error) => {
     caught = true;
     is(error.code, 401, "Should have returned a 401");
   });
   ok(caught, "Should have caught the error requesting /token without a hawk session");
   yield checkFxA401();
 });
@@ -288,17 +283,17 @@ add_task(function* basicAuthorizationAnd
 
   info("registering");
   mockPushHandler.registrationPushURL = "https://localhost/pushUrl/guest";
   yield MozLoopService.promiseRegisteredWithServers();
 
   let statusChangedPromise = promiseObserverNotified("loop-status-changed");
   yield loadLoopPanel({stayOnline: true});
   yield statusChangedPromise;
-  let loopDoc = document.getElementById("loop").contentDocument;
+  let loopDoc = document.getElementById("loop-panel-iframe").contentDocument;
   let visibleEmail = loopDoc.getElementsByClassName("user-identity")[0];
   is(visibleEmail.textContent, "Guest", "Guest should be displayed on the panel when not logged in");
   is(MozLoopService.userProfile, null, "profile should be null before log-in");
   let loopButton = document.getElementById("loop-button");
   is(loopButton.getAttribute("state"), "", "state of loop button should be empty when not logged in");
 
   info("Login");
   let tokenData = yield MozLoopService.logInToFxA();
@@ -359,51 +354,44 @@ add_task(function* loginWithParams401() 
   });
 
   yield checkFxA401();
 });
 
 add_task(function* logoutWithIncorrectPushURL() {
   yield resetFxA();
   let pushURL = "http://www.example.com/";
-  mockPushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA] = pushURL;
-  mockPushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA] = pushURL;
-
   // Create a fake FxA hawk session token
   const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
   Services.prefs.setCharPref(fxASessionPref, "X".repeat(HAWK_TOKEN_LENGTH));
-
-  yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA);
+  yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, "calls", pushURL);
   let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
   ise(registrationResponse.response.simplePushURLs.calls, pushURL, "Check registered push URL");
-  mockPushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA] = "http://www.example.com/invalid";
+  MozLoopServiceInternal.pushURLs.get(LOOP_SESSION_TYPE.FXA).calls = "http://www.example.com/invalid";
   let caught = false;
   yield MozLoopService.logOutFromFxA().catch((error) => {
     caught = true;
   });
   ok(caught, "Should have caught an error logging out with a mismatched push URL");
   checkLoggedOutState();
   registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
   ise(registrationResponse.response.simplePushURLs.calls, pushURL, "Check registered push URL wasn't deleted");
 });
 
 add_task(function* logoutWithNoPushURL() {
   yield resetFxA();
   let pushURL = "http://www.example.com/";
-  mockPushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA] = pushURL;
-
   // Create a fake FxA hawk session token
   const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
   Services.prefs.setCharPref(fxASessionPref, "X".repeat(HAWK_TOKEN_LENGTH));
 
-  yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA);
+  yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, "calls", pushURL);
   let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
   ise(registrationResponse.response.simplePushURLs.calls, pushURL, "Check registered push URL");
-  mockPushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA] = null;
-  mockPushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA] = null;
+  MozLoopServiceInternal.pushURLs.delete(LOOP_SESSION_TYPE.FXA);
   yield MozLoopService.logOutFromFxA();
   checkLoggedOutState();
   registrationResponse = yield promiseOAuthGetRegistration(BASE_URL);
   ise(registrationResponse.response.simplePushURLs.calls, pushURL, "Check registered push URL wasn't deleted");
 });
 
 add_task(function* loginWithRegistration401() {
   yield resetFxA();
--- a/browser/components/loop/test/mochitest/head.js
+++ b/browser/components/loop/test/mochitest/head.js
@@ -9,16 +9,17 @@ const {
 const {LoopCalls} = Cu.import("resource:///modules/loop/LoopCalls.jsm", {});
 const {LoopRooms} = Cu.import("resource:///modules/loop/LoopRooms.jsm", {});
 
 // Cache this value only once, at the beginning of a
 // test run, so that it doesn't pick up the offline=true
 // if offline mode is requested multiple times in a test run.
 const WAS_OFFLINE = Services.io.offline;
 
+
 var gMozLoopAPI;
 
 function promiseGetMozLoopAPI() {
   return new Promise((resolve, reject) => {
     let loopPanel = document.getElementById("loop-notification-panel");
     let btn = document.getElementById("loop-button");
 
     // Wait for the popup to be shown if it's not already, then we can get the iframe and
@@ -109,17 +110,16 @@ function promiseOAuthParamsSetup(baseURL
   });
 }
 
 function* resetFxA() {
   let global = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
   global.gHawkClient = null;
   global.gFxAOAuthClientPromise = null;
   global.gFxAOAuthClient = null;
-  MozLoopServiceInternal.deferredRegistrations.delete(LOOP_SESSION_TYPE.FXA);
   MozLoopServiceInternal.fxAOAuthProfile = null;
   MozLoopServiceInternal.fxAOAuthTokenData = null;
   const fxASessionPref = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA);
   Services.prefs.clearUserPref(fxASessionPref);
   MozLoopService.errors.clear();
   let notified = promiseObserverNotified("loop-status-changed");
   MozLoopServiceInternal.notifyStatusChanged();
   yield notified;
@@ -184,33 +184,45 @@ function getLoopString(stringID) {
  * This is used to fake push registration and notifications for
  * MozLoopService tests. There is only one object created per test instance, as
  * once registration has taken place, the object cannot currently be changed.
  */
 let mockPushHandler = {
   // This sets the registration result to be returned when initialize
   // is called. By default, it is equivalent to success.
   registrationResult: null,
-  registrationPushURL: null,
+  registrationPushURLs: {},
   notificationCallback: {},
   registeredChannels: {},
 
   /**
    * MozLoopPushHandler API
    */
   initialize: function(options = {}) {
     if ("mockWebSocket" in options) {
       this._mockWebSocket = options.mockWebSocket;
     }
+    this.registrationPushURLs[MozLoopService.channelIDs.callsGuest] =
+      "https://localhost/pushUrl/guest-calls";
+    this.registrationPushURLs[MozLoopService.channelIDs.roomsGuest] =
+      "https://localhost/pushUrl/guest-rooms";
+    this.registrationPushURLs[MozLoopService.channelIDs.callsFxA] =
+      "https://localhost/pushUrl/fxa-calls";
+    this.registrationPushURLs[MozLoopService.channelIDs.roomsFxA] =
+      "https://localhost/pushUrl/fxa-rooms";
   },
 
   register: function(channelId, registerCallback, notificationCallback) {
     this.notificationCallback[channelId] = notificationCallback;
-    this.registeredChannels[channelId] = this.registrationPushURL;
-    setTimeout(registerCallback(this.registrationResult, this.registrationPushURL, channelId), 0);
+    this.registeredChannels[channelId] = this.registrationPushURLs[channelId];
+    setTimeout(registerCallback(this.registrationResult, this.registeredChannels[channelId], channelId), 0);
+  },
+
+  unregister: function(channelID) {
+    return;
   },
 
   /**
    * Test-only API to simplify notifying a push notification result.
    */
   notify: function(version, chanId) {
     this.notificationCallback[chanId](version, chanId);
   }
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -350,16 +350,22 @@ describe("loop.store.ActiveRoomStore", f
     });
   });
 
   describe("#gotMediaPermission", function() {
     beforeEach(function() {
       store.setStoreState({roomToken: "tokenFake"});
     });
 
+    it("should set the room state to JOINING", function() {
+      store.gotMediaPermission();
+
+      expect(store.getStoreState().roomState).eql(ROOM_STATES.JOINING);
+    });
+
     it("should call rooms.join on mozLoop", function() {
       store.gotMediaPermission();
 
       sinon.assert.calledOnce(fakeMozLoop.rooms.join);
       sinon.assert.calledWith(fakeMozLoop.rooms.join, "tokenFake");
     });
 
     it("should dispatch `JoinedRoom` on success", function() {
@@ -672,16 +678,27 @@ describe("loop.store.ActiveRoomStore", f
     it("should call mozLoop.rooms.leave", function() {
       store.windowUnload();
 
       sinon.assert.calledOnce(fakeMozLoop.rooms.leave);
       sinon.assert.calledWithExactly(fakeMozLoop.rooms.leave,
         "fakeToken", "1627384950");
     });
 
+    it("should call mozLoop.rooms.leave if the room state is JOINING",
+      function() {
+        store.setStoreState({roomState: ROOM_STATES.JOINING});
+
+        store.windowUnload();
+
+        sinon.assert.calledOnce(fakeMozLoop.rooms.leave);
+        sinon.assert.calledWithExactly(fakeMozLoop.rooms.leave,
+          "fakeToken", "1627384950");
+      });
+
     it("should set the state to CLOSING", function() {
       store.windowUnload();
 
       expect(store._storeState.roomState).eql(ROOM_STATES.CLOSING);
     });
   });
 
   describe("#leaveRoom", function() {
--- a/browser/components/loop/test/xpcshell/head.js
+++ b/browser/components/loop/test/xpcshell/head.js
@@ -97,36 +97,47 @@ let mockPushHandler = {
   },
 
   register: function(channelId, registerCallback, notificationCallback) {
     this.notificationCallback[channelId] = notificationCallback;
     this.registeredChannels[channelId] = this.registrationPushURL;
     registerCallback(this.registrationResult, this.registrationPushURL, channelId);
   },
 
+  unregister: function(channelID) {
+    return;
+  },
+
   /**
    * Test-only API to simplify notifying a push notification result.
    */
   notify: function(version, chanId) {
     this.notificationCallback[chanId](version, chanId);
   }
 };
 
 /**
  * Mock nsIWebSocketChannel for tests. This mocks the WebSocketChannel, and
  * enables us to check parameters and return messages similar to the push
  * server.
  */
-let MockWebSocketChannel = function(options = {}) {
-  this.defaultMsgHandler = options.defaultMsgHandler;
-};
+function MockWebSocketChannel() {};
 
 MockWebSocketChannel.prototype = {
   QueryInterface: XPCOMUtils.generateQI(Ci.nsIWebSocketChannel),
 
+  initRegStatus: 0,
+
+  defaultMsgHandler: function(msg) {
+    // Treat as a ping
+    this.listener.onMessageAvailable(this.context,
+                                     JSON.stringify({}));
+    return;
+  },
+
   /**
    * nsIWebSocketChannel implementations.
    * See nsIWebSocketChannel.idl for API details.
    */
   asyncOpen: function(aURI, aOrigin, aListener, aContext) {
     this.uri = aURI;
     this.origin = aOrigin;
     this.listener = aListener;
@@ -157,16 +168,20 @@ MockWebSocketChannel.prototype = {
                           channelID: this.channelID,
                           pushEndpoint: kEndPointUrl}));
         break;
       default:
         this.defaultMsgHandler && this.defaultMsgHandler(message);
     }
   },
 
+  close: function(aCode, aReason) {
+    this.stop(aCode);
+  },
+
   notify: function(version) {
     this.listener.onMessageAvailable(this.context,
       JSON.stringify({
         messageType: "notification", updates: [{
           channelID: this.channelID,
           version: version
         }]
     }));
--- a/browser/components/loop/test/xpcshell/test_looppush_initialize.js
+++ b/browser/components/loop/test/xpcshell/test_looppush_initialize.js
@@ -7,17 +7,159 @@
 
   add_test(function test_initalize_offline() {
     Services.io.offline = true;
     do_check_false(MozLoopPushHandler.initialize());
     Services.io.offline = false;
     run_next_test();
   });
 
-  add_test(function test_initalize() {
+  add_test(function test_initalize_missing_chanid() {
+    Assert.throws(() => MozLoopPushHandler.register(null, dummyCallback, dummyCallback));
+    run_next_test();
+  });
+
+  add_test(function test_initalize_missing_regcallback() {
+    Assert.throws(() => MozLoopPushHandler.register("chan-1", null, dummyCallback));
+    run_next_test();
+  });
+
+  add_test(function test_initalize_missing_notifycallback() {
+    Assert.throws(() => MozLoopPushHandler.register("chan-1", dummyCallback, null));
+    run_next_test();
+  });
+
+  add_test(function test_initalize_websocket() {
+    do_check_true(MozLoopPushHandler.initialize({mockWebSocket: mockWebSocket}));
+    MozLoopPushHandler.register(
+      "chan-1",
+      function(err, url, id) {
+        Assert.equal(err, null, "err should be null to indicate success");
+        Assert.equal(url, kEndPointUrl, "Should return push server application URL");
+        Assert.equal(id, "chan-1", "Should have channel id = chan-1");
+        Assert.equal(mockWebSocket.uri.prePath, kServerPushUrl,
+                     "Should have the url from preferences");
+        Assert.equal(mockWebSocket.origin, kServerPushUrl,
+                     "Should have the origin url from preferences");
+        Assert.equal(mockWebSocket.protocol, "push-notification",
+                     "Should have the protocol set to push-notifications");
+        mockWebSocket.notify(15);
+      },
+      function(version, id) {
+        Assert.equal(version, 15, "Should have version number 15");
+        Assert.equal(id, "chan-1", "Should have channel id = chan-1");
+        run_next_test();
+      });
+  });
+
+  add_test(function test_register_twice_same_channel() {
+    MozLoopPushHandler.register(
+      "chan-2",
+      function(err, url, id) {
+        Assert.equal(err, null, "Should return null for success");
+        Assert.equal(url, kEndPointUrl, "Should return push server application URL");
+        Assert.equal(id, "chan-2", "Should have channel id = chan-2");
+        Assert.equal(mockWebSocket.uri.prePath, kServerPushUrl,
+                     "Should have the url from preferences");
+        Assert.equal(mockWebSocket.origin, kServerPushUrl,
+                     "Should have the origin url from preferences");
+        Assert.equal(mockWebSocket.protocol, "push-notification",
+                     "Should have the protocol set to push-notifications");
+
+        // Register again for the same channel
+        MozLoopPushHandler.register(
+          "chan-2",
+          function(err, url, id) {
+            Assert.equal(err, null, "Should return null for success");
+            Assert.equal(id, "chan-2", "Should have channel id = chan-2");
+            run_next_test();
+          },
+          dummyCallback
+        );
+      },
+      dummyCallback
+    );
+  });
+
+  // Test that the PushHander will re-connect after the near-end disconnect.
+  // The uaID is cleared to force re-registration of all notification channels.
+  add_test(function test_reconnect_websocket() {
+    MozLoopPushHandler.uaID = undefined;
+    mockWebSocket.stop();
+    // Previously registered onRegistration callbacks will fire and be checked (see above).
+  });
+
+  // Test that the PushHander will re-connect after the far-end disconnect.
+  // The uaID is cleared to force re-regsitration of all notification channels.
+  add_test(function test_reopen_websocket() {
+    MozLoopPushHandler.uaID = undefined;
+    MozLoopPushHandler.registeredChannels = {}; //Do this to force a new registration callback.
+    mockWebSocket.serverClose();
+    // Previously registered onRegistration callbacks will fire and be checked (see above).
+  });
+
+  // Force a re-registration cycle and have the PushServer return a 500.
+  // A retry should occur and the registration then complete.
+  add_test(function test_retry_registration() {
+    MozLoopPushHandler.uaID = undefined;
+    mockWebSocket.initRegStatus = 500;
+    mockWebSocket.stop();
+  });
+
+  add_test(function test_reconnect_no_registration() {
+    let regCnt = 0;
+    MozLoopPushHandler.shutdown();
+    MozLoopPushHandler.initialize({mockWebSocket: mockWebSocket});
+    MozLoopPushHandler.register(
+      "test-chan",
+      function(err, url, id) {
+        Assert.equal(++regCnt, 1, "onRegistered should only be called once");
+        Assert.equal(err, null, "err should be null to indicate success");
+        Assert.equal(url, kEndPointUrl, "Should return push server application URL");
+        Assert.equal(id, "test-chan", "Should have channel id = test-chan");
+        mockWebSocket.stop();
+        setTimeout(run_next_test(), 0);
+      },
+      dummyCallback
+    );
+  });
+
+  add_test(function test_ping_websocket() {
+    let pingReceived = false,
+        socketClosed = false;
+    mockWebSocket.defaultMsgHandler = (msg) => {
+      pingReceived = true;
+      // Do not send a ping response.
+    }
+    mockWebSocket.close = () => {
+      socketClosed = true;
+    }
+
+    MozLoopPushHandler.shutdown();
+    MozLoopPushHandler.initialize({mockWebSocket: mockWebSocket});
+    MozLoopPushHandler.register(
+      "test-chan",
+      function(err, url) {
+        Assert.equal(err, null, "err should be null to indicate success");
+        waitForCondition(() => pingReceived).then(() => {
+          waitForCondition(() => socketClosed).then(() => {
+            run_next_test();
+          }, () => {
+            do_throw("should have closed the websocket");
+          });
+        }, () => {
+          do_throw("should have sent ping");
+        });
+      },
+      dummyCallback
+    );
+  });
+
+  add_test(function test_retry_pushurl() {
+    MozLoopPushHandler.shutdown();
     loopServer.registerPathHandler("/push-server-config", (request, response) => {
       // The PushHandler should retry the request for the push-server-config for
       // each of these cases without throwing an error.
       let n = 0;
       switch (++pushServerRequestCount) {
       case ++n:
         // Non-200 response
         response.setStatusLine(null, 500, "Retry");
@@ -46,112 +188,35 @@
         run_next_test();
         break;
       }
     });
 
     do_check_true(MozLoopPushHandler.initialize({mockWebSocket: mockWebSocket}));
   });
 
-  add_test(function test_initalize_missing_chanid() {
-    Assert.throws(() => {MozLoopPushHandler.register(null, dummyCallback, dummyCallback)});
-    run_next_test();
-  });
-
-  add_test(function test_initalize_missing_regcallback() {
-    Assert.throws(() => {MozLoopPushHandler.register("chan-1", null, dummyCallback)});
-    run_next_test();
-  });
-
-  add_test(function test_initalize_missing_notifycallback() {
-    Assert.throws(() => {MozLoopPushHandler.register("chan-1", dummyCallback, null)});
-    run_next_test();
-  });
-
-  add_test(function test_initalize_websocket() {
-    MozLoopPushHandler.register(
-      "chan-1",
-      function(err, url, id) {
-        Assert.equal(err, null, "Should return null for success");
-        Assert.equal(url, kEndPointUrl, "Should return push server application URL");
-        Assert.equal(id, "chan-1", "Should have channel id = chan-1");
-        Assert.equal(mockWebSocket.uri.prePath, kServerPushUrl,
-                     "Should have the url from preferences");
-        Assert.equal(mockWebSocket.origin, kServerPushUrl,
-                     "Should have the origin url from preferences");
-        Assert.equal(mockWebSocket.protocol, "push-notification",
-                     "Should have the protocol set to push-notifications");
-        mockWebSocket.notify(15);
-      },
-      function(version, id) {
-        Assert.equal(version, 15, "Should have version number 15");
-        Assert.equal(id, "chan-1", "Should have channel id = chan-1");
-        run_next_test();
-      });
-  });
-
-  add_test(function test_register_twice_same_channel() {
-    MozLoopPushHandler.register(
-      "chan-2",
-      function(err, url, id) {
-        Assert.equal(err, null, "Should return null for success");
-        Assert.equal(url, kEndPointUrl, "Should return push server application URL");
-        Assert.equal(id, "chan-2", "Should have channel id = chan-2");
-        Assert.equal(mockWebSocket.uri.prePath, kServerPushUrl,
-                     "Should have the url from preferences");
-        Assert.equal(mockWebSocket.origin, kServerPushUrl,
-                     "Should have the origin url from preferences");
-        Assert.equal(mockWebSocket.protocol, "push-notification",
-                     "Should have the protocol set to push-notifications");
-
-        // Register again for the same channel
-        MozLoopPushHandler.register(
-          "chan-2",
-          function(err, url, id) {
-            Assert.notEqual(err, null, "Should have returned an error");
-            // Notify the first registration to make sure that still works.
-            mockWebSocket.notify(16);
-          },
-          function(version, id) {
-            Assert.ok(false, "The 2nd onNotification callback shouldn't be called");
-        });
-      },
-      function(version, id) {
-        Assert.equal(version, 16, "Should have version number 16");
-        Assert.equal(id, "chan-2", "Should have channel id = chan-2");
-        run_next_test();
-      });
-  });
-
-  add_test(function test_reconnect_websocket() {
-    MozLoopPushHandler.uaID = undefined;
-    MozLoopPushHandler.registeredChannels = {}; //Do this to force a new registration callback.
-    mockWebSocket.stop();
-  });
-
-  add_test(function test_reopen_websocket() {
-    MozLoopPushHandler.uaID = undefined;
-    MozLoopPushHandler.registeredChannels = {}; //Do this to force a new registration callback.
-    mockWebSocket.serverClose();
-  });
-
-  add_test(function test_retry_registration() {
-    MozLoopPushHandler.uaID = undefined;
-    MozLoopPushHandler.registeredChannels = {}; //Do this to force a new registration callback.
-    mockWebSocket.initRegStatus = 500;
-    mockWebSocket.stop();
-  });
-
   function run_test() {
     setupFakeLoopServer();
 
+    loopServer.registerPathHandler("/push-server-config", (request, response) => {
+      response.setStatusLine(null, 200, "OK");
+      response.write(JSON.stringify({pushServerURI: kServerPushUrl}));
+      response.processAsync();
+      response.finish();
+    });
+
+    Services.prefs.setCharPref("services.push.serverURL", kServerPushUrl);
     Services.prefs.setIntPref("loop.retry_delay.start", 10); // 10 ms
     Services.prefs.setIntPref("loop.retry_delay.limit", 20); // 20 ms
+    Services.prefs.setIntPref("loop.ping.interval", 50); // 50 ms
+    Services.prefs.setIntPref("loop.ping.timeout", 20); // 20 ms
 
     do_register_cleanup(function() {
+      Services.prefs.clearUserPref("services.push.serverULR");
       Services.prefs.clearUserPref("loop.retry_delay.start");
       Services.prefs.clearUserPref("loop.retry_delay.limit");
-      Services.prefs.setCharPref("loop.server", kLoopServerUrl);
+      Services.prefs.clearUserPref("loop.ping.interval");
+      Services.prefs.clearUserPref("loop.ping.timeout");
     });
 
     run_next_test();
   };
 }
--- a/browser/components/loop/test/xpcshell/test_loopservice_busy.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_busy.js
@@ -123,17 +123,18 @@ add_task(function* test_busy_1guest_1fxa
 function run_test() {
   setupFakeLoopServer();
 
   // Setup fake login state so we get FxA requests.
   const MozLoopServiceInternal = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}).MozLoopServiceInternal;
   MozLoopServiceInternal.fxAOAuthTokenData = {token_type:"bearer",access_token:"1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",scope:"profile"};
   MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
 
-  let mockWebSocket = new MockWebSocketChannel({defaultMsgHandler: msgHandler});
+  let mockWebSocket = new MockWebSocketChannel();
+  mockWebSocket.defaultMsgHandler = msgHandler;
   LoopCallsInternal.mocks.webSocket = mockWebSocket;
 
   Services.io.offline = false;
 
   // For each notification received from the PushServer, MozLoopService will first query
   // for any pending calls on the FxA hawk session and then again using the guest session.
   // A pair of response objects in the callsResponses array will be consumed for each
   // notification. The even calls object is for the FxA session, the odd the Guest session.
--- a/browser/components/loop/test/xpcshell/test_loopservice_registration.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_registration.js
@@ -35,20 +35,24 @@ add_test(function test_register_websocke
 
 add_test(function test_register_success() {
   mockPushHandler.registrationPushURL = kEndPointUrl;
   mockPushHandler.registrationResult = null;
 
   loopServer.registerPathHandler("/registration", (request, response) => {
     let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
     let data = JSON.parse(body);
-    Assert.equal(data.simplePushURLs.calls, kEndPointUrl,
-                 "Should send correct calls push url");
-    Assert.equal(data.simplePushURLs.rooms, kEndPointUrl,
-                 "Should send correct rooms push url");
+    if (data.simplePushURLs.calls) {
+      Assert.equal(data.simplePushURLs.calls, kEndPointUrl,
+                   "Should send correct calls push url");
+    }
+    if (data.simplePushURLs.rooms) {
+      Assert.equal(data.simplePushURLs.rooms, kEndPointUrl,
+                   "Should send correct rooms push url");
+    }
 
     response.setStatusLine(null, 200, "OK");
     response.processAsync();
     response.finish();
   });
   MozLoopService.promiseRegisteredWithServers().then(() => {
     run_next_test();
   }, err => {
--- a/browser/components/loop/test/xpcshell/test_loopservice_registration_retry.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_registration_retry.js
@@ -25,37 +25,41 @@ add_test(function test_retry_after_faile
 
     // Remove the error
     mockPushHandler.registrationResult = null;
     mockPushHandler.registrationPushURL = kEndPointUrl;
 
     yield regError.friendlyDetailsButtonCallback();
     Assert.strictEqual(MozLoopService.errors.size, 0, "Check that the errors are gone");
     let deferredRegistrations = MozLoopServiceInternal.deferredRegistrations;
-    yield deferredRegistrations.get(LOOP_SESSION_TYPE.GUEST).promise.then(() => {
+    yield deferredRegistrations.get(LOOP_SESSION_TYPE.GUEST).then(() => {
       Assert.ok(true, "The retry of registration succeeded");
     },
     (error) => {
       Assert.ok(false, "The retry of registration should have succeeded");
     });
 
     run_next_test();
   }));
 });
 
 function run_test() {
   setupFakeLoopServer();
 
   loopServer.registerPathHandler("/registration", (request, response) => {
     let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
     let data = JSON.parse(body);
-    Assert.equal(data.simplePushURLs.calls, kEndPointUrl,
-                 "Should send correct calls push url");
-    Assert.equal(data.simplePushURLs.rooms, kEndPointUrl,
-                 "Should send correct rooms push url");
+    if (data.simplePushURLs.calls) {
+      Assert.equal(data.simplePushURLs.calls, kEndPointUrl,
+                   "Should send correct calls push url");
+    }
+    if (data.simplePushURLs.rooms) {
+      Assert.equal(data.simplePushURLs.rooms, kEndPointUrl,
+                   "Should send correct rooms push url");
+    }
 
     response.setStatusLine(null, 200, "OK");
   });
 
   let nowSeconds = Date.now() / 1000;
   Services.prefs.setIntPref("loop.urlsExpiryTimeSeconds", nowSeconds + 60);
 
   do_register_cleanup(function() {
--- a/browser/components/loop/test/xpcshell/test_loopservice_token_invalid.js
+++ b/browser/components/loop/test/xpcshell/test_loopservice_token_invalid.js
@@ -29,17 +29,17 @@ add_test(function test_registration_inva
     }
     response.processAsync();
     response.finish();
   });
 
   MozLoopService.promiseRegisteredWithServers().then(() => {
     // Due to the way the time stamp checking code works in hawkclient, we expect a couple
     // of authorization requests before we reset the token.
-    Assert.equal(authorizationAttempts, 2);
+    Assert.equal(authorizationAttempts, 4); //hawk will repeat each registration attemtp twice: calls and rooms.
     Assert.equal(Services.prefs.getCharPref(LOOP_HAWK_PREF), fakeSessionToken2);
     run_next_test();
   }, err => {
     do_throw("shouldn't be a failure result: " + err);
   });
 });
 
 
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -2464,29 +2464,29 @@ let DefaultBrowserCheck = {
       this._notification = notificationBox.appendNotification(promptMessage, "default-browser",
                                                               iconURL, priority, buttons,
                                                               callback);
     } else {
       // Modal prompt
       let promptTitle = shellBundle.getString("setDefaultBrowserTitle");
       let promptMessage = shellBundle.getFormattedString("setDefaultBrowserMessage",
                                                          [brandShortName]);
-      let dontAskLabel = shellBundle.getFormattedString("setDefaultBrowserDontAsk",
-                                                        [brandShortName]);
+      let askLabel = shellBundle.getFormattedString("setDefaultBrowserDontAsk",
+                                                    [brandShortName]);
 
       let ps = Services.prompt;
-      let dontAsk = { value: false };
+      let shouldAsk = { value: true };
       let buttonFlags = (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0) +
                         (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_1) +
                         ps.BUTTON_POS_0_DEFAULT;
       let rv = ps.confirmEx(win, promptTitle, promptMessage, buttonFlags,
-                            yesButton, notNowButton, null, dontAskLabel, dontAsk);
+                            yesButton, notNowButton, null, askLabel, shouldAsk);
       if (rv == 0) {
         this.setAsDefault();
-      } else if (dontAsk.value) {
+      } else if (!shouldAsk.value) {
         ShellService.shouldCheckDefaultBrowser = false;
       }
     }
   },
 
   _onNotificationEvent: function(eventType) {
     if (eventType == "removed") {
       let doc = this._notification.ownerDocument;
@@ -2497,17 +2497,17 @@ let DefaultBrowserCheck = {
     }
   },
 };
 
 #ifdef E10S_TESTING_ONLY
 let E10SUINotification = {
   // Increase this number each time we want to roll out an
   // e10s testing period to Nightly users.
-  CURRENT_NOTICE_COUNT: 3,
+  CURRENT_NOTICE_COUNT: 4,
   CURRENT_PROMPT_PREF: "browser.displayedE10SPrompt.1",
   PREVIOUS_PROMPT_PREF: "browser.displayedE10SPrompt",
 
   checkStatus: function() {
     let skipE10sChecks = false;
     try {
       skipE10sChecks = (UpdateChannel.get() != "nightly") ||
                        Services.prefs.getBoolPref("browser.tabs.remote.autostart.disabled-because-using-a11y");
deleted file mode 100644
--- a/browser/components/places/BrowserPlaces.manifest
+++ /dev/null
@@ -1,2 +0,0 @@
-component {6bcb9bde-9018-4443-a071-c32653469597} PlacesProtocolHandler.js
-contract @mozilla.org/network/protocol;1?name=place {6bcb9bde-9018-4443-a071-c32653469597}
deleted file mode 100644
--- a/browser/components/places/PlacesProtocolHandler.js
+++ /dev/null
@@ -1,49 +0,0 @@
-/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
- * vim: sw=2 ts=2 sts=2 et
- * 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/. */
-
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-
-Components.utils.import("resource://gre/modules/NetUtil.jsm");
-Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-const SCHEME = "place";
-const URL = "chrome://browser/content/places/content-ui/controller.xhtml";
-
-function PlacesProtocolHandler() {}
-
-PlacesProtocolHandler.prototype = {
-  scheme: SCHEME,
-  defaultPort: -1,
-  protocolFlags: Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD |
-                 Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE |
-                 Ci.nsIProtocolHandler.URI_NORELATIVE |
-                 Ci.nsIProtocolHandler.URI_NOAUTH,
-
-  newURI: function PPH_newURI(aSpec, aOriginCharset, aBaseUri) {
-    let uri = Cc["@mozilla.org/network/simple-uri;1"].createInstance(Ci.nsIURI);
-    uri.spec = aSpec;
-    return uri;
-  },
-
-  newChannel: function PPH_newChannel(aUri) {
-    let chan = NetUtil.newChannel(URL);
-    chan.originalURI = aUri;
-    return chan;
-  },
-
-  allowPort: function PPH_allowPort(aPort, aScheme) {
-    return false;
-  },
-
-  QueryInterface: XPCOMUtils.generateQI([
-    Ci.nsIProtocolHandler
-  ]),
-
-  classID: Components.ID("{6bcb9bde-9018-4443-a071-c32653469597}")
-};
-
-this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PlacesProtocolHandler]);
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -821,22 +821,22 @@ this.PlacesUIUtils = {
     this._openNodeIn(aNode, window.whereToOpenLink(aEvent, false, true), window);
   },
 
   /**
    * Loads the node's URL in the appropriate tab or window or as a
    * web panel.
    * see also openUILinkIn
    */
-  openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aView) {
+  openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aView, aPrivate) {
     let window = aView.ownerWindow;
-    this._openNodeIn(aNode, aWhere, window);
+    this._openNodeIn(aNode, aWhere, window, aPrivate);
   },
 
-  _openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aWindow) {
+  _openNodeIn: function PUIU_openNodeIn(aNode, aWhere, aWindow, aPrivate=false) {
     if (aNode && PlacesUtils.nodeIsURI(aNode) &&
         this.checkURLSecurity(aNode, aWindow)) {
       let isBookmark = PlacesUtils.nodeIsBookmark(aNode);
 
       if (!PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
         if (isBookmark)
           this.markPageAsFollowedBookmark(aNode.uri);
         else
@@ -850,18 +850,20 @@ this.PlacesUIUtils = {
                        .itemHasAnnotation(aNode.itemId, this.LOAD_IN_SIDEBAR_ANNO)) {
           let browserWin = this._getTopBrowserWin();
           if (browserWin) {
             browserWin.openWebPanel(aNode.title, aNode.uri);
             return;
           }
         }
       }
+
       aWindow.openUILinkIn(aNode.uri, aWhere, {
-        inBackground: Services.prefs.getBoolPref("browser.tabs.loadBookmarksInBackground")
+        inBackground: Services.prefs.getBoolPref("browser.tabs.loadBookmarksInBackground"),
+        private: aPrivate,
       });
     }
   },
 
   /**
    * Helper for guessing scheme from an url string.
    * Used to avoid nsIURI overhead in frequently called UI functions.
    *
--- a/browser/components/places/content/controller.js
+++ b/browser/components/places/content/controller.js
@@ -2,16 +2,18 @@
 /* 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/. */
 
 XPCOMUtils.defineLazyModuleGetter(this, "ForgetAboutSite",
                                   "resource://gre/modules/ForgetAboutSite.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 // XXXmano: we should move most/all of these constants to PlacesUtils
 const ORGANIZER_ROOT_BOOKMARKS = "place:folder=BOOKMARKS_MENU&excludeItems=1&queryType=1";
 
 // No change to the view, preserve current selection
 const RELOAD_ACTION_NOTHING = 0;
 // Inserting items new to the view, select the inserted rows
 const RELOAD_ACTION_INSERT = 1;
@@ -173,16 +175,17 @@ PlacesController.prototype = {
       if (this._view.selType != "single") {
         let rootNode = this._view.result.root;
         if (rootNode.containerOpen && rootNode.childCount > 0)
           return true;
       }
       return false;
     case "placesCmd_open":
     case "placesCmd_open:window":
+    case "placesCmd_open:privatewindow":
     case "placesCmd_open:tab":
       var selectedNode = this._view.selectedNode;
       return selectedNode && PlacesUtils.nodeIsURI(selectedNode);
     case "placesCmd_new:folder":
       return this._canInsert();
     case "placesCmd_new:bookmark":
       return this._canInsert();
     case "placesCmd_new:separator":
@@ -258,16 +261,19 @@ PlacesController.prototype = {
       this.selectAll();
       break;
     case "placesCmd_open":
       PlacesUIUtils.openNodeIn(this._view.selectedNode, "current", this._view);
       break;
     case "placesCmd_open:window":
       PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view);
       break;
+    case "placesCmd_open:privatewindow":
+      PlacesUIUtils.openNodeIn(this._view.selectedNode, "window", this._view, true);
+      break;
     case "placesCmd_open:tab":
       PlacesUIUtils.openNodeIn(this._view.selectedNode, "tab", this._view);
       break;
     case "placesCmd_new:folder":
       this.newItem("folder");
       break;
     case "placesCmd_new:bookmark":
       this.newItem("bookmark");
@@ -596,17 +602,20 @@ PlacesController.prototype = {
     var visibleItemsBeforeSep = false;
     var usableItemCount = 0;
     for (var i = 0; i < aPopup.childNodes.length; ++i) {
       var item = aPopup.childNodes[i];
       if (item.localName != "menuseparator") {
         // We allow pasting into tag containers, so special case that.
         var hideIfNoIP = item.getAttribute("hideifnoinsertionpoint") == "true" &&
                          noIp && !(ip && ip.isTag && item.id == "placesContext_paste");
-        var shouldHideItem = hideIfNoIP || !this._shouldShowMenuItem(item, metadata);
+        var hideIfPrivate = item.getAttribute("hideifprivatebrowsing") == "true" &&
+                            PrivateBrowsingUtils.isWindowPrivate(window);
+        var shouldHideItem = hideIfNoIP || hideIfPrivate ||
+                             !this._shouldShowMenuItem(item, metadata);
         item.hidden = item.disabled = shouldHideItem;
 
         if (!item.hidden) {
           visibleItemsBeforeSep = true;
           usableItemCount++;
 
           // Show the separator above the menu-item if any
           if (separator) {
@@ -1685,16 +1694,17 @@ function goUpdatePlacesCommands() {
   var placesController = doGetPlacesControllerForCommand("placesCmd_open");
   function updatePlacesCommand(aCommand) {
     goSetCommandEnabled(aCommand, placesController &&
                                   placesController.isCommandEnabled(aCommand));
   }
 
   updatePlacesCommand("placesCmd_open");
   updatePlacesCommand("placesCmd_open:window");
+  updatePlacesCommand("placesCmd_open:privatewindow");
   updatePlacesCommand("placesCmd_open:tab");
   updatePlacesCommand("placesCmd_new:folder");
   updatePlacesCommand("placesCmd_new:bookmark");
   updatePlacesCommand("placesCmd_new:separator");
   updatePlacesCommand("placesCmd_show:info");
   updatePlacesCommand("placesCmd_moveBookmarks");
   updatePlacesCommand("placesCmd_reload");
   updatePlacesCommand("placesCmd_sortBy:name");
--- a/browser/components/places/content/editBookmarkOverlay.js
+++ b/browser/components/places/content/editBookmarkOverlay.js
@@ -872,26 +872,31 @@ var gEditItemOverlay = {
                                   this._folderTree.columns.getFirstColumn());
   },
 
   // nsIDOMEventListener
   handleEvent: function EIO_nsIDOMEventListener(aEvent) {
     switch (aEvent.type) {
     case "CheckboxStateChange":
       // Update the tags field when items are checked/unchecked in the listbox
-      var tags = this._getTagsArrayFromTagField();
+      let tags = this._getTagsArrayFromTagField();
+      let tagCheckbox = aEvent.target;
+
+      let curTagIndex = tags.indexOf(tagCheckbox.label);
 
-      if (aEvent.target.checked) {
-        if (tags.indexOf(aEvent.target.label) == -1)
-          tags.push(aEvent.target.label);
+      let tagsSelector = this._element("tagsSelector");
+      tagsSelector.selectedItem = tagCheckbox;
+
+      if (tagCheckbox.checked) {
+        if (curTagIndex == -1)
+          tags.push(tagCheckbox.label);
       }
       else {
-        var indexOfItem = tags.indexOf(aEvent.target.label);
-        if (indexOfItem != -1)
-          tags.splice(indexOfItem, 1);
+        if (curTagIndex != -1)
+          tags.splice(curTagIndex, 1);
       }
       this._element("tagsField").value = tags.join(", ");
       this._updateTags();
       break;
     case "blur":
       let replaceFn = (str, firstLetter) => firstLetter.toUpperCase();
       let nodeName = aEvent.target.id.replace(/editBMPanel_(\w)/, replaceFn);
       this["on" + nodeName + "Blur"]();
--- a/browser/components/places/content/placesOverlay.xul
+++ b/browser/components/places/content/placesOverlay.xul
@@ -49,16 +49,18 @@
   <commandset id="placesCommands"
               commandupdater="true"
               events="focus,sort,places"
               oncommandupdate="goUpdatePlacesCommands();">
     <command id="placesCmd_open"
              oncommand="goDoPlacesCommand('placesCmd_open');"/>
     <command id="placesCmd_open:window"
              oncommand="goDoPlacesCommand('placesCmd_open:window');"/>
+    <command id="placesCmd_open:privatewindow"
+             oncommand="goDoPlacesCommand('placesCmd_open:privatewindow');"/>
     <command id="placesCmd_open:tab"
              oncommand="goDoPlacesCommand('placesCmd_open:tab');"/>
 
     <command id="placesCmd_new:bookmark"
              oncommand="goDoPlacesCommand('placesCmd_new:bookmark');"/>
     <command id="placesCmd_new:folder"
              oncommand="goDoPlacesCommand('placesCmd_new:folder');"/>
     <command id="placesCmd_new:separator"
@@ -124,16 +126,23 @@
               selectiontype="multiple"
               selection="link"/>
     <menuitem id="placesContext_open:newwindow"
               command="placesCmd_open:window"
               label="&cmd.open_window.label;"
               accesskey="&cmd.open_window.accesskey;"
               selectiontype="single"
               selection="link"/>
+    <menuitem id="placesContext_open:newprivatewindow"
+              command="placesCmd_open:privatewindow"
+              label="&cmd.open_private_window.label;"
+              accesskey="&cmd.open_private_window.accesskey;"
+              selectiontype="single"
+              selection="link"
+              hideifprivatebrowsing="true"/>
     <menuseparator id="placesContext_openSeparator"/>
     <menuitem id="placesContext_new:bookmark"
               command="placesCmd_new:bookmark"
               label="&cmd.new_bookmark.label;"
               accesskey="&cmd.new_bookmark.accesskey;"
               selectiontype="any"
               hideifnoinsertionpoint="true"/>
     <menuitem id="placesContext_new:folder"
--- a/browser/components/places/moz.build
+++ b/browser/components/places/moz.build
@@ -5,16 +5,11 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
 MOCHITEST_CHROME_MANIFESTS += ['tests/chrome/chrome.ini']
 BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
 
 JAR_MANIFESTS += ['jar.mn']
 
-EXTRA_COMPONENTS += [
-    'BrowserPlaces.manifest',
-    'PlacesProtocolHandler.js',
-]
-
 EXTRA_PP_JS_MODULES += [
     'PlacesUIUtils.jsm',
 ]
--- a/browser/components/preferences/in-content/search.js
+++ b/browser/components/preferences/in-content/search.js
@@ -19,16 +19,22 @@ var gSearchPane = {
         document.location.hash = "";
       return;
     }
 
     gEngineView = new EngineView(new EngineStore());
     document.getElementById("engineList").view = gEngineView;
     this.buildDefaultEngineDropDown();
 
+    window.addEventListener("click", this, false);
+    window.addEventListener("command", this, false);
+    window.addEventListener("dragstart", this, false);
+    window.addEventListener("keypress", this, false);
+    window.addEventListener("select", this, false);
+
     Services.obs.addObserver(this, "browser-search-engine-modified", false);
     window.addEventListener("unload", () => {
       Services.obs.removeObserver(this, "browser-search-engine-modified", false);
     });
   },
 
   buildDefaultEngineDropDown: function() {
     // This is called each time something affects the list of engines.
@@ -58,16 +64,59 @@ var gSearchPane = {
         item.setAttribute("image", uri);
       }
       item.engine = e;
       if (e.name == currentEngine)
         list.selectedItem = item;
     });
   },
 
+  handleEvent: function(aEvent) {
+    switch (aEvent.type) {
+      case "click":
+        if (aEvent.target.id == "addEngines" && aEvent.button == 0) {
+          Services.wm.getMostRecentWindow('navigator:browser')
+                     .BrowserSearch.loadAddEngines();
+        }
+        break;
+      case "command":
+        switch (aEvent.target.id) {
+          case "":
+            if (aEvent.target.parentNode &&
+                aEvent.target.parentNode.parentNode &&
+                aEvent.target.parentNode.parentNode.id == "defaultEngine") {
+              gSearchPane.setDefaultEngine();
+            }
+            break;
+          case "restoreDefaultSearchEngines":
+            gSearchPane.onRestoreDefaults();
+            break;
+          case "removeEngineButton":
+            gSearchPane.remove();
+            break;
+        }
+        break;
+      case "dragstart":
+        if (aEvent.target.id == "engineChildren") {
+          onDragEngineStart(aEvent);
+        }
+        break;
+      case "keypress":
+        if (aEvent.target.id == "engineList") {
+          gSearchPane.onTreeKeyPress(aEvent);
+        }
+        break;
+      case "select":
+        if (aEvent.target.id == "engineList") {
+          gSearchPane.onTreeSelect();
+        }
+        break;
+    }
+  },
+
   observe: function(aEngine, aTopic, aVerb) {
     if (aTopic == "browser-search-engine-modified") {
       aEngine.QueryInterface(Components.interfaces.nsISearchEngine);
       switch (aVerb) {
       case "engine-added":
         gEngineView._engineStore.addEngine(aEngine);
         gEngineView.rowCountChanged(gEngineView.lastIndex, 1);
         gSearchPane.buildDefaultEngineDropDown();
--- a/browser/components/preferences/in-content/search.xul
+++ b/browser/components/preferences/in-content/search.xul
@@ -23,53 +23,50 @@
           data-category="paneSearch">
       <label class="header-name">&paneSearch.title;</label>
     </hbox>
 
     <!-- Default Search Engine -->
     <groupbox id="defaultEngineGroup" align="start" data-category="paneSearch">
       <caption label="&defaultSearchEngine.label;"/>
       <label>&chooseYourDefaultSearchEngine.label;</label>
-      <menulist id="defaultEngine" oncommand="gSearchPane.setDefaultEngine();">
+      <menulist id="defaultEngine">
         <menupopup/>
       </menulist>
       <checkbox id="suggestionsInSearchFieldsCheckbox"
                 label="&provideSearchSuggestions.label;"
                 accesskey="&provideSearchSuggestions.accesskey;"
                 preference="browser.search.suggest.enabled"/>
     </groupbox>
 
     <groupbox id="oneClickSearchProvidersGroup" data-category="paneSearch">
       <caption label="&oneClickSearchEngines.label;"/>
       <label>&chooseWhichOneToDisplay.label;</label>
 
       <tree id="engineList" flex="1" rows="8" hidecolumnpicker="true" editable="true"
-            seltype="single" onselect="gSearchPane.onTreeSelect();"
-            onkeypress="gSearchPane.onTreeKeyPress(event);">
-        <treechildren id="engineChildren" flex="1"
-                      ondragstart="onDragEngineStart(event);"/>
+            seltype="single">
+        <treechildren id="engineChildren" flex="1"/>
         <treecols>
-          <treecol id="engineShown" type="checkbox" style="min-width: 26px;" editable="true"/>
+          <treecol id="engineShown" type="checkbox" editable="true"/>
           <treecol id="engineName" flex="4" label="&engineNameColumn.label;"/>
           <treecol id="engineKeyword" flex="1" label="&engineKeywordColumn.label;" editable="true"/>
         </treecols>
       </tree>
 
       <hbox>
         <button id="restoreDefaultSearchEngines"
                 label="&restoreDefaultSearchEngines.label;"
                 accesskey="&restoreDefaultSearchEngines.accesskey;"
-                oncommand="gSearchPane.onRestoreDefaults();"/>
+                />
         <spacer flex="1"/>
         <button id="removeEngineButton"
                 label="&removeEngine.label;"
                 accesskey="&removeEngine.accesskey;"
                 disabled="true"
-                oncommand="gSearchPane.remove();"/>
+                />
       </hbox>
 
       <separator class="thin"/>
 
-      <hbox pack="start" style="margin-bottom: 1em">
-        <label id="addEngines" class="text-link" value="&addMoreSearchEngines.label;"
-               onclick="if (event.button == 0) { Services.wm.getMostRecentWindow('navigator:browser').BrowserSearch.loadAddEngines(); }"/>
+      <hbox id="addEnginesBox" pack="start">
+        <label id="addEngines" class="text-link" value="&addMoreSearchEngines.label;"/>
       </hbox>
     </groupbox>
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -118,29 +118,29 @@ this.UITour = {
       query: "#panic-button",
       widgetName: "panic-button",
       allowAdd: true,
     }],
     ["loop",        {query: "#loop-button"}],
     ["loop-newRoom", {
       infoPanelPosition: "leftcenter topright",
       query: (aDocument) => {
-        let loopBrowser = aDocument.querySelector("#loop-notification-panel > #loop");
+        let loopBrowser = aDocument.querySelector("#loop-notification-panel > #loop-panel-iframe");
         if (!loopBrowser) {
           return null;
         }
         // Use the parentElement full-width container of the button so our arrow
         // doesn't overlap the panel contents much.
         return loopBrowser.contentDocument.querySelector(".new-room-button").parentElement;
       },
     }],
     ["loop-roomList", {
       infoPanelPosition: "leftcenter topright",
       query: (aDocument) => {
-        let loopBrowser = aDocument.querySelector("#loop-notification-panel > #loop");
+        let loopBrowser = aDocument.querySelector("#loop-notification-panel > #loop-panel-iframe");
         if (!loopBrowser) {
           return null;
         }
         return loopBrowser.contentDocument.querySelector(".room-list");
       },
     }],
     ["loop-selectedRoomButtons", {
       infoPanelOffsetY: -20,
@@ -157,17 +157,17 @@ this.UITour = {
         // But anchor on the <browser> in the chatbox so the panel doesn't jump to undefined
         // positions when the copy/email buttons disappear e.g. when the feedback form opens or
         // somebody else joins the room.
         return chatbox.content;
       },
     }],
     ["loop-signInUpLink", {
       query: (aDocument) => {
-        let loopBrowser = aDocument.querySelector("#loop-notification-panel > #loop");
+        let loopBrowser = aDocument.querySelector("#loop-notification-panel > #loop-panel-iframe");
         if (!loopBrowser) {
           return null;
         }
         return loopBrowser.contentDocument.querySelector(".signin-link");
       },
     }],
     ["privateWindow",  {query: "#privatebrowsing-button"}],
     ["quit",        {query: "#PanelUI-quit"}],
--- a/browser/config/mozconfigs/linux32/common-opt
+++ b/browser/config/mozconfigs/linux32/common-opt
@@ -1,14 +1,15 @@
 # This file is sourced by nightly, beta, and release mozconfigs.
 
 ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
 ac_add_options --enable-update-packaging
 ac_add_options --with-google-api-keyfile=/builds/gapi.data
 ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
+ac_add_options --with-mozilla-api-keyfile=/builds/mozilla-desktop-geoloc-api.key
 
 . $topsrcdir/build/unix/mozconfig.linux32
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
 export MOZ_TELEMETRY_REPORTING=1
 
--- a/browser/config/mozconfigs/linux32/debug
+++ b/browser/config/mozconfigs/linux32/debug
@@ -1,15 +1,13 @@
 ac_add_options --enable-debug
 ac_add_options --enable-dmd
 ac_add_options --enable-signmar
 ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
 
-ac_add_options --disable-unified-compilation
-
 MOZ_AUTOMATION_L10N_CHECK=0
 
 . $topsrcdir/build/unix/mozconfig.linux32
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
 #Use ccache
--- a/browser/config/mozconfigs/linux64/common-opt
+++ b/browser/config/mozconfigs/linux64/common-opt
@@ -1,14 +1,15 @@
 # This file is sourced by the nightly, beta, and release mozconfigs.
 
 ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
 ac_add_options --enable-update-packaging
 ac_add_options --with-google-api-keyfile=/builds/gapi.data
 ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
+ac_add_options --with-mozilla-api-keyfile=/builds/mozilla-desktop-geoloc-api.key
 
 . $topsrcdir/build/unix/mozconfig.linux
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
 export MOZ_TELEMETRY_REPORTING=1
 
deleted file mode 100644
--- a/browser/config/mozconfigs/linux64/debug-nonunified
+++ /dev/null
@@ -1,5 +0,0 @@
-MOZ_AUTOMATION_UPLOAD=0
-
-. "$topsrcdir/browser/config/mozconfigs/linux64/debug"
-
-ac_add_options --disable-unified-compilation
--- a/browser/config/mozconfigs/linux64/debug-static-analysis-clang
+++ b/browser/config/mozconfigs/linux64/debug-static-analysis-clang
@@ -2,18 +2,16 @@ MOZ_AUTOMATION_BUILD_SYMBOLS=0
 MOZ_AUTOMATION_PACKAGE_TESTS=0
 MOZ_AUTOMATION_L10N_CHECK=0
 
 . "$topsrcdir/build/mozconfig.common"
 
 ac_add_options --enable-debug
 ac_add_options --enable-dmd
 
-ac_add_options --disable-unified-compilation
-
 # Use Clang as specified in manifest
 export CC="$topsrcdir/clang/bin/clang"
 export CXX="$topsrcdir/clang/bin/clang++"
 
 # Add the static checker
 ac_add_options --enable-clang-plugin
 
 # Avoid dependency on libstdc++ 4.7
deleted file mode 100644
--- a/browser/config/mozconfigs/linux64/nightly-nonunified
+++ /dev/null
@@ -1,6 +0,0 @@
-MOZ_AUTOMATION_UPLOAD=0
-MOZ_AUTOMATION_PRETTY=1
-
-. "$topsrcdir/browser/config/mozconfigs/linux64/nightly"
-
-ac_add_options --disable-unified-compilation
--- a/browser/config/mozconfigs/macosx-universal/common-opt
+++ b/browser/config/mozconfigs/macosx-universal/common-opt
@@ -4,16 +4,17 @@
 
 # Universal builds override the default of browser (bug 575283 comment 29)
 ac_add_options --enable-application=browser
 
 ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
 ac_add_options --enable-update-packaging
 ac_add_options --with-google-api-keyfile=/builds/gapi.data
 ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
+ac_add_options --with-mozilla-api-keyfile=/builds/mozilla-desktop-geoloc-api.key
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
 export MOZ_TELEMETRY_REPORTING=1
 
 # Treat warnings as errors in directories with FAIL_ON_WARNINGS.
 ac_add_options --enable-warnings-as-errors
deleted file mode 100644
--- a/browser/config/mozconfigs/macosx-universal/nightly-nonunified
+++ /dev/null
@@ -1,5 +0,0 @@
-MOZ_AUTOMATION_PRETTY=1
-MOZ_AUTOMATION_UPLOAD=0
-. "$topsrcdir/browser/config/mozconfigs/macosx-universal/nightly"
-
-ac_add_options --disable-unified-compilation
--- a/browser/config/mozconfigs/macosx64/debug-asan
+++ b/browser/config/mozconfigs/macosx64/debug-asan
@@ -1,17 +1,15 @@
 . $topsrcdir/build/unix/mozconfig.asan
 
 ac_add_options --enable-application=browser
 ac_add_options --enable-debug
 ac_add_options --enable-optimize="-O1"
 ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
 
-ac_add_options --disable-unified-compilation
-
 # Package js shell.
 export MOZ_PACKAGE_JSSHELL=1
 
 if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then
 ac_add_options --with-macbundlename-prefix=Firefox
 fi
 
 # Need this to prevent name conflicts with the normal nightly build packages
deleted file mode 100644
--- a/browser/config/mozconfigs/macosx64/debug-nonunified
+++ /dev/null
@@ -1,4 +0,0 @@
-MOZ_AUTOMATION_UPLOAD=0
-. "$topsrcdir/browser/config/mozconfigs/macosx64/debug"
-
-ac_add_options --disable-unified-compilation
--- a/browser/config/mozconfigs/win32/common-opt
+++ b/browser/config/mozconfigs/win32/common-opt
@@ -15,16 +15,17 @@ fi
 ac_add_options --with-google-api-keyfile=${_gapi_keyfile}
 
 if [ -f /c/builds/google-oauth-api.key ]; then
   _google_oauth_api_keyfile=/c/builds/google-oauth-api.key
 else
   _google_oauth_api_keyfile=/e/builds/google-oauth-api.key
 fi
 ac_add_options --with-google-oauth-api-keyfile=${_google_oauth_api_keyfile}
+ac_add_options --with-mozilla-api-keyfile=/c/builds/mozilla-desktop-geoloc-api.key
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
 export MOZ_TELEMETRY_REPORTING=1
 
 if test "$PROCESSOR_ARCHITECTURE" = "AMD64" -o "$PROCESSOR_ARCHITEW6432" = "AMD64"; then
   . $topsrcdir/build/win32/mozconfig.vs2013-win64
deleted file mode 100644
--- a/browser/config/mozconfigs/win32/debug-nonunified
+++ /dev/null
@@ -1,6 +0,0 @@
-. "$topsrcdir/build/mozconfig.win-common"
-MOZ_AUTOMATION_L10N_CHECK=0
-MOZ_AUTOMATION_UPLOAD=0
-. "$topsrcdir/browser/config/mozconfigs/win32/debug"
-
-ac_add_options --disable-unified-compilation
deleted file mode 100644
--- a/browser/config/mozconfigs/win32/nightly-nonunified
+++ /dev/null
@@ -1,6 +0,0 @@
-. "$topsrcdir/build/mozconfig.win-common"
-MOZ_AUTOMATION_PRETTY=1
-MOZ_AUTOMATION_UPLOAD=0
-. "$topsrcdir/browser/config/mozconfigs/win32/nightly"
-
-ac_add_options --disable-unified-compilation
--- a/browser/config/mozconfigs/win64/common-opt
+++ b/browser/config/mozconfigs/win64/common-opt
@@ -13,16 +13,17 @@ fi
 ac_add_options --with-google-api-keyfile=${_gapi_keyfile}
 
 if [ -f /c/builds/google-oauth-api.key ]; then
   _google_oauth_api_keyfile=/c/builds/google-oauth-api.key
 else
   _google_oauth_api_keyfile=/e/builds/google-oauth-api.key
 fi
 ac_add_options --with-google-oauth-api-keyfile=${_google_oauth_api_keyfile}
+ac_add_options --with-mozilla-api-keyfile=/c/builds/mozilla-desktop-geoloc-api.key
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
 export MOZ_TELEMETRY_REPORTING=1
 
 # Package js shell.
 export MOZ_PACKAGE_JSSHELL=1
deleted file mode 100644
--- a/browser/config/mozconfigs/win64/debug-nonunified
+++ /dev/null
@@ -1,3 +0,0 @@
-. "$topsrcdir/browser/config/mozconfigs/win64/debug"
-
-ac_add_options --disable-unified-compilation
deleted file mode 100644
--- a/browser/config/mozconfigs/win64/nightly-nonunified
+++ /dev/null
@@ -1,3 +0,0 @@
-. "$topsrcdir/browser/config/mozconfigs/win64/nightly"
-
-ac_add_options --disable-unified-compilation
--- a/browser/devtools/debugger/debugger-panes.js
+++ b/browser/devtools/debugger/debugger-panes.js
@@ -5,21 +5,24 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 // Used to detect minification for automatic pretty printing
 const SAMPLE_SIZE = 50; // no of lines
 const INDENT_COUNT_THRESHOLD = 5; // percentage
 const CHARACTER_LIMIT = 250; // line character limit
 
-// Maps known URLs to friendly source group names
+// Maps known URLs to friendly source group names and put them at the
+// bottom of source list.
 const KNOWN_SOURCE_GROUPS = {
   "Add-on SDK": "resource://gre/modules/commonjs/",
 };
 
+KNOWN_SOURCE_GROUPS[L10N.getStr("evalGroupLabel")] = "eval";
+
 /**
  * Functions handling the sources UI.
  */
 function SourcesView() {
   dumpn("SourcesView was instantiated");
 
   this.togglePrettyPrint = this.togglePrettyPrint.bind(this);
   this.toggleBlackBoxing = this.toggleBlackBoxing.bind(this);
@@ -165,24 +168,32 @@ SourcesView.prototype = Heritage.extend(
       }
     });
   },
 
   _parseUrl: function(aSource) {
     let fullUrl = aSource.url || aSource.introductionUrl;
     let url = fullUrl.split(" -> ").pop();
     let label = aSource.addonPath ? aSource.addonPath : SourceUtils.getSourceLabel(url);
+    let group;
 
     if (!aSource.url && aSource.introductionUrl) {
-      label += ' > eval';
+      label += ' > ' + aSource.introductionType;
+      group = L10N.getStr("evalGroupLabel");
+    }
+    else if(aSource.addonID) {
+      group = aSource.addonID;
+    }
+    else {
+      group = SourceUtils.getSourceGroup(url);
     }
 
     return {
       label: label,
-      group: aSource.addonID ? aSource.addonID : SourceUtils.getSourceGroup(url),
+      group: group,
       unicodeUrl: NetworkHelper.convertToUnicode(unescape(fullUrl))
     };
   },
 
   /**
    * Adds a breakpoint to this sources container.
    *
    * @param object aBreakpointClient
--- a/browser/devtools/debugger/test/browser_dbg_sources-eval-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_sources-eval-01.js
@@ -23,12 +23,17 @@ function test() {
       is(gSources.values.length, 1, "Should have 1 source");
 
       let newSource = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.NEW_SOURCE);
       callInTab(gTab, "evalSource");
       yield newSource;
 
       is(gSources.values.length, 2, "Should have 2 sources");
 
+      let item = gSources.getItemForAttachment(e => e.label.indexOf("> eval") !== -1);
+      ok(item, "Source label is incorrect.");
+      is(item.attachment.group, gDebugger.L10N.getStr('evalGroupLabel'),
+         'Source group is incorrect');
+
       yield closeDebuggerAndFinish(gPanel);
     });
   });
 }
--- a/browser/devtools/debugger/test/browser_dbg_sources-eval-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_sources-eval-02.js
@@ -27,17 +27,18 @@ function test() {
       let newSource = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.NEW_SOURCE);
       callInTab(gTab, "evalSourceWithSourceURL");
       yield newSource;
 
       is(gSources.values.length, 2, "Should have 2 sources");
 
       let item = gSources.getItemForAttachment(e => e.label == "bar.js");
       ok(item, "Source label is incorrect.");
-      ok(item.attachment.group === 'http://example.com', 'Source group is incorrect');
+      is(item.attachment.group, 'http://example.com',
+         'Source group is incorrect');
 
       let shown = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.SOURCE_SHOWN);
       gSources.selectedItem = item;
       yield shown;
 
       ok(gEditor.getText().indexOf('bar = function() {') === 0,
          'Correct source is shown');
 
--- a/browser/devtools/framework/toolbox-process-window.js
+++ b/browser/devtools/framework/toolbox-process-window.js
@@ -46,16 +46,17 @@ let connect = Task.async(function*() {
   });
 });
 
 // Certain options should be toggled since we can assume chrome debugging here
 function setPrefDefaults() {
   Services.prefs.setBoolPref("devtools.inspector.showUserAgentStyles", true);
   Services.prefs.setBoolPref("devtools.profiler.ui.show-platform-data", true);
   Services.prefs.setBoolPref("browser.devedition.theme.showCustomizeButton", false);
+  Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true);
 }
 
 window.addEventListener("load", function() {
   let cmdClose = document.getElementById("toolbox-cmd-close");
   cmdClose.addEventListener("command", onCloseCommand);
   setPrefDefaults();
   connect().catch(Cu.reportError);
 });
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -1498,17 +1498,19 @@ Toolbox.prototype = {
   /**
    * Initialize the inspector/walker/selection/highlighter fronts.
    * Returns a promise that resolves when the fronts are initialized
    */
   initInspector: function() {
     if (!this._initInspector) {
       this._initInspector = Task.spawn(function*() {
         this._inspector = InspectorFront(this._target.client, this._target.form);
-        this._walker = yield this._inspector.getWalker();
+        this._walker = yield this._inspector.getWalker(
+          {showAllAnonymousContent: Services.prefs.getBoolPref("devtools.inspector.showAllAnonymousContent")}
+        );
         this._selection = new Selection(this._walker);
 
         if (this.highlighterUtils.isRemoteHighlightable()) {
           this.walker.on("highlighter-ready", this._highlighterReady);
           this.walker.on("highlighter-hide", this._highlighterHidden);
 
           let autohide = !gDevTools.testing;
           this._highlighter = yield this._inspector.getHighlighter(autohide);
--- a/browser/devtools/jar.mn
+++ b/browser/devtools/jar.mn
@@ -80,29 +80,32 @@ browser.jar:
     content/browser/devtools/webaudioeditor.xul                        (webaudioeditor/webaudioeditor.xul)
     content/browser/devtools/dagre-d3.js                               (webaudioeditor/lib/dagre-d3.js)
     content/browser/devtools/webaudioeditor/includes.js                (webaudioeditor/includes.js)
     content/browser/devtools/webaudioeditor/models.js                  (webaudioeditor/models.js)
     content/browser/devtools/webaudioeditor/controller.js              (webaudioeditor/controller.js)
     content/browser/devtools/webaudioeditor/views/utils.js             (webaudioeditor/views/utils.js)
     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/webaudioeditor/views/properties.js        (webaudioeditor/views/properties.js)
+    content/browser/devtools/webaudioeditor/views/automation.js        (webaudioeditor/views/automation.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/performance-controller.js     (performance/performance-controller.js)
     content/browser/devtools/performance/performance-view.js           (performance/performance-view.js)
     content/browser/devtools/performance/views/overview.js             (performance/views/overview.js)
     content/browser/devtools/performance/views/details.js              (performance/views/details.js)
     content/browser/devtools/performance/views/details-call-tree.js    (performance/views/details-call-tree.js)
     content/browser/devtools/performance/views/details-waterfall.js    (performance/views/details-waterfall.js)
     content/browser/devtools/performance/views/details-flamegraph.js   (performance/views/details-flamegraph.js)
+    content/browser/devtools/performance/views/recordings.js           (performance/views/recordings.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)
--- a/browser/devtools/markupview/test/browser.ini
+++ b/browser/devtools/markupview/test/browser.ini
@@ -29,16 +29,17 @@ support-files =
   lib_jquery_1.7_min.js
   lib_jquery_1.11.1_min.js
   lib_jquery_2.1.1_min.js
 
 [browser_markupview_anonymous_01.js]
 [browser_markupview_anonymous_02.js]
 skip-if = e10s # scratchpad.xul is not loading in e10s window
 [browser_markupview_anonymous_03.js]
+[browser_markupview_anonymous_04.js]
 [browser_markupview_copy_image_data.js]
 [browser_markupview_css_completion_style_attribute.js]
 [browser_markupview_events.js]
 skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
 [browser_markupview_events-overflow.js]
 skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
 [browser_markupview_events_jquery_1.0.js]
 skip-if = e10s # Bug 1040751 - CodeMirror editor.destroy() isn't e10s compatible
--- a/browser/devtools/markupview/test/browser_markupview_anonymous_01.js
+++ b/browser/devtools/markupview/test/browser_markupview_anonymous_01.js
@@ -22,9 +22,23 @@ add_task(function*() {
 
   info ("Checking the normal child element");
   let span = children.nodes[1];
   yield isEditingMenuEnabled(span, inspector);
 
   info ("Checking the ::after pseudo element");
   let after = children.nodes[2];
   yield isEditingMenuDisabled(after, inspector);
+
+  let native = yield getNodeFront("#native", inspector);
+
+  // Markup looks like: <div><video controls /></div>
+  let nativeChildren = yield inspector.walker.children(native);
+  is (nativeChildren.nodes.length, 1, "Children returned from walker");
+
+  info ("Checking the video element");
+  let video = nativeChildren.nodes[0];
+  ok (!video.isAnonymous, "<video> is not anonymous");
+
+  let videoChildren = yield inspector.walker.children(video);
+  is (videoChildren.nodes.length, 0,
+    "No native children returned from walker for <video> by default");
 });
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_markupview_anonymous_04.js
@@ -0,0 +1,36 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test native anonymous content in the markupview with devtools.inspector.showAllAnonymousContent
+// set to true
+const TEST_URL = TEST_URL_ROOT + "doc_markup_anonymous.html";
+
+add_task(function*() {
+  Services.prefs.setBoolPref("devtools.inspector.showAllAnonymousContent", true);
+
+  let {inspector} = yield addTab(TEST_URL).then(openInspector);
+
+  let native = yield getNodeFront("#native", inspector);
+
+  // Markup looks like: <div><video controls /></div>
+  let nativeChildren = yield inspector.walker.children(native);
+  is (nativeChildren.nodes.length, 1, "Children returned from walker");
+
+  info ("Checking the video element");
+  let video = nativeChildren.nodes[0];
+  ok (!video.isAnonymous, "<video> is not anonymous");
+
+  let videoChildren = yield inspector.walker.children(video);
+  is (videoChildren.nodes.length, 3, "<video> has native anonymous children");
+
+  for (let node of videoChildren.nodes) {
+    ok (node.isAnonymous, "Child is anonymous");
+    ok (!node._form.isXBLAnonymous, "Child is not XBL anonymous");
+    ok (!node._form.isShadowAnonymous, "Child is not shadow anonymous");
+    ok (node._form.isNativeAnonymous, "Child is native anonymous");
+    yield isEditingMenuDisabled(node, inspector);
+  }
+});
--- a/browser/devtools/markupview/test/doc_markup_anonymous.html
+++ b/browser/devtools/markupview/test/doc_markup_anonymous.html
@@ -15,16 +15,18 @@
     }
   </style>
 </head>
 <body>
   <div id="pseudo"><span>middle</span></div>
 
   <div id="shadow">light dom</div>
 
+  <div id="native"><video controls></video></div>
+
   <script>
   var host = document.querySelector('#shadow');
   if (host.createShadowRoot) {
     var root = host.createShadowRoot();
     root.innerHTML = '<h3>Shadow DOM</h3><select multiple></select>';
   }
   </script>
 </body>
--- a/browser/devtools/markupview/test/head.js
+++ b/browser/devtools/markupview/test/head.js
@@ -27,16 +27,17 @@ registerCleanupFunction(() => gDevTools.
 // Clear preferences that may be set during the course of tests.
 registerCleanupFunction(() => {
   Services.prefs.clearUserPref("devtools.inspector.htmlPanelOpen");
   Services.prefs.clearUserPref("devtools.inspector.sidebarOpen");
   Services.prefs.clearUserPref("devtools.inspector.activeSidebar");
   Services.prefs.clearUserPref("devtools.dump.emit");
   Services.prefs.clearUserPref("devtools.markup.pagesize");
   Services.prefs.clearUserPref("dom.webcomponents.enabled");
+  Services.prefs.clearUserPref("devtools.inspector.showAllAnonymousContent");
 });
 
 // Auto close the toolbox and close the test tabs when the test ends
 registerCleanupFunction(function*() {
   let target = TargetFactory.forTab(gBrowser.selectedTab);
   yield gDevTools.closeToolbox(target);
 
   while (gBrowser.tabs.length > 1) {
--- a/browser/devtools/performance/modules/io.js
+++ b/browser/devtools/performance/modules/io.js
@@ -13,17 +13,18 @@ loader.lazyImporter(this, "FileUtils",
 loader.lazyImporter(this, "NetUtil",
   "resource://gre/modules/NetUtil.jsm");
 
 // This identifier string is used to tentatively ascertain whether or not
 // a JSON loaded from disk is actually something generated by this tool.
 // It isn't, of course, a definitive verification, but a Good Enough™
 // approximation before continuing the import. Don't localize this.
 const PERF_TOOL_SERIALIZER_IDENTIFIER = "Recorded Performance Data";
-const PERF_TOOL_SERIALIZER_VERSION = 1;
+const PERF_TOOL_SERIALIZER_LEGACY_VERSION = 1;
+const PERF_TOOL_SERIALIZER_CURRENT_VERSION = 2;
 
 /**
  * Helpers for importing/exporting JSON.
  */
 let PerformanceIO = {
   /**
    * Gets a nsIScriptableUnicodeConverter instance with a default UTF-8 charset.
    * @return object
@@ -46,17 +47,17 @@ let PerformanceIO = {
    * @return object
    *         A promise that is resolved once streaming finishes, or rejected
    *         if there was an error.
    */
   saveRecordingToFile: function(recordingData, file) {
     let deferred = promise.defer();
 
     recordingData.fileType = PERF_TOOL_SERIALIZER_IDENTIFIER;
-    recordingData.version = PERF_TOOL_SERIALIZER_VERSION;
+    recordingData.version = PERF_TOOL_SERIALIZER_CURRENT_VERSION;
 
     let string = JSON.stringify(recordingData);
     let inputStream = this.getUnicodeConverter().convertToInputStream(string);
     let outputStream = FileUtils.openSafeFileOutputStream(file);
 
     NetUtil.asyncCopy(inputStream, outputStream, deferred.resolve);
     return deferred.promise;
   },
@@ -83,20 +84,69 @@ let PerformanceIO = {
       } catch (e) {
         deferred.reject(new Error("Could not read recording data file."));
         return;
       }
       if (recordingData.fileType != PERF_TOOL_SERIALIZER_IDENTIFIER) {
         deferred.reject(new Error("Unrecognized recording data file."));
         return;
       }
-      if (recordingData.version != PERF_TOOL_SERIALIZER_VERSION) {
+      if (!isValidSerializerVersion(recordingData.version)) {
         deferred.reject(new Error("Unsupported recording data file version."));
         return;
       }
+      if (recordingData.version === PERF_TOOL_SERIALIZER_LEGACY_VERSION) {
+        recordingData = convertLegacyData(recordingData);
+      }
       deferred.resolve(recordingData);
     });
 
     return deferred.promise;
   }
 };
 
 exports.PerformanceIO = PerformanceIO;
+
+/**
+ * Returns a boolean indicating whether or not the passed in `version`
+ * is supported by this serializer.
+ *
+ * @param number version
+ * @return boolean
+ */
+function isValidSerializerVersion (version) {
+  return !!~[
+    PERF_TOOL_SERIALIZER_LEGACY_VERSION,
+    PERF_TOOL_SERIALIZER_CURRENT_VERSION
+  ].indexOf(version);
+}
+
+
+/**
+ * Takes recording data (with version `1`, from the original profiler tool), and
+ * massages the data to be line with the current performance tool's property names
+ * and values.
+ *
+ * @param object legacyData
+ * @return object
+ */
+function convertLegacyData (legacyData) {
+  let { profilerData, ticksData, recordingDuration } = legacyData;
+
+  // The `profilerData` stays, and the previously unrecorded fields
+  // just are empty arrays.
+  let data = {
+    markers: [],
+    frames: [],
+    memory: [],
+    ticks: ticksData,
+    profilerData: profilerData,
+    // Data from the original profiler won't contain `interval` fields,
+    // but a recording duration, as well as the current time, which can be used
+    // to infer the interval startTime and endTime.
+    interval: {
+      startTime: profilerData.currentTime - recordingDuration,
+      endTime: profilerData.currentTime
+    }
+  };
+
+  return data;
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/modules/recording-model.js
@@ -0,0 +1,248 @@
+/* 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 { PerformanceIO } = require("devtools/performance/io");
+
+const RECORDING_IN_PROGRESS = exports.RECORDING_IN_PROGRESS = -1;
+const RECORDING_UNAVAILABLE = exports.RECORDING_UNAVAILABLE = null;
+/**
+ * Model for a wholistic profile, containing start/stop times, profiling data, frames data,
+ * timeline (marker, tick, memory) data, and methods to start/stop recording.
+ */
+
+const RecordingModel = function (options={}) {
+  this._front = options.front;
+  this._performance = options.performance;
+  this._label = options.label || "";
+};
+
+RecordingModel.prototype = {
+  _localStartTime: RECORDING_UNAVAILABLE,
+  _startTime: RECORDING_UNAVAILABLE,
+  _endTime: RECORDING_UNAVAILABLE,
+  _markers: [],
+  _frames: [],
+  _ticks: [],
+  _memory: [],
+  _profilerData: {},
+  _label: "",
+  _imported: false,
+  _isRecording: false,
+
+  /**
+   * Loads a recording from a file.
+   *
+   * @param nsILocalFile file
+   *        The file to import the data form.
+   */
+  importRecording: Task.async(function *(file) {
+    let recordingData = yield PerformanceIO.loadRecordingFromFile(file);
+
+    this._imported = true;
+    this._label = recordingData.profilerData.profilerLabel || "";
+    this._startTime = recordingData.interval.startTime;
+    this._endTime = recordingData.interval.endTime;
+    this._markers = recordingData.markers;
+    this._frames = recordingData.frames;
+    this._memory = recordingData.memory;
+    this._ticks = recordingData.ticks;
+    this._profilerData = recordingData.profilerData;
+
+    return recordingData;
+  }),
+
+  /**
+   * Saves the current recording to a file.
+   *
+   * @param nsILocalFile file
+   *        The file to stream the data into.
+   */
+  exportRecording: Task.async(function *(file) {
+    let recordingData = this.getAllData();
+    yield PerformanceIO.saveRecordingToFile(recordingData, file);
+  }),
+
+  /**
+   * Starts recording with the PerformanceFront, storing the start times
+   * on the model.
+   */
+  startRecording: Task.async(function *() {
+    // Times must come from the actor in order to be self-consistent.
+    // However, we also want to update the view with the elapsed time
+    // even when the actor is not generating data. To do this we get
+    // the local time and use it to compute a reasonable elapsed time.
+    this._localStartTime = this._performance.now();
+
+    let { startTime } = yield this._front.startRecording({
+      withTicks: true,
+      withMemory: true
+    });
+    this._isRecording = true;
+
+    this._startTime = startTime;
+    this._endTime = RECORDING_IN_PROGRESS;
+    this._markers = [];
+    this._frames = [];
+    this._memory = [];
+    this._ticks = [];
+  }),
+
+  /**
+   * Stops recording with the PerformanceFront, storing the end times
+   * on the model.
+   */
+  stopRecording: Task.async(function *() {
+    let results = yield this._front.stopRecording();
+    this._isRecording = false;
+
+    // If `endTime` is not yielded from timeline actor (< Fx36), fake it.
+    if (!results.endTime) {
+      results.endTime = this._startTime + this.getLocalElapsedTime();
+    }
+
+    this._endTime = results.endTime;
+    this._profilerData = results.profilerData;
+    this._markers = this._markers.sort((a,b) => (a.start > b.start));
+
+    return results;
+  }),
+
+  /**
+   * Returns the profile's label, from `console.profile(LABEL)`.
+   */
+  getLabel: function () {
+    return this._label;
+  },
+
+  /**
+   * Gets the amount of time elapsed locally after starting a recording.
+   */
+  getLocalElapsedTime: function () {
+    return this._performance.now() - this._localStartTime;
+  },
+
+  /**
+   * Returns duration of this recording, in milliseconds.
+   */
+  getDuration: function () {
+    let { startTime, endTime } = this.getInterval();
+    return endTime - startTime;
+  },
+
+  /**
+   * Gets the time interval for the current recording.
+   * @return object
+   */
+  getInterval: function() {
+    let startTime = this._startTime;
+    let endTime = this._endTime;
+
+    // Compute an approximate ending time for the current recording. This is
+    // needed to ensure that the view updates even when new data is
+    // not being generated.
+    if (endTime == RECORDING_IN_PROGRESS) {
+      endTime = startTime + this.getLocalElapsedTime();
+    }
+
+    return { startTime, endTime };
+  },
+
+  /**
+   * Gets the accumulated markers in the current recording.
+   * @return array
+   */
+  getMarkers: function() {
+    return this._markers;
+  },
+
+  /**
+   * Gets the accumulated stack frames in the current recording.
+   * @return array
+   */
+  getFrames: function() {
+    return this._frames;
+  },
+
+  /**
+   * Gets the accumulated memory measurements in this recording.
+   * @return array
+   */
+  getMemory: function() {
+    return this._memory;
+  },
+
+  /**
+   * Gets the accumulated refresh driver ticks in this recording.
+   * @return array
+   */
+  getTicks: function() {
+    return this._ticks;
+  },
+
+  /**
+   * Gets the profiler data in this recording.
+   * @return array
+   */
+  getProfilerData: function() {
+    return this._profilerData;
+  },
+
+  /**
+   * Gets all the data in this recording.
+   */
+  getAllData: function() {
+    let interval = this.getInterval();
+    let markers = this.getMarkers();
+    let frames = this.getFrames();
+    let memory = this.getMemory();
+    let ticks = this.getTicks();
+    let profilerData = this.getProfilerData();
+    return { interval, markers, frames, memory, ticks, profilerData };
+  },
+
+  /**
+   * Returns a boolean indicating whether or not this recording model
+   * is recording.
+   */
+  isRecording: function () {
+    return this._isRecording;
+  },
+
+  /**
+   * Fired whenever the PerformanceFront emits markers, memory or ticks.
+   */
+  addTimelineData: function (eventName, ...data) {
+    // If this model isn't currently recording,
+    // ignore the timeline data.
+    if (!this.isRecording()) {
+      return;
+    }
+
+    switch (eventName) {
+      // Accumulate markers into an array.
+      case "markers":
+        let [markers] = data;
+        Array.prototype.push.apply(this._markers, markers);
+        break;
+      // Accumulate stack frames into an array.
+      case "frames":
+        let [, frames] = data;
+        Array.prototype.push.apply(this._frames, frames);
+        break;
+      // Accumulate memory measurements into an array.
+      case "memory":
+        let [delta, measurement] = data;
+        this._memory.push({ delta, value: measurement.total / 1024 / 1024 });
+        break;
+      // Save the accumulated refresh driver ticks.
+      case "ticks":
+        let [, timestamps] = data;
+        this._ticks = timestamps;
+        break;
+    }
+  }
+};
+
+exports.RecordingModel = RecordingModel;
--- a/browser/devtools/performance/moz.build
+++ b/browser/devtools/performance/moz.build
@@ -1,12 +1,13 @@
 # vim: set filetype=python:
 # 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/.
 
 EXTRA_JS_MODULES.devtools.performance += [
     'modules/front.js',
     'modules/io.js',
+    'modules/recording-model.js',
     'panel.js'
 ]
 
 BROWSER_CHROME_MANIFESTS += ['test/browser.ini']
--- a/browser/devtools/performance/performance-controller.js
+++ b/browser/devtools/performance/performance-controller.js
@@ -36,30 +36,40 @@ devtools.lazyRequireGetter(this, "CallVi
 devtools.lazyRequireGetter(this, "ThreadNode",
   "devtools/profiler/tree-model", true);
 
 
 devtools.lazyImporter(this, "CanvasGraphUtils",
   "resource:///modules/devtools/Graphs.jsm");
 devtools.lazyImporter(this, "LineGraphWidget",
   "resource:///modules/devtools/Graphs.jsm");
+devtools.lazyImporter(this, "SideMenuWidget",
+  "resource:///modules/devtools/SideMenuWidget.jsm");
+
+const { RecordingModel, RECORDING_IN_PROGRESS, RECORDING_UNAVAILABLE } =
+  devtools.require("devtools/performance/recording-model");
 
 devtools.lazyImporter(this, "FlameGraphUtils",
   "resource:///modules/devtools/FlameGraph.jsm");
 devtools.lazyImporter(this, "FlameGraph",
   "resource:///modules/devtools/FlameGraph.jsm");
 
 // Events emitted by various objects in the panel.
 const EVENTS = {
+  // Emitted by the PerformanceController or RecordingView
+  // when a recording model is selected
+  RECORDING_SELECTED: "Performance:RecordingSelected",
+
   // Emitted by the PerformanceView on record button click
   UI_START_RECORDING: "Performance:UI:StartRecording",
   UI_STOP_RECORDING: "Performance:UI:StopRecording",
 
-  // Emitted by the PerformanceView on import or export button click
+  // Emitted by the PerformanceView on import button click
   UI_IMPORT_RECORDING: "Performance:UI:ImportRecording",
+  // Emitted by the RecordingsView on export button click
   UI_EXPORT_RECORDING: "Performance:UI:ExportRecording",
 
   // When a recording is started or stopped via the PerformanceController
   RECORDING_STARTED: "Performance:RecordingStarted",
   RECORDING_STOPPED: "Performance:RecordingStopped",
 
   // When a recording is imported or exported via the PerformanceController
   RECORDING_IMPORTED: "Performance:RecordingImported",
@@ -91,21 +101,16 @@ const EVENTS = {
 
   // Emitted by the WaterfallView when it has been rendered
   WATERFALL_RENDERED: "Performance:UI:WaterfallRendered",
 
   // Emitted by the FlameGraphView when it has been rendered
   FLAMEGRAPH_RENDERED: "Performance:UI:FlameGraphRendered"
 };
 
-// Constant defining the end time for a recording that hasn't finished
-// or is not yet available.
-const RECORDING_IN_PROGRESS = -1;
-const RECORDING_UNAVAILABLE = null;
-
 /**
  * The current target and the profiler connection, set by this tool's host.
  */
 let gToolbox, gTarget, gFront;
 
 /**
  * Initializes the profiler controller and views.
  */
@@ -145,253 +150,228 @@ let PrefObserver = {
   }
 };
 
 /**
  * Functions handling target-related lifetime events and
  * UI interaction.
  */
 let PerformanceController = {
-  /**
-   * Permanent storage for the markers and the memory measurements streamed by
-   * the backend, along with the start and end timestamps.
-   */
-  _localStartTime: RECORDING_UNAVAILABLE,
-  _startTime: RECORDING_UNAVAILABLE,
-  _endTime: RECORDING_UNAVAILABLE,
-  _markers: [],
-  _frames: [],
-  _memory: [],
-  _ticks: [],
-  _profilerData: {},
+  _recordings: [],
+  _currentRecording: null,
 
   /**
    * 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);
     this.importRecording = this.importRecording.bind(this);
     this.exportRecording = this.exportRecording.bind(this);
     this._onTimelineData = this._onTimelineData.bind(this);
+    this._onRecordingSelectFromView = this._onRecordingSelectFromView.bind(this);
 
     PerformanceView.on(EVENTS.UI_START_RECORDING, this.startRecording);
     PerformanceView.on(EVENTS.UI_STOP_RECORDING, this.stopRecording);
-    PerformanceView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
     PerformanceView.on(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
+    RecordingsView.on(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
+    RecordingsView.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView);
 
     gFront.on("ticks", this._onTimelineData); // framerate
     gFront.on("markers", this._onTimelineData); // timeline markers
     gFront.on("frames", this._onTimelineData); // stack frames
     gFront.on("memory", this._onTimelineData); // timeline memory
   },
 
   /**
    * Remove events handled by the PerformanceController
    */
   destroy: function() {
     PerformanceView.off(EVENTS.UI_START_RECORDING, this.startRecording);
     PerformanceView.off(EVENTS.UI_STOP_RECORDING, this.stopRecording);
-    PerformanceView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
     PerformanceView.off(EVENTS.UI_IMPORT_RECORDING, this.importRecording);
+    RecordingsView.off(EVENTS.UI_EXPORT_RECORDING, this.exportRecording);
+    RecordingsView.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelectFromView);
 
     gFront.off("ticks", this._onTimelineData);
     gFront.off("markers", this._onTimelineData);
     gFront.off("frames", this._onTimelineData);
     gFront.off("memory", this._onTimelineData);
   },
 
   /**
    * Starts recording with the PerformanceFront. Emits `EVENTS.RECORDING_STARTED`
    * when the front has started to record.
    */
   startRecording: Task.async(function *() {
-    // Times must come from the actor in order to be self-consistent.
-    // However, we also want to update the view with the elapsed time
-    // even when the actor is not generating data. To do this we get
-    // the local time and use it to compute a reasonable elapsed time.
-    this._localStartTime = performance.now();
+    let model = this.createNewRecording();
+    this.setCurrentRecording(model);
+    yield model.startRecording();
 
-    let { startTime } = yield gFront.startRecording({
-      withTicks: true,
-      withMemory: true
-    });
-
-    this._startTime = startTime;
-    this._endTime = RECORDING_IN_PROGRESS;
-    this._markers = [];
-    this._frames = [];
-    this._memory = [];
-    this._ticks = [];
-
-    this.emit(EVENTS.RECORDING_STARTED);
+    this.emit(EVENTS.RECORDING_STARTED, model);
   }),
 
   /**
    * Stops recording with the PerformanceFront. Emits `EVENTS.RECORDING_STOPPED`
    * when the front has stopped recording.
    */
   stopRecording: Task.async(function *() {
-    let results = yield gFront.stopRecording();
+    let recording = this._getLatest();
+    yield recording.stopRecording();
 
-    // If `endTime` is not yielded from timeline actor (< Fx36), fake it.
-    if (!results.endTime) {
-      results.endTime = this._startTime + this.getLocalElapsedTime();
-    }
-
-    this._endTime = results.endTime;
-    this._profilerData = results.profilerData;
-    this._markers = this._markers.sort((a,b) => (a.start > b.start));
-
-    this.emit(EVENTS.RECORDING_STOPPED);
+    this.emit(EVENTS.RECORDING_STOPPED, recording);
   }),
 
   /**
    * Saves the current recording to a file.
    *
+   * @param RecordingModel recording
+   *        The model that holds the recording data.
    * @param nsILocalFile file
    *        The file to stream the data into.
    */
-  exportRecording: Task.async(function*(_, file) {
-    let recordingData = this.getAllData();
+  exportRecording: Task.async(function*(_, recording, file) {
+    let recordingData = recording.getAllData();
     yield PerformanceIO.saveRecordingToFile(recordingData, file);
 
     this.emit(EVENTS.RECORDING_EXPORTED, recordingData);
   }),
 
   /**
-   * Loads a recording from a file, replacing the current one.
-   * XXX: Handle multiple recordings, bug 1111004.
+   * Loads a recording from a file, adding it to the recordings list.
    *
    * @param nsILocalFile file
    *        The file to import the data from.
    */
   importRecording: Task.async(function*(_, file) {
-    let recordingData = yield PerformanceIO.loadRecordingFromFile(file);
+    let model = this.createNewRecording();
+    yield model.importRecording(file);
+
+    this.emit(EVENTS.RECORDING_IMPORTED, model.getAllData(), model);
+  }),
 
-    this._startTime = recordingData.interval.startTime;
-    this._endTime = recordingData.interval.endTime;
-    this._markers = recordingData.markers;
-    this._frames = recordingData.frames;
-    this._memory = recordingData.memory;
-    this._ticks = recordingData.ticks;
-    this._profilerData = recordingData.profilerData;
+  /**
+   * Creates a new RecordingModel, fires events and stores it
+   * internally in the controller.
+   */
+  createNewRecording: function () {
+    let model = new RecordingModel({
+      front: gFront,
+      performance: performance
+    });
+    this._recordings.push(model);
+    this.emit(EVENTS.RECORDING_CREATED, model);
+    return model;
+  },
 
-    this.emit(EVENTS.RECORDING_IMPORTED, recordingData);
+  /**
+   * Sets the active RecordingModel to `recording`.
+   */
+  setCurrentRecording: function (recording) {
+    if (this._currentRecording !== recording) {
+      this._currentRecording = recording;
+      this.emit(EVENTS.RECORDING_SELECTED, recording);
+    }
+  },
 
-    // Flush the current recording.
-    this.emit(EVENTS.RECORDING_STARTED);
-    this.emit(EVENTS.RECORDING_STOPPED);
-  }),
+  /**
+   * Return the current active RecordingModel.
+   */
+  getCurrentRecording: function () {
+    return this._currentRecording;
+  },
 
   /**
    * Gets the amount of time elapsed locally after starting a recording.
    */
-  getLocalElapsedTime: function() {
-    return performance.now() - this._localStartTime;
+  getLocalElapsedTime: function () {
+    return this.getCurrentRecording().getLocalElapsedTime;
   },
 
   /**
    * Gets the time interval for the current recording.
    * @return object
    */
   getInterval: function() {
-    let startTime = this._startTime;
-    let endTime = this._endTime;
-
-    // Compute an approximate ending time for the current recording. This is
-    // needed to ensure that the view updates even when new data is
-    // not being generated.
-    if (endTime == RECORDING_IN_PROGRESS) {
-      endTime = startTime + this.getLocalElapsedTime();
-    }
-
-    return { startTime, endTime };
+    return this.getCurrentRecording().getInterval();
   },
 
   /**
    * Gets the accumulated markers in the current recording.
    * @return array
    */
   getMarkers: function() {
-    return this._markers;
+    return this.getCurrentRecording().getMarkers();
   },
 
   /**
    * Gets the accumulated stack frames in the current recording.
    * @return array
    */
   getFrames: function() {
-    return this._frames;
+    return this.getCurrentRecording().getFrames();
   },
 
   /**
    * Gets the accumulated memory measurements in this recording.
    * @return array
    */
   getMemory: function() {
-    return this._memory;
+    return this.getCurrentRecording().getMemory();
   },
 
   /**
    * Gets the accumulated refresh driver ticks in this recording.
    * @return array
    */
   getTicks: function() {
-    return this._ticks;
+    return this.getCurrentRecording().getTicks();
   },
 
   /**
    * Gets the profiler data in this recording.
    * @return array
    */
   getProfilerData: function() {
-    return this._profilerData;
+    return this.getCurrentRecording().getProfilerData();
   },
 
   /**
    * Gets all the data in this recording.
    */
   getAllData: function() {
-    let interval = this.getInterval();
-    let markers = this.getMarkers();
-    let frames = this.getFrames();
-    let memory = this.getMemory();
-    let ticks = this.getTicks();
-    let profilerData = this.getProfilerData();
-    return { interval, markers, frames, memory, ticks, profilerData };
+    return this.getCurrentRecording().getAllData();
+  },
+
+  /**
+  /**
+   * Get most recently added profile that was triggered manually (via UI)
+   */
+  _getLatest: function () {
+    for (let i = this._recordings.length - 1; i >= 0; i--) {
+      return this._recordings[i];
+    }
+    return null;
   },
 
   /**
    * Fired whenever the PerformanceFront emits markers, memory or ticks.
    */
-  _onTimelineData: function (eventName, ...data) {
-    // Accumulate markers into an array.
-    if (eventName == "markers") {
-      let [markers] = data;
-      Array.prototype.push.apply(this._markers, markers);
-    }
-    // Accumulate stack frames into an array.
-    else if (eventName == "frames") {
-      let [delta, frames] = data;
-      Array.prototype.push.apply(this._frames, frames);
-    }
-    // Accumulate memory measurements into an array.
-    else if (eventName == "memory") {
-      let [delta, measurement] = data;
-      this._memory.push({ delta, value: measurement.total / 1024 / 1024 });
-    }
-    // Save the accumulated refresh driver ticks.
-    else if (eventName == "ticks") {
-      let [delta, timestamps] = data;
-      this._ticks = timestamps;
-    }
+  _onTimelineData: function (...data) {
+    this._recordings.forEach(profile => profile.addTimelineData.apply(profile, data));
+    this.emit(EVENTS.TIMELINE_DATA, ...data);
+  },
 
-    this.emit(EVENTS.TIMELINE_DATA, eventName, ...data);
+  /**
+   * Fired from RecordingsView, we listen on the PerformanceController
+   * so we can set it here and re-emit on the controller, where all views can listen.
+   */
+  _onRecordingSelectFromView: function (_, recording) {
+    this.setCurrentRecording(recording);
   }
 };
 
 /**
  * Convenient way of emitting events from the controller.
  */
 EventEmitter.decorate(PerformanceController);
 
--- a/browser/devtools/performance/performance-view.js
+++ b/browser/devtools/performance/performance-view.js
@@ -8,50 +8,48 @@
  */
 let PerformanceView = {
   /**
    * Sets up the view with event binding and main subviews.
    */
   initialize: function () {
     this._recordButton = $("#record-button");
     this._importButton = $("#import-button");
-    this._exportButton = $("#export-button");
 
     this._onRecordButtonClick = this._onRecordButtonClick.bind(this);
     this._onImportButtonClick = this._onImportButtonClick.bind(this);
-    this._onExportButtonClick = this._onExportButtonClick.bind(this);
     this._lockRecordButton = this._lockRecordButton.bind(this);
     this._unlockRecordButton = this._unlockRecordButton.bind(this);
 
     this._recordButton.addEventListener("click", this._onRecordButtonClick);
     this._importButton.addEventListener("click", this._onImportButtonClick);
-    this._exportButton.addEventListener("click", this._onExportButtonClick);
 
     // Bind to controller events to unlock the record button
     PerformanceController.on(EVENTS.RECORDING_STARTED, this._unlockRecordButton);
     PerformanceController.on(EVENTS.RECORDING_STOPPED, this._unlockRecordButton);
 
     return promise.all([
+      RecordingsView.initialize(),
       OverviewView.initialize(),
       DetailsView.initialize()
     ]);
   },
 
   /**
    * Unbinds events and destroys subviews.
    */
   destroy: function () {
     this._recordButton.removeEventListener("click", this._onRecordButtonClick);
     this._importButton.removeEventListener("click", this._onImportButtonClick);
-    this._exportButton.removeEventListener("click", this._onExportButtonClick);
 
     PerformanceController.off(EVENTS.RECORDING_STARTED, this._unlockRecordButton);
     PerformanceController.off(EVENTS.RECORDING_STOPPED, this._unlockRecordButton);
 
     return promise.all([
+      RecordingsView.destroy(),
       OverviewView.destroy(),
       DetailsView.destroy()
     ]);
   },
 
   /**
    * Adds the `locked` attribute on the record button. This prevents it
    * from being clicked while recording is started or stopped.
@@ -89,32 +87,15 @@ let PerformanceView = {
     let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
     fp.init(window, L10N.getStr("recordingsList.saveDialogTitle"), Ci.nsIFilePicker.modeOpen);
     fp.appendFilter(L10N.getStr("recordingsList.saveDialogJSONFilter"), "*.json");
     fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*");
 
     if (fp.show() == Ci.nsIFilePicker.returnOK) {
       this.emit(EVENTS.UI_IMPORT_RECORDING, fp.file);
     }
-  },
-
-  /**
-   * Handler for clicking the export button.
-   */
-  _onExportButtonClick: function(e) {
-    let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
-    fp.init(window, L10N.getStr("recordingsList.saveDialogTitle"), Ci.nsIFilePicker.modeSave);
-    fp.appendFilter(L10N.getStr("recordingsList.saveDialogJSONFilter"), "*.json");
-    fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*");
-    fp.defaultString = "profile.json";
-
-    fp.open({ done: result => {
-      if (result != Ci.nsIFilePicker.returnCancel) {
-        this.emit(EVENTS.UI_EXPORT_RECORDING, fp.file);
-      }
-    }});
   }
 };
 
 /**
  * Convenient way of emitting events from the view.
  */
 EventEmitter.decorate(PerformanceView);
--- a/browser/devtools/performance/performance.xul
+++ b/browser/devtools/performance/performance.xul
@@ -11,98 +11,101 @@
   <!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/performance-controller.js"/>
   <script type="application/javascript" src="performance/performance-view.js"/>
+  <script type="application/javascript" src="performance/recording-model.js"/>
   <script type="application/javascript" src="performance/views/overview.js"/>
   <script type="application/javascript" src="performance/views/details.js"/>
   <script type="application/javascript" src="performance/views/details-call-tree.js"/>
   <script type="application/javascript" src="performance/views/details-waterfall.js"/>
   <script type="application/javascript" src="performance/views/details-flamegraph.js"/>
+  <script type="application/javascript" src="performance/views/recordings.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;"/>
-      </hbox>
-      <hbox id="performance-toolbar-controls-detail-views" class="devtools-toolbarbutton-group">
-        <toolbarbutton id="select-waterfall-view"
-                       class="devtools-toolbarbutton"
-                       data-view="waterfall" />
-        <toolbarbutton id="select-calltree-view"
-                       class="devtools-toolbarbutton"
-                       data-view="calltree" />
-        <toolbarbutton id="select-flamegraph-view"
-                       class="devtools-toolbarbutton"
-                       data-view="flamegraph" />
-      </hbox>
-      <spacer flex="1"></spacer>
-      <hbox id="performance-toolbar-controls-storage" class="devtools-toolbarbutton-group">
-        <toolbarbutton id="import-button"
-                       class="devtools-toolbarbutton"
-                       label="&profilerUI.importButton;"/>
-        <toolbarbutton id="export-button"
-                       class="devtools-toolbarbutton"
-                       label="&profilerUI.exportButton;"/>
-        <toolbarbutton id="clear-button"
-                       class="devtools-toolbarbutton"
-                       label="&profilerUI.clearButton;"/>
-      </hbox>
-    </toolbar>
-
-    <vbox id="overview-pane">
-      <hbox id="markers-overview"/>
-      <hbox id="memory-overview"/>
-      <hbox id="time-framerate"/>
+  <hbox class="theme-body" flex="1">
+    <vbox id="recordings-pane">
+      <toolbar id="recordings-toolbar"
+               class="devtools-toolbar">
+        <hbox id="recordings-controls"
+              class="devtools-toolbarbutton-group">
+          <toolbarbutton id="record-button"
+                         class="devtools-toolbarbutton"
+                         tooltiptext="&profilerUI.recordButton.tooltip;"/>
+          <toolbarbutton id="import-button"
+                         class="devtools-toolbarbutton"
+                         label="&profilerUI.importButton;"/>
+          <toolbarbutton id="clear-button"
+                         class="devtools-toolbarbutton"
+                         label="&profilerUI.clearButton;"/>
+        </hbox>
+      </toolbar>
+      <vbox id="recordings-list" flex="1"/>
     </vbox>
-
-    <deck id="details-pane" flex="1">
+    <vbox flex="1">
+      <toolbar id="performance-toolbar" class="devtools-toolbar">
+        <hbox id="performance-toolbar-controls-detail-views" class="devtools-toolbarbutton-group">
+          <toolbarbutton id="select-waterfall-view"
+                         class="devtools-toolbarbutton"
+                         data-view="waterfall" />
+          <toolbarbutton id="select-calltree-view"
+                         class="devtools-toolbarbutton"
+                         data-view="calltree" />
+          <toolbarbutton id="select-flamegraph-view"
+                         class="devtools-toolbarbutton"
+                         data-view="flamegraph" />
+        </hbox>
+        <spacer flex="1"></spacer>
+      </toolbar>
 
-      <hbox id="waterfall-view" flex="1">
-        <vbox id="waterfall-breakdown" flex="1" />
-        <splitter class="devtools-side-splitter"/>
-        <vbox id="waterfall-details"
-              class="theme-sidebar"
-              width="150"
-              height="150"/>
-      </hbox>
+      <vbox id="overview-pane">
+        <hbox id="markers-overview"/>
+        <hbox id="memory-overview"/>
+        <hbox id="time-framerate"/>
+      </vbox>
+      <deck id="details-pane" flex="1">
+        <hbox id="waterfall-view" flex="1">
+          <vbox id="waterfall-breakdown" flex="1" />
+          <splitter class="devtools-side-splitter"/>
+          <vbox id="waterfall-details"
+                class="theme-sidebar"
+                width="150"
+                height="150"/>
+        </hbox>
 
-      <vbox id="calltree-view" flex="1">
-        <hbox class="call-tree-headers-container">
-          <label class="plain call-tree-header"
-                 type="duration"
-                 crop="end"
-                 value="&profilerUI.table.totalDuration;"/>
-          <label class="plain call-tree-header"
-                 type="percentage"
-                 crop="end"
-                 value="&profilerUI.table.totalPercentage;"/>
-          <label class="plain call-tree-header"
-                 type="self-duration"
-                 crop="end"
-                 value="&profilerUI.table.selfDuration;"/>
-          <label class="plain call-tree-header"
-                 type="self-percentage"
-                 crop="end"
-                 value="&profilerUI.table.selfPercentage;"/>
-          <label class="plain call-tree-header"
-                 type="samples"
-                 crop="end"
-                 value="&profilerUI.table.samples;"/>
-          <label class="plain call-tree-header"
-                 type="function"
-                 crop="end"
-                 value="&profilerUI.table.function;"/>
+        <vbox id="calltree-view" flex="1">
+          <hbox class="call-tree-headers-container">
+            <label class="plain call-tree-header"
+                   type="duration"
+                   crop="end"
+                   value="&profilerUI.table.totalDuration;"/>
+            <label class="plain call-tree-header"
+                   type="percentage"
+                   crop="end"
+                   value="&profilerUI.table.totalPercentage;"/>
+            <label class="plain call-tree-header"
+                   type="self-duration"
+                   crop="end"
+                   value="&profilerUI.table.selfDuration;"/>
+            <label class="plain call-tree-header"
+                   type="self-percentage"
+                   crop="end"
+                   value="&profilerUI.table.selfPercentage;"/>
+            <label class="plain call-tree-header"
+                   type="samples"
+                   crop="end"
+                   value="&profilerUI.table.samples;"/>
+            <label class="plain call-tree-header"
+                   type="function"
+                   crop="end"
+                   value="&profilerUI.table.function;"/>
+          </hbox>
+          <vbox class="call-tree-cells-container" flex="1"/>
+        </vbox>
+        <hbox id="flamegraph-view" flex="1">
         </hbox>
-        <vbox class="call-tree-cells-container" flex="1"/>
-      </vbox>
-
-      <hbox id="flamegraph-view" flex="1">
-      </hbox>
-    </deck>
-  </vbox>
+      </deck>
+    </vbox>
+  </hbox>
 </window>
--- a/browser/devtools/performance/test/browser.ini
+++ b/browser/devtools/performance/test/browser.ini
@@ -34,8 +34,12 @@ support-files =
 [browser_perf-overview-selection-03.js]
 [browser_perf-shared-connection-02.js]
 [browser_perf-shared-connection-03.js]
 # [browser_perf-shared-connection-04.js] bug 1077464
 [browser_perf-ui-recording.js]
 [browser_perf_recordings-io-01.js]
 [browser_perf_recordings-io-02.js]
 [browser_perf_recordings-io-03.js]
+[browser_perf-recording-selected-01.js]
+[browser_perf-recording-selected-02.js]
+[browser_perf-recording-selected-03.js]
+[browser_perf_recordings-io-04.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-recording-selected-01.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the profiler correctly handles multiple recordings and can
+ * successfully switch between them.
+ */
+
+let test = Task.async(function*() {
+  let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
+  let { EVENTS, PerformanceController, RecordingsView } = panel.panelWin;
+
+  yield startRecording(panel);
+  yield stopRecording(panel);
+
+  yield startRecording(panel);
+  yield stopRecording(panel);
+
+  is(RecordingsView.itemCount, 2,
+    "There should be two recordings visible.");
+  is(RecordingsView.selectedIndex, 1,
+    "The second recording item should be selected.");
+
+  let select = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+  RecordingsView.selectedIndex = 0;
+  yield select;
+
+  is(RecordingsView.itemCount, 2,
+    "There should still be two recordings visible.");
+  is(RecordingsView.selectedIndex, 0,
+    "The first recording item should be selected.");
+
+  yield teardown(panel);
+  finish();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-recording-selected-02.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the profiler correctly handles multiple recordings and can
+ * successfully switch between them, even when one of them is in progress.
+ */
+
+let test = Task.async(function*() {
+  let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
+  let { EVENTS, PerformanceController, RecordingsView } = panel.panelWin;
+
+  yield startRecording(panel);
+  yield stopRecording(panel);
+
+  yield startRecording(panel);
+
+  is(RecordingsView.itemCount, 2,
+    "There should be two recordings visible.");
+  is(RecordingsView.selectedIndex, 1,
+    "The new recording item should be selected.");
+
+  let select = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+  RecordingsView.selectedIndex = 0;
+  yield select;
+
+  is(RecordingsView.itemCount, 2,
+    "There should still be two recordings visible.");
+  is(RecordingsView.selectedIndex, 0,
+    "The first recording item should be selected now.");
+
+  select = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+  RecordingsView.selectedIndex = 1;
+  yield select;
+
+  is(RecordingsView.itemCount, 2,
+    "There should still be two recordings visible.");
+  is(RecordingsView.selectedIndex, 1,
+    "The second recording item should be selected again.");
+
+  yield stopRecording(panel);
+
+  yield teardown(panel);
+  finish();
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf-recording-selected-03.js
@@ -0,0 +1,33 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the profiler UI does not forget that recording is active when
+ * selected recording changes. Bug 1060885.
+ */
+
+let test = Task.async(function*() {
+  let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
+  let { $, EVENTS, PerformanceController, RecordingsView } = panel.panelWin;
+
+  yield startRecording(panel);
+  yield stopRecording(panel);
+
+  yield startRecording(panel);
+
+  info("Selecting recording #0 and waiting for it to be displayed.");
+  let select = once(PerformanceController, EVENTS.RECORDING_SELECTED);
+  RecordingsView.selectedIndex = 0;
+  yield select;
+
+  ok($("#record-button").hasAttribute("checked"),
+    "Button is still checked after selecting another item.");
+
+  ok(!$("#record-button").hasAttribute("locked"),
+    "Button is not locked after selecting another item.");
+
+  yield stopRecording(panel);
+  yield teardown(panel);
+  finish();
+});
--- a/browser/devtools/performance/test/browser_perf_recordings-io-01.js
+++ b/browser/devtools/performance/test/browser_perf_recordings-io-01.js
@@ -18,17 +18,17 @@ let test = Task.async(function*() {
   ok(originalData, "The original recording is not empty.");
 
   // Save recording.
 
   let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
   file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
 
   let exported = once(PerformanceController, EVENTS.RECORDING_EXPORTED);
-  yield PerformanceController.exportRecording("", file);
+  yield PerformanceController.exportRecording("", PerformanceController.getCurrentRecording(), file);
 
   yield exported;
   ok(true, "The recording data appears to have been successfully saved.");
 
   // Import recording.
 
   let rerendered = waitForWidgetsRendered(panel);
   let imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/test/browser_perf_recordings-io-04.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests if the performance tool can import profiler data from the
+ * original profiler tool.
+ */
+
+let test = Task.async(function*() {
+  let { target, panel, toolbox } = yield initPerformance(SIMPLE_URL);
+  let { EVENTS, PerformanceController } = panel.panelWin;
+
+  yield startRecording(panel);
+  yield stopRecording(panel);
+
+  // Get data from the current profiler
+  let data = PerformanceController.getAllData();
+
+  // Create a structure from the data that mimics the old profiler's data.
+  // Different name for `ticks`, different way of storing time,
+  // and no memory, markers data.
+  let oldProfilerData = {
+    recordingDuration: data.interval.endTime - data.interval.startTime,
+    ticksData: data.ticks,
+    profilerData: data.profilerData,
+    fileType: "Recorded Performance Data",
+    version: 1
+  };
+
+  // Save recording as an old profiler data.
+  let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
+  file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
+  yield asyncCopy(oldProfilerData, file);
+
+  // Import recording.
+
+  let rerendered = waitForWidgetsRendered(panel);
+  let imported = once(PerformanceController, EVENTS.RECORDING_IMPORTED);
+  yield PerformanceController.importRecording("", file);
+
+  yield imported;
+  ok(true, "The original profiler data appears to have been successfully imported.");
+
+  yield rerendered;
+  ok(true, "The imported data was re-rendered.");
+
+  // Verify imported recording.
+
+  let importedData = PerformanceController.getAllData();
+
+  is(importedData.startTime, data.startTime,
+    "The imported legacy data was successfully converted for the current tool (1).");
+  is(importedData.endTime, data.endTime,
+    "The imported legacy data was successfully converted for the current tool (2).");
+  is(importedData.markers.toSource(), [].toSource(),
+    "The imported legacy data was successfully converted for the current tool (3).");
+  is(importedData.memory.toSource(), [].toSource(),
+    "The imported legacy data was successfully converted for the current tool (4).");
+  is(importedData.ticks.toSource(), data.ticks.toSource(),
+    "The imported legacy data was successfully converted for the current tool (5).");
+  is(importedData.profilerData.toSource(), data.profilerData.toSource(),
+    "The imported legacy data was successfully converted for the current tool (6).");
+
+  yield teardown(panel);
+  finish();
+});
+
+function getUnicodeConverter() {
+  let className = "@mozilla.org/intl/scriptableunicodeconverter";
+  let converter = Cc[className].createInstance(Ci.nsIScriptableUnicodeConverter);
+  converter.charset = "UTF-8";
+  return converter;
+}
+
+function asyncCopy(data, file) {
+  let deferred = Promise.defer();
+
+  let string = JSON.stringify(data);
+  let inputStream = getUnicodeConverter().convertToInputStream(string);
+  let outputStream = FileUtils.openSafeFileOutputStream(file);
+
+  NetUtil.asyncCopy(inputStream, outputStream, status => {
+    if (!Components.isSuccessCode(status)) {
+      deferred.reject(new Error("Could not save data to file."));
+    }
+    deferred.resolve();
+  });
+
+  return deferred.promise;
+}
--- a/browser/devtools/performance/views/details-call-tree.js
+++ b/browser/devtools/performance/views/details-call-tree.js
@@ -8,29 +8,32 @@
  */
 let CallTreeView = {
   /**
    * Sets up the view with event binding.
    */
   initialize: function () {
     this._callTree = $(".call-tree-cells-container");
     this._onRecordingStopped = this._onRecordingStopped.bind(this);
+    this._onRecordingSelected = this._onRecordingSelected.bind(this);
     this._onRangeChange = this._onRangeChange.bind(this);
     this._onLink = this._onLink.bind(this);
 
     PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
+    PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
     OverviewView.on(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
     OverviewView.on(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
   },
 
   /**
    * Unbinds events.
    */
   destroy: function () {
     PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
+    PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
     OverviewView.off(EVENTS.OVERVIEW_RANGE_SELECTED, this._onRangeChange);
     OverviewView.off(EVENTS.OVERVIEW_RANGE_CLEARED, this._onRangeChange);
   },
 
   /**
    * Method for handling all the set up for rendering a new call tree.
    */
   render: function (profilerData, beginAt, endAt, options={}) {
@@ -47,16 +50,29 @@ let CallTreeView = {
    * Called when recording is stopped.
    */
   _onRecordingStopped: function () {
     let profilerData = PerformanceController.getProfilerData();
     this.render(profilerData);
   },
 
   /**
+   * Called when a recording has been selected.
+   */
+  _onRecordingSelected: function (_, recording) {
+    // If not recording, then this recording is done and we can render all of it
+    // Otherwise, TODO in bug 1120699 will hide the details view altogether if
+    // this is still recording.
+    if (!recording.isRecording()) {
+      let profilerData = recording.getProfilerData();
+      this.render(profilerData);
+    }
+  },
+
+  /**
    * Fired when a range is selected or cleared in the OverviewView.
    */
   _onRangeChange: function (_, params) {
     // When a range is cleared, we'll have no beginAt/endAt data,
     // so the rebuild will just render all the data again.
     let profilerData = PerformanceController.getProfilerData();
     let { beginAt, endAt } = params || {};
     this.render(profilerData, beginAt, endAt);
--- a/browser/devtools/performance/views/details-waterfall.js
+++ b/browser/devtools/performance/views/details-waterfall.js
@@ -8,42 +8,45 @@
  */
 let WaterfallView = {
   /**
    * Sets up the view with event binding.
    */
   initialize: Task.async(function *() {
     this._onRecordingStarted = this._onRecordingStarted.bind(this);
     this._onRecordingStopped = this._onRecordingStopped.bind(this);
+    this._onRecordingSelected = this._onRecordingSelected.bind(this);
     this._onMarkerSelected = this._onMarkerSelected.bind(this);
     this._onResize = this._onResize.bind(this);
 
     this.waterfall = new Waterfall($("#waterfall-breakdown"), $("#details-pane"), TIMELINE_BLUEPRINT);
     this.details = new MarkerDetails($("#waterfall-details"), $("#waterfall-view > splitter"));
 
     this.waterfall.on("selected", this._onMarkerSelected);
     this.waterfall.on("unselected", this._onMarkerSelected);
     this.details.on("resize", this._onResize);
 
     PerformanceController.on(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
     PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
+    PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
 
     this.waterfall.recalculateBounds();
   }),
 
   /**
    * Unbinds events.
    */
   destroy: function () {
     this.waterfall.off("selected", this._onMarkerSelected);
     this.waterfall.off("unselected", this._onMarkerSelected);
     this.details.off("resize", this._onResize);
 
     PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
     PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
+    PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
   },
 
   /**
    * Method for handling all the set up for rendering a new waterfall.
    */
   render: function() {
     let { startTime, endTime } = PerformanceController.getInterval();
     let markers = PerformanceController.getMarkers();
@@ -63,16 +66,25 @@ let WaterfallView = {
   /**
    * Called when recording stops.
    */
   _onRecordingStopped: function () {
     this.render();
   },
 
   /**
+   * Called when a recording is selected.
+   */
+  _onRecordingSelected: function (_, recording) {
+    if (!recording.isRecording()) {
+      this.render();
+    }
+  },
+
+  /**
    * Called when a marker is selected in the waterfall view,
    * updating the markers detail view.
    */
   _onMarkerSelected: function (event, marker) {
     if (event === "selected") {
       this.details.render({
         toolbox: gToolbox,
         marker: marker,
--- a/browser/devtools/performance/views/overview.js
+++ b/browser/devtools/performance/views/overview.js
@@ -25,16 +25,17 @@ const GRAPH_SCROLL_EVENTS_DRAIN = 50; //
  */
 let OverviewView = {
   /**
    * Sets up the view with event binding.
    */
   initialize: Task.async(function *() {
     this._onRecordingStarted = this._onRecordingStarted.bind(this);
     this._onRecordingStopped = this._onRecordingStopped.bind(this);
+    this._onRecordingSelected = this._onRecordingSelected.bind(this);
     this._onRecordingTick = this._onRecordingTick.bind(this);
     this._onGraphMouseUp = this._onGraphMouseUp.bind(this);
     this._onGraphScroll = this._onGraphScroll.bind(this);
 
     yield this._showFramerateGraph();
     yield this._showMarkersGraph();
     yield this._showMemoryGraph();
 
@@ -42,32 +43,34 @@ let OverviewView = {
     this.framerateGraph.on("scroll", this._onGraphScroll);
     this.markersOverview.on("mouseup", this._onGraphMouseUp);
     this.markersOverview.on("scroll", this._onGraphScroll);
     this.memoryOverview.on("mouseup", this._onGraphMouseUp);
     this.memoryOverview.on("scroll", this._onGraphScroll);
 
     PerformanceController.on(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
     PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
+    PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
   }),
 
   /**
    * Unbinds events.
    */
   destroy: function () {
     this.framerateGraph.off("mouseup", this._onGraphMouseUp);
     this.framerateGraph.off("scroll", this._onGraphScroll);
     this.markersOverview.off("mouseup", this._onGraphMouseUp);
     this.markersOverview.off("scroll", this._onGraphScroll);
     this.memoryOverview.off("mouseup", this._onGraphMouseUp);
     this.memoryOverview.off("scroll", this._onGraphScroll);
 
     clearNamedTimeout("graph-scroll");
     PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
     PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
+    PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingSelected);
   },
 
   /**
    * Sets up the framerate graph.
    */
   _showFramerateGraph: Task.async(function *() {
     this.framerateGraph = new LineGraphWidget($("#time-framerate"), {
       metric: L10N.getStr("graphs.fps")
@@ -180,34 +183,49 @@ let OverviewView = {
     if (this._timeoutId) {
       this._timeoutId = setTimeout(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL);
     }
   },
 
   /**
    * Called when recording starts.
    */
-  _onRecordingStarted: function () {
+  _onRecordingStarted: function (_, recording) {
+    this._checkSelection(recording);
     this._timeoutId = setTimeout(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL);
-
     this.framerateGraph.dropSelection();
-    this.framerateGraph.selectionEnabled = false;
-    this.markersOverview.selectionEnabled = false;
-    this.memoryOverview.selectionEnabled = false;
   },
 
   /**
    * Called when recording stops.
    */
-  _onRecordingStopped: function () {
+  _onRecordingStopped: function (_, recording) {
+    this._checkSelection(recording);
     clearTimeout(this._timeoutId);
     this._timeoutId = null;
 
     this.render(FRAMERATE_GRAPH_HIGH_RES_INTERVAL);
+  },
 
-    this.framerateGraph.selectionEnabled = true;
-    this.markersOverview.selectionEnabled = true;
-    this.memoryOverview.selectionEnabled = true;
+  /**
+   * Called when a new recording is selected.
+   */
+  _onRecordingSelected: function (_, recording) {
+    this.framerateGraph.dropSelection();
+    this._checkSelection(recording);
+
+    // If timeout exists, we have something recording, so
+    // this will still tick away at rendering. Otherwise, force a render.
+    if (!this._timeoutId) {
+      this.render(FRAMERATE_GRAPH_HIGH_RES_INTERVAL);
+    }
+  },
+
+  _checkSelection: function (recording) {
+    let selectionEnabled = !recording.isRecording();
+    this.framerateGraph.selectionEnabled = selectionEnabled;
+    this.markersOverview.selectionEnabled = selectionEnabled;
+    this.memoryOverview.selectionEnabled = selectionEnabled;
   }
 };
 
 // Decorates the OverviewView as an EventEmitter
 EventEmitter.decorate(OverviewView);
new file mode 100644
--- /dev/null
+++ b/browser/devtools/performance/views/recordings.js
@@ -0,0 +1,237 @@
+/* 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";
+
+/**
+ * Functions handling the recordings UI.
+ */
+let RecordingsView = Heritage.extend(WidgetMethods, {
+  /**
+   * Initialization function, called when the tool is started.
+   */
+  initialize: function() {
+    this.widget = new SideMenuWidget($("#recordings-list"));
+
+    this._onSelect = this._onSelect.bind(this);
+    this._onRecordingStarted = this._onRecordingStarted.bind(this);
+    this._onRecordingStopped = this._onRecordingStopped.bind(this);
+    this._onRecordingImported = this._onRecordingImported.bind(this);
+    this._onSaveButtonClick = this._onSaveButtonClick.bind(this);
+
+    this.emptyText = L10N.getStr("noRecordingsText");
+
+    PerformanceController.on(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
+    PerformanceController.on(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
+    PerformanceController.on(EVENTS.RECORDING_IMPORTED, this._onRecordingImported);
+    this.widget.addEventListener("select", this._onSelect, false);
+  },
+
+  /**
+   * Destruction function, called when the tool is closed.
+   */
+  destroy: function() {
+    PerformanceController.off(EVENTS.RECORDING_STARTED, this._onRecordingStarted);
+    PerformanceController.off(EVENTS.RECORDING_STOPPED, this._onRecordingStopped);
+    PerformanceController.off(EVENTS.RECORDING_IMPORTED, this._onRecordingImported);
+    this.widget.removeEventListener("select", this._onSelect, false);
+  },
+
+  /**
+   * Adds an empty recording to this container.
+   *
+   * @param RecordingModel recording
+   *        A model for the new recording item created.
+   */
+  addEmptyRecording: function (recording) {
+    let titleNode = document.createElement("label");
+    titleNode.className = "plain recording-item-title";
+    titleNode.setAttribute("value", recording.getLabel() ||
+      L10N.getFormatStr("recordingsList.itemLabel", this.itemCount + 1));
+
+    let durationNode = document.createElement("label");
+    durationNode.className = "plain recording-item-duration";
+    durationNode.setAttribute("value",
+      L10N.getStr("recordingsList.recordingLabel"));
+
+    let saveNode = document.createElement("label");
+    saveNode.className = "plain recording-item-save";
+    saveNode.addEventListener("click", this._onSaveButtonClick);
+
+    let hspacer = document.createElement("spacer");
+    hspacer.setAttribute("flex", "1");
+
+    let footerNode = document.createElement("hbox");
+    footerNode.className = "recording-item-footer";
+    footerNode.appendChild(durationNode);
+    footerNode.appendChild(hspacer);
+    footerNode.appendChild(saveNode);
+
+    let vspacer = document.createElement("spacer");
+    vspacer.setAttribute("flex", "1");
+
+    let contentsNode = document.createElement("vbox");
+    contentsNode.className = "recording-item";
+    contentsNode.setAttribute("flex", "1");
+    contentsNode.appendChild(titleNode);
+    contentsNode.appendChild(vspacer);
+    contentsNode.appendChild(footerNode);
+
+    // Append a recording item to this container.
+    return this.push([contentsNode], {
+      // Store the recording model that contains all the data to be
+      // rendered in the item.
+      attachment: recording
+    });
+  },
+
+  /**
+   * Signals that a recording session has started.
+   *
+   * @param RecordingModel recording
+   *        Model of the recording that was started.
+   */
+  _onRecordingStarted: function (_, recording) {
+    // Insert a "dummy" recording item, to hint that recording has now started.
+    let recordingItem;
+
+    // If a label is specified (e.g due to a call to `console.profile`),
+    // then try reusing a pre-existing recording item, if there is one.
+    // This is symmetrical to how `this.handleRecordingEnded` works.
+    if (recording.getLabel()) {
+      recordingItem = this.getItemForAttachment(e =>
+        e.getLabel() === recording.getLabel());
+    }
+    // Otherwise, create a new empty recording item.
+    if (!recordingItem) {
+      recordingItem = this.addEmptyRecording(recording);
+    }
+
+    // Mark the corresponding item as being a "record in progress".
+    recordingItem.isRecording = true;
+
+    // If this is a manual recording, immediately select it.
+    if (!recording.getLabel()) {
+      this.selectedItem = recordingItem;
+    }
+
+    this.emit(EVENTS.RECORDING_SELECTED, recording);
+  },
+
+  /**
+   * Signals that a recording session has ended.
+   *
+   * @param RecordingModel recording
+   *        The model of the recording that just stopped.
+   */
+  _onRecordingStopped: function (_, recording) {
+    let profileLabel = recording.getLabel();
+    let recordingItem;
+
+    // If a label is specified (e.g due to a call to `console.profileEnd`),
+    // then try reusing a pre-existing recording item, if there is one.
+    // This is symmetrical to how `this.handleRecordingStarted` works.
+    if (profileLabel) {
+      recordingItem = this.getItemForAttachment(e =>
+        e.profilerData.profileLabel == profileLabel);
+    }
+    // Otherwise, just use the first available recording item.
+    if (!recordingItem) {
+      recordingItem = this.getItemForPredicate(e => e.isRecording);
+    }
+
+    // Mark the corresponding item as being a "finished recording".
+    recordingItem.isRecording = false;
+
+    // Render the recording item with finalized information (timing, etc)
+    this.finalizeRecording(recordingItem);
+    this.forceSelect(recordingItem);
+  },
+
+  /**
+   * Signals that a recording has been imported.
+   *
+   * @param object recordingData
+   *        The profiler and refresh driver ticks data received from the front.
+   * @param RecordingModel model
+   *        The recording model containing data on the recording session.
+   */
+  _onRecordingImported: function (_, recordingData, model) {
+    let recordingItem = this.addEmptyRecording(model);
+    recordingItem.isRecording = false;
+
+    // Immediately select the imported recording
+    this.selectedItem = recordingItem;
+
+    // Render the recording item with finalized information (timing, etc)
+    this.finalizeRecording(recordingItem);
+
+    // Fire the selection and allow to propogate.
+    this.emit(EVENTS.RECORDING_SELECTED, model);
+  },
+
+  /**
+   * Adds recording data to a recording item in this container.
+   *
+   * @param Item recordingItem
+   *        An item inserted via `RecordingsView.addEmptyRecording`.
+   */
+  finalizeRecording: function (recordingItem) {
+    let model = recordingItem.attachment;
+
+    let saveNode = $(".recording-item-save", recordingItem.target);
+    saveNode.setAttribute("value",
+      L10N.getStr("recordingsList.saveLabel"));
+
+    let durationMillis = model.getDuration().toFixed(0);
+    let durationNode = $(".recording-item-duration", recordingItem.target);
+    durationNode.setAttribute("value",
+      L10N.getFormatStr("recordingsList.durationLabel", durationMillis));
+  },
+
+  /**
+   * The select listener for this container.
+   */
+  _onSelect: Task.async(function*({ detail: recordingItem }) {
+    // TODO 1120699
+    // show appropriate empty/recording panels for several scenarios below
+    if (!recordingItem) {
+      return;
+    }
+
+    let model = recordingItem.attachment;
+
+    // If recording, don't abort completely, as we still want to fire an event
+    // for selection so we can continue repainting the overview graphs.
+    if (recordingItem.isRecording) {
+      this.emit(EVENTS.RECORDING_SELECTED, model);
+      return;
+    }
+
+    this.emit(EVENTS.RECORDING_SELECTED, model);
+  }),
+
+  /**
+   * The click listener for the "save" button of each item in this container.
+   */
+  _onSaveButtonClick: function (e) {
+    let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+    fp.init(window, L10N.getStr("recordingsList.saveDialogTitle"), Ci.nsIFilePicker.modeSave);
+    fp.appendFilter(L10N.getStr("recordingsList.saveDialogJSONFilter"), "*.json");
+    fp.appendFilter(L10N.getStr("recordingsList.saveDialogAllFilter"), "*.*");
+    fp.defaultString = "profile.json";
+
+    fp.open({ done: result => {
+      if (result == Ci.nsIFilePicker.returnCancel) {
+        return;
+      }
+      let recordingItem = this.getItemForElement(e.target);
+      this.emit(EVENTS.UI_EXPORT_RECORDING, recordingItem.attachment, fp.file);
+    }});
+  }
+});
+
+/**
+ * Convenient way of emitting events from the RecordingsView.
+ */
+EventEmitter.decorate(RecordingsView);
--- a/browser/devtools/projecteditor/lib/projecteditor.js
+++ b/browser/devtools/projecteditor/lib/projecteditor.js
@@ -750,17 +750,17 @@ var ProjectEditor = Class({
    *          True if there are no unsaved changes
    *          Otherwise, ask the user to confirm and return the outcome.
    */
   confirmUnsaved: function() {
 
     if (this.hasUnsavedResources) {
       return confirm(
         getLocalizedString("projecteditor.confirmUnsavedTitle"),
-        getLocalizedString("projecteditor.confirmUnsavedLabel")
+        getLocalizedString("projecteditor.confirmUnsavedLabel2")
       );
     }
 
     return true;
   }
 
 });
 
--- a/browser/devtools/webaudioeditor/controller.js
+++ b/browser/devtools/webaudioeditor/controller.js
@@ -10,28 +10,32 @@ let gAudioNodes = new AudioNodesCollecti
 
 /**
  * Initializes the web audio editor views
  */
 function startupWebAudioEditor() {
   return all([
     WebAudioEditorController.initialize(),
     ContextView.initialize(),
-    InspectorView.initialize()
+    InspectorView.initialize(),
+    PropertiesView.initialize(),
+    AutomationView.initialize()
   ]);
 }
 
 /**
  * Destroys the web audio editor controller and views.
  */
 function shutdownWebAudioEditor() {
   return all([
     WebAudioEditorController.destroy(),
     ContextView.destroy(),
     InspectorView.destroy(),
+    PropertiesView.destroy(),
+    AutomationView.destroy()
   ]);
 }
 
 /**
  * Functions handling target-related lifetime events.
  */
 let WebAudioEditorController = {
   /**
@@ -78,16 +82,17 @@ let WebAudioEditorController = {
   /**
    * Called when page is reloaded to show the reload notice and waiting
    * for an audio context notice.
    */
   reset: function () {
     $("#content").hidden = true;
     ContextView.resetUI();
     InspectorView.resetUI();
+    PropertiesView.resetUI();
   },
 
   // Since node create and connect are probably executed back to back,
   // and the controller's `_onCreateNode` needs to look up type,
   // the edge creation could be called before the graph node is actually
   // created. This way, we can check and listen for the event before
   // adding an edge.
   _waitForNodeCreation: function (sourceActor, destActor) {
--- a/browser/devtools/webaudioeditor/includes.js
+++ b/browser/devtools/webaudioeditor/includes.js
@@ -5,27 +5,30 @@
 
 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 Cu.import("resource:///modules/devtools/gDevTools.jsm");
 
-const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
+const devtools = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
+const { require } = devtools;
 
 let { console } = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
 let { EventTarget } = require("sdk/event/target");
 const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
 const { Class } = require("sdk/core/heritage");
 const EventEmitter = require("devtools/toolkit/event-emitter");
 const STRINGS_URI = "chrome://browser/locale/devtools/webaudioeditor.properties"
 const L10N = new ViewHelpers.L10N(STRINGS_URI);
 const Telemetry = require("devtools/shared/telemetry");
 const telemetry = new Telemetry();
+devtools.lazyImporter(this, "LineGraphWidget",
+  "resource:///modules/devtools/Graphs.jsm");
 
 // Override DOM promises with Promise.jsm helpers
 const { defer, all } = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
 
 /* Events fired on `window` to indicate state or actions*/
 const EVENTS = {
   // Fired when the first AudioNode has been created, signifying
   // that the AudioContext is being used and should be tracked via the editor.
@@ -48,21 +51,27 @@ const EVENTS = {
   UI_INSPECTOR_NODE_SET: "WebAudioEditor:UIInspectorNodeSet",
 
   // When the inspector is finished rendering in or out of view.
   UI_INSPECTOR_TOGGLED: "WebAudioEditor:UIInspectorToggled",
 
   // When an audio node is finished loading in the Properties tab.
   UI_PROPERTIES_TAB_RENDERED: "WebAudioEditor:UIPropertiesTabRendered",
 
+  // When an audio node is finished loading in the Automation tab.
+  UI_AUTOMATION_TAB_RENDERED: "WebAudioEditor:UIAutomationTabRendered",
+
   // When the Audio Context graph finishes rendering.
   // Is called with two arguments, first representing number of nodes
   // rendered, second being the number of edge connections rendering (not counting
   // param edges), followed by the count of the param edges rendered.
-  UI_GRAPH_RENDERED: "WebAudioEditor:UIGraphRendered"
+  UI_GRAPH_RENDERED: "WebAudioEditor:UIGraphRendered",
+
+  // Called when the inspector splitter is moved and resized.
+  UI_INSPECTOR_RESIZE: "WebAudioEditor:UIInspectorResize"
 };
 
 /**
  * The current target and the Web Audio Editor front, set by this tool's host.
  */
 let gToolbox, gTarget, gFront;
 
 /**
--- a/browser/devtools/webaudioeditor/models.js
+++ b/browser/devtools/webaudioeditor/models.js
@@ -81,16 +81,27 @@ const AudioNodeModel = Class({
    *
    * @return Promise->Object
    */
   getParams: function () {
     return this.actor.getParams();
   },
 
   /**
+   * Returns a promise that resolves to an object containing an
+   * array of event information and an array of automation data.
+   *
+   * @param String paramName
+   * @return Promise->Array
+   */
+  getAutomationData: function (paramName) {
+    return this.actor.getAutomationData(paramName);
+  },
+
+  /**
    * Takes a `dagreD3.Digraph` object and adds this node to
    * the graph to be rendered.
    *
    * @param dagreD3.Digraph
    */
   addToGraph: function (graph) {
     graph.addNode(this.id, {
       type: this.type,
--- a/browser/devtools/webaudioeditor/test/browser.ini
+++ b/browser/devtools/webaudioeditor/test/browser.ini
@@ -56,8 +56,11 @@ support-files =
 [browser_wa_properties-view.js]
 [browser_wa_properties-view-edit-01.js]
 skip-if = true # bug 1010423
 [browser_wa_properties-view-edit-02.js]
 skip-if = true # bug 1010423
 [browser_wa_properties-view-media-nodes.js]
 [browser_wa_properties-view-params.js]
 [browser_wa_properties-view-params-objects.js]
+
+[browser_wa_automation-view-01.js]
+[browser_wa_automation-view-02.js]
--- a/browser/devtools/webaudioeditor/test/browser_audionode-actor-get-param-flags.js
+++ b/browser/devtools/webaudioeditor/test/browser_audionode-actor-get-param-flags.js
@@ -34,16 +34,14 @@ add_task(function*() {
       if (param === "buffer") {
         is(flags.Buffer, true, "`buffer` params have Buffer flag");
       }
       else if (param === "bufferSize" || param === "frequencyBinCount") {
         is(flags.readonly, true, param + " is readonly");
       }
       else if (param === "curve") {
         is(flags["Float32Array"], true, "`curve` param has Float32Array flag");
-      } else {
-        is(Object.keys(flags), 0, type + "-" + param + " has no flags set")
       }
     }
   }
 
   yield removeTab(target.tab);
 });
--- a/browser/devtools/webaudioeditor/test/browser_audionode-actor-get-params-01.js
+++ b/browser/devtools/webaudioeditor/test/browser_audionode-actor-get-params-01.js
@@ -31,16 +31,14 @@ add_task(function*() {
       if (param === "buffer") {
         is(flags.Buffer, true, "`buffer` params have Buffer flag");
       }
       else if (param === "bufferSize" || param === "frequencyBinCount") {
         is(flags.readonly, true, param + " is readonly");
       }
       else if (param === "curve") {
         is(flags["Float32Array"], true, "`curve` param has Float32Array flag");
-      } else {
-        is(Object.keys(flags), 0, type + "-" + param + " has no flags set")
       }
     });
   });
 
   yield removeTab(target.tab);
 });
new file mode 100644
--- /dev/null
+++ b/browser/devtools/webaudioeditor/test/browser_wa_automation-view-01.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that automation view shows the correct view depending on if events
+ * or params exist.
+ */
+
+add_task(function*() {
+  let { target, panel } = yield initWebAudioEditor(AUTOMATION_URL);
+  let { panelWin } = panel;
+  let { gFront, $, $$, EVENTS } = panelWin;
+
+  let started = once(gFront, "start-context");
+
+  reload(target);
+
+  let [actors] = yield Promise.all([
+    get3(gFront, "create-node"),
+    waitForGraphRendered(panelWin, 3, 2)
+  ]);
+  let nodeIds = actors.map(actor => actor.actorID);
+
+  // Oscillator node
+  click(panelWin, findGraphNode(panelWin, nodeIds[1]));
+  yield waitForInspectorRender(panelWin, EVENTS);
+  click(panelWin, $("#automation-tab"));
+
+  ok(isVisible($("#automation-graph-container")), "graph container should be visible");
+  ok(isVisible($("#automation-content")), "automation content should be visible");
+  ok(!isVisible($("#automation-no-events")), "no-events panel should not be visible");
+  ok(!isVisible($("#automation-empty")), "empty panel should not be visible");
+
+  // Gain node