Merge m-c to oak
authorRobert Strong <robert.bugzilla@gmail.com>
Fri, 10 Oct 2014 13:39:33 -0700
changeset 491327 bd6eee8c6e8c5f5c1d34658039129d24725a808d
parent 491326 4ad053cb848f1c13b49199ef004dcb186190ef27 (current diff)
parent 209753 097821fd89ed755f444f07ab501009855d996b2d (diff)
child 491328 e65a2a04a0210b132eb34e9e5f4380492770a2e3
push id47343
push userbmo:dothayer@mozilla.com
push dateWed, 01 Mar 2017 22:58:58 +0000
milestone35.0a1
Merge m-c to oak
b2g/installer/Makefile.in
browser/app/macbuild/Contents/MacOS-files.in
browser/app/profile/firefox.js
browser/base/content/test/general/privateBrowsingMode.js
browser/confvars.sh
browser/installer/package-manifest.in
browser/themes/linux/aboutSocialError.css
browser/themes/linux/icon.png
browser/themes/osx/aboutSocialError.css
browser/themes/osx/icon.png
browser/themes/shared/devtools/projecteditor/file-icons-sheet@2x.png
browser/themes/windows/aboutSocialError.css
browser/themes/windows/icon.png
build/automationutils.py
content/base/public/nsDOMFile.h
content/base/src/nsDOMBlobBuilder.cpp
content/base/src/nsDOMBlobBuilder.h
content/base/src/nsDOMFile.cpp
dom/icc/interfaces/nsIDOMIccInfo.idl
dom/mobileconnection/gonk/MobileConnectionService.js
dom/workers/File.cpp
dom/workers/File.h
gfx/angle/src/libGLESv2/constants.h
js/src/jit-test/tests/basic/testErrorReportIn_getPrototypeOf.js
js/src/jit-test/tests/debug/Script-sourceMapURL-deprecated.js
js/src/jit-test/tests/debug/Script-sourceMapURL.js
js/src/jsonparser.cpp
js/src/jsonparser.h
js/src/vm/ObjectImpl-inl.h
js/src/vm/ObjectImpl.cpp
js/src/vm/ObjectImpl.h
js/xpconnect/src/XPCShellImpl.cpp
memory/replace/dmd/check_test_output.py
memory/replace/dmd/test-expected.dmd
mobile/android/base/resources/layout-xlarge-v11/remote_tabs_child.xml
mobile/android/base/resources/layout-xlarge-v11/remote_tabs_group.xml
mobile/android/base/resources/layout/remote_tabs_child.xml
mobile/android/base/resources/layout/remote_tabs_container_panel.xml
mobile/android/base/resources/layout/remote_tabs_group.xml
mobile/android/base/resources/layout/remote_tabs_setup_panel.xml
mobile/android/base/resources/layout/remote_tabs_verification_panel.xml
mobile/android/base/tabs/RemoteTabsContainerPanel.java
mobile/android/base/tabs/RemoteTabsList.java
mobile/android/base/tabs/RemoteTabsSetupPanel.java
mobile/android/base/tabs/RemoteTabsVerificationPanel.java
mobile/android/search/res/color/facet_button_text_color.xml
mobile/android/search/res/drawable-hdpi/ic_action_settings.png
mobile/android/search/res/drawable-hdpi/ic_widget_new_tab.png
mobile/android/search/res/drawable-hdpi/ic_widget_search.png
mobile/android/search/res/drawable-hdpi/network_error.png
mobile/android/search/res/drawable-hdpi/search_clear.png
mobile/android/search/res/drawable-hdpi/search_fox.png
mobile/android/search/res/drawable-hdpi/search_history.png
mobile/android/search/res/drawable-hdpi/search_icon_active.png
mobile/android/search/res/drawable-hdpi/search_icon_inactive.png
mobile/android/search/res/drawable-hdpi/search_launcher.png
mobile/android/search/res/drawable-hdpi/search_plus.png
mobile/android/search/res/drawable-hdpi/widget_bg.9.png
mobile/android/search/res/drawable-mdpi/ic_action_settings.png
mobile/android/search/res/drawable-mdpi/ic_widget_new_tab.png
mobile/android/search/res/drawable-mdpi/ic_widget_search.png
mobile/android/search/res/drawable-mdpi/network_error.png
mobile/android/search/res/drawable-mdpi/search_clear.png
mobile/android/search/res/drawable-mdpi/search_fox.png
mobile/android/search/res/drawable-mdpi/search_history.png
mobile/android/search/res/drawable-mdpi/search_icon_active.png
mobile/android/search/res/drawable-mdpi/search_icon_inactive.png
mobile/android/search/res/drawable-mdpi/search_launcher.png
mobile/android/search/res/drawable-mdpi/search_plus.png
mobile/android/search/res/drawable-mdpi/widget_bg.9.png
mobile/android/search/res/drawable-xhdpi/ic_action_settings.png
mobile/android/search/res/drawable-xhdpi/ic_widget_new_tab.png
mobile/android/search/res/drawable-xhdpi/ic_widget_search.png
mobile/android/search/res/drawable-xhdpi/network_error.png
mobile/android/search/res/drawable-xhdpi/search_clear.png
mobile/android/search/res/drawable-xhdpi/search_fox.png
mobile/android/search/res/drawable-xhdpi/search_history.png
mobile/android/search/res/drawable-xhdpi/search_icon_active.png
mobile/android/search/res/drawable-xhdpi/search_icon_inactive.png
mobile/android/search/res/drawable-xhdpi/search_launcher.png
mobile/android/search/res/drawable-xhdpi/search_plus.png
mobile/android/search/res/drawable-xhdpi/widget_bg.9.png
mobile/android/search/res/drawable-xxhdpi/ic_action_settings.png
mobile/android/search/res/drawable-xxhdpi/ic_widget_new_tab.png
mobile/android/search/res/drawable-xxhdpi/ic_widget_search.png
mobile/android/search/res/drawable-xxhdpi/network_error.png
mobile/android/search/res/drawable-xxhdpi/search_clear.png
mobile/android/search/res/drawable-xxhdpi/search_fox.png
mobile/android/search/res/drawable-xxhdpi/search_history.png
mobile/android/search/res/drawable-xxhdpi/search_icon_active.png
mobile/android/search/res/drawable-xxhdpi/search_icon_inactive.png
mobile/android/search/res/drawable-xxhdpi/search_launcher.png
mobile/android/search/res/drawable-xxhdpi/search_plus.png
mobile/android/search/res/drawable-xxxhdpi/search_launcher.png
mobile/android/search/res/drawable/edit_text_default.xml
mobile/android/search/res/drawable/edit_text_focused.xml
mobile/android/search/res/drawable/facet_button_background.xml
mobile/android/search/res/drawable/facet_button_background_default.xml
mobile/android/search/res/drawable/facet_button_background_pressed.xml
mobile/android/search/res/drawable/progressbar.xml
mobile/android/search/res/drawable/search_row_background.xml
mobile/android/search/res/drawable/widget_button_left.xml
mobile/android/search/res/drawable/widget_button_left_default.xml
mobile/android/search/res/drawable/widget_button_left_pressed.xml
mobile/android/search/res/drawable/widget_button_middle.xml
mobile/android/search/res/drawable/widget_button_middle_pressed.xml
mobile/android/search/res/drawable/widget_button_right.xml
mobile/android/search/res/drawable/widget_button_right_pressed.xml
mobile/android/search/res/layout/keyguard_widget.xml
mobile/android/search/res/layout/search_activity_main.xml
mobile/android/search/res/layout/search_bar.xml
mobile/android/search/res/layout/search_empty.xml
mobile/android/search/res/layout/search_fragment_post_search.xml
mobile/android/search/res/layout/search_fragment_pre_search.xml
mobile/android/search/res/layout/search_history_row.xml
mobile/android/search/res/layout/search_sugestions.xml
mobile/android/search/res/layout/search_suggestions_row.xml
mobile/android/search/res/layout/search_widget.xml
mobile/android/search/res/values-v13/search_styles.xml
mobile/android/search/res/values-v16/search_styles.xml
mobile/android/search/res/values/search_attrs.xml
mobile/android/search/res/values/search_colors.xml
mobile/android/search/res/values/search_dimens.xml
mobile/android/search/res/values/search_styles.xml
mobile/android/search/res/xml/search_preferences.xml
mobile/android/search/res/xml/search_widget_info.xml
services/crypto/modules/WeaveCrypto.js
testing/gtest/Makefile.in
testing/mochitest/runtests.py
testing/xpcshell/runxpcshelltests.py
testing/xpcshell/xpcshell_android.ini
testing/xpcshell/xpcshell_b2g.ini
toolkit/components/telemetry/Histograms.json
toolkit/crashreporter/CrashSubmit.jsm
toolkit/crashreporter/nsExceptionHandler.cpp
toolkit/mozapps/installer/packager.mk
toolkit/mozapps/installer/signing.mk
toolkit/xre/nsAppRunner.cpp
toolkit/xre/nsXREDirProvider.cpp
toolkit/xre/nsXREDirProvider.h
xpcom/build/BinaryPath.h
--- 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.
 
-Bug 1069071: IPDL changes require CLOBBER
+Bug 1069071: IPDL changes require CLOBBER (second time around)
--- a/accessible/base/ARIAMap.cpp
+++ b/accessible/base/ARIAMap.cpp
@@ -734,17 +734,17 @@ static const AttrCharacteristics gWAIUni
   {&nsGkAtoms::aria_valuetext,         ATTR_BYPASSOBJ                               }
 };
 
 namespace {
 
 struct RoleComparator
 {
   const nsDependentSubstring& mRole;
-  RoleComparator(const nsDependentSubstring& aRole) : mRole(aRole) {}
+  explicit RoleComparator(const nsDependentSubstring& aRole) : mRole(aRole) {}
   int operator()(const nsRoleMapEntry& aEntry) const {
     return Compare(mRole, aEntry.ARIARoleString());
   }
 };
 
 }
 
 nsRoleMapEntry*
--- a/accessible/generic/Accessible-inl.h
+++ b/accessible/generic/Accessible-inl.h
@@ -62,12 +62,20 @@ Accessible::HasNumericValue() const
 
 inline void
 Accessible::ScrollTo(uint32_t aHow) const
 {
   if (mContent)
     nsCoreUtils::ScrollTo(mDoc->PresShell(), mContent, aHow);
 }
 
+inline bool
+Accessible::UpdateChildren()
+{
+  AutoTreeMutation mut(this);
+  InvalidateChildren();
+  return EnsureChildren();
+}
+
 } // namespace a11y
 } // namespace mozilla
 
 #endif
--- a/accessible/generic/Accessible.cpp
+++ b/accessible/generic/Accessible.cpp
@@ -1931,30 +1931,34 @@ Accessible::BindToParent(Accessible* aPa
       NS_ERROR("Binding to the same parent!");
       return;
     }
   }
 
   mParent = aParent;
   mIndexInParent = aIndexInParent;
 
-  mParent->InvalidateChildrenGroupInfo();
+#ifdef DEBUG
+  AssertInMutatingSubtree();
+#endif
 
   // Note: this is currently only used for richlistitems and their children.
   if (mParent->HasNameDependentParent() || mParent->IsXULListItem())
     mContextFlags |= eHasNameDependentParent;
   else
     mContextFlags &= ~eHasNameDependentParent;
 }
 
 // Accessible protected
 void
 Accessible::UnbindFromParent()
 {
-  mParent->InvalidateChildrenGroupInfo();
+#ifdef DEBUG
+  AssertInMutatingSubtree();
+#endif
   mParent = nullptr;
   mIndexInParent = -1;
   mIndexOfEmbeddedChild = -1;
   mGroupInfo = nullptr;
   mContextFlags &= ~eHasNameDependentParent;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -2653,16 +2657,31 @@ Accessible::StaticAsserts() const
   static_assert(eLastAccType <= (1 << kTypeBits) - 1,
                 "Accessible::mType was oversized by eLastAccType!");
   static_assert(eLastContextFlag <= (1 << kContextFlagsBits) - 1,
                 "Accessible::mContextFlags was oversized by eLastContextFlag!");
   static_assert(eLastAccGenericType <= (1 << kGenericTypesBits) - 1,
                 "Accessible::mGenericType was oversized by eLastAccGenericType!");
 }
 
+void
+Accessible::AssertInMutatingSubtree() const
+{
+  if (IsDoc() || IsApplication())
+    return;
+
+  const Accessible *acc = this;
+  while (!acc->IsDoc() && !(acc->mStateFlags & eSubtreeMutating)) {
+    acc = acc->Parent();
+    if (!acc)
+      return;
+  }
+
+  MOZ_ASSERT(acc->mStateFlags & eSubtreeMutating);
+}
 
 ////////////////////////////////////////////////////////////////////////////////
 // KeyBinding class
 
 // static
 uint32_t
 KeyBinding::AccelModifier()
 {
--- a/accessible/generic/Accessible.h
+++ b/accessible/generic/Accessible.h
@@ -368,21 +368,17 @@ public:
    * Set the ARIA role map entry for a new accessible.
    */
   void SetRoleMapEntry(nsRoleMapEntry* aRoleMapEntry)
     { mRoleMapEntry = aRoleMapEntry; }
 
   /**
    * Update the children cache.
    */
-  inline bool UpdateChildren()
-  {
-    InvalidateChildren();
-    return EnsureChildren();
-  }
+  bool UpdateChildren();
 
   /**
    * Cache children if necessary. Return true if the accessible is defunct.
    */
   bool EnsureChildren();
 
   /**
    * Set the child count to -1 (unknown) and null out cached child pointers.
@@ -943,17 +939,18 @@ protected:
    */
   enum StateFlags {
     eIsDefunct = 1 << 0, // accessible is defunct
     eIsNotInDocument = 1 << 1, // accessible is not in document
     eSharedNode = 1 << 2, // accessible shares DOM node from another accessible
     eNotNodeMapEntry = 1 << 3, // accessible shouldn't be in document node map
     eHasNumericValue = 1 << 4, // accessible has a numeric value
     eGroupInfoDirty = 1 << 5, // accessible needs to update group info
-    eIgnoreDOMUIEvent = 1 << 6, // don't process DOM UI events for a11y events
+    eSubtreeMutating = 1 << 6, // subtree is being mutated
+    eIgnoreDOMUIEvent = 1 << 7, // don't process DOM UI events for a11y events
 
     eLastStateFlag = eIgnoreDOMUIEvent
   };
 
   /**
    * Flags used for contextual information about the accessible.
    */
   enum ContextFlags {
@@ -1059,34 +1056,36 @@ protected:
   nsCOMPtr<nsIContent> mContent;
   DocAccessible* mDoc;
 
   nsRefPtr<Accessible> mParent;
   nsTArray<nsRefPtr<Accessible> > mChildren;
   int32_t mIndexInParent;
 
   static const uint8_t kChildrenFlagsBits = 2;
-  static const uint8_t kStateFlagsBits = 7;
+  static const uint8_t kStateFlagsBits = 8;
   static const uint8_t kContextFlagsBits = 1;
   static const uint8_t kTypeBits = 6;
   static const uint8_t kGenericTypesBits = 13;
 
   /**
    * Keep in sync with ChildrenFlags, StateFlags, ContextFlags, and AccTypes.
    */
   uint32_t mChildrenFlags : kChildrenFlagsBits;
   uint32_t mStateFlags : kStateFlagsBits;
   uint32_t mContextFlags : kContextFlagsBits;
   uint32_t mType : kTypeBits;
   uint32_t mGenericTypes : kGenericTypesBits;
 
   void StaticAsserts() const;
+  void AssertInMutatingSubtree() const;
 
   friend class DocAccessible;
   friend class xpcAccessible;
+  friend class AutoTreeMutation;
 
   nsAutoPtr<mozilla::a11y::EmbeddedObjCollector> mEmbeddedObjCollector;
   int32_t mIndexOfEmbeddedChild;
   friend class EmbeddedObjCollector;
 
   nsAutoPtr<AccGroupInfo> mGroupInfo;
   friend class AccGroupInfo;
 
@@ -1160,12 +1159,41 @@ public:
 private:
   void ToPlatformFormat(nsAString& aValue) const;
   void ToAtkFormat(nsAString& aValue) const;
 
   uint32_t mKey;
   uint32_t mModifierMask;
 };
 
+/**
+ * This class makes sure required tasks are done before and after tree
+ * mutations. Currently this only includes group info invalidation. You must
+ * have an object of this class on the stack when calling methods that mutate
+ * the accessible tree.
+ */
+class AutoTreeMutation
+{
+public:
+  AutoTreeMutation(Accessible* aRoot, bool aInvalidationRequired = true) :
+    mInvalidationRequired(aInvalidationRequired), mRoot(aRoot)
+  {
+    MOZ_ASSERT(!(mRoot->mStateFlags & Accessible::eSubtreeMutating));
+    mRoot->mStateFlags |= Accessible::eSubtreeMutating;
+  }
+  ~AutoTreeMutation()
+  {
+    if (mInvalidationRequired)
+      mRoot->InvalidateChildrenGroupInfo();
+
+    MOZ_ASSERT(mRoot->mStateFlags & Accessible::eSubtreeMutating);
+    mRoot->mStateFlags &= ~Accessible::eSubtreeMutating;
+  }
+
+  bool mInvalidationRequired;
+private:
+  Accessible* mRoot;
+};
+
 } // namespace a11y
 } // namespace mozilla
 
 #endif
--- a/accessible/generic/DocAccessible.cpp
+++ b/accessible/generic/DocAccessible.cpp
@@ -478,17 +478,23 @@ DocAccessible::Shutdown()
     mVirtualCursor = nullptr;
   }
 
   mPresShell->SetDocAccessible(nullptr);
   mPresShell = nullptr;  // Avoid reentrancy
 
   mDependentIDsHash.Clear();
   mNodeToAccessibleMap.Clear();
-  ClearCache(mAccessibleCache);
+
+  {
+    // We're about to get rid of all of our children so there won't be anything
+    // to invalidate.
+    AutoTreeMutation mut(this, false);
+    ClearCache(mAccessibleCache);
+  }
 
   HyperTextAccessibleWrap::Shutdown();
 
   GetAccService()->NotifyOfDocumentShutdown(kungFuDeathGripDoc);
 }
 
 nsIFrame*
 DocAccessible::GetFrame() const
@@ -1313,18 +1319,20 @@ DocAccessible::ProcessInvalidationList()
       Accessible* container = GetContainerAccessible(content);
       if (container) {
         container->UpdateChildren();
         accessible = GetAccessible(content);
       }
     }
 
     // Make sure the subtree is created.
-    if (accessible)
+    if (accessible) {
+      AutoTreeMutation mut(accessible);
       CacheChildrenInSubtree(accessible);
+    }
   }
 
   mInvalidationList.Clear();
 }
 
 Accessible*
 DocAccessible::GetAccessibleEvenIfNotInMap(nsINode* aNode) const
 {
@@ -1420,17 +1428,19 @@ DocAccessible::DoInitialUpdate()
   // miss the notification (since content tree change notifications are ignored
   // prior to initial update). Make sure the content element is valid.
   nsIContent* contentElm = nsCoreUtils::GetRoleContent(mDocumentNode);
   if (mContent != contentElm) {
     mContent = contentElm;
     SetRoleMapEntry(aria::GetRoleMap(mContent));
   }
 
-  // Build initial tree.
+  // Build initial tree.  Since its the initial tree there's no group info to
+  // invalidate.
+  AutoTreeMutation mut(this, false);
   CacheChildrenInSubtree(this);
 
   // Fire reorder event after the document tree is constructed. Note, since
   // this reorder event is processed by parent document then events targeted to
   // this document may be fired prior to this reorder event. If this is
   // a problem then consider to keep event processing per tab document.
   if (!IsRoot()) {
     nsRefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(Parent());
@@ -1654,16 +1664,19 @@ DocAccessible::ProcessContentInserted(Ac
         // there is no HTML body element.
       }
 
       // XXX: Invalidate parent-child relations for container accessible and its
       // children because there's no good way to find insertion point of new child
       // accessibles into accessible tree. We need to invalidate children even
       // there's no inserted accessibles in the end because accessible children
       // are created while parent recaches child accessibles.
+      // XXX Group invalidation here may be redundant with invalidation in
+      // UpdateTree.
+      AutoTreeMutation mut(aContainer);
       aContainer->InvalidateChildren();
       CacheChildrenInSubtree(aContainer);
     }
 
     UpdateTree(aContainer, aInsertedContent->ElementAt(idx), true);
   }
 }
 
@@ -1686,16 +1699,17 @@ DocAccessible::UpdateTree(Accessible* aC
     else
       logging::MsgEntry("child accessible: null");
 
     logging::MsgEnd();
   }
 #endif
 
   nsRefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(aContainer);
+  AutoTreeMutation mut(aContainer);
 
   if (child) {
     updateFlags |= UpdateTreeInternal(child, aIsInsert, reorderEvent);
   } else {
     if (aIsInsert) {
       TreeWalker walker(aContainer, aChildNode, TreeWalker::eWalkCache);
 
       while ((child = walker.NextChild()))
--- a/accessible/html/HTMLImageMapAccessible.cpp
+++ b/accessible/html/HTMLImageMapAccessible.cpp
@@ -80,33 +80,34 @@ HTMLImageMapAccessible::UpdateChildAreas
 {
   nsImageFrame* imageFrame = do_QueryFrame(mContent->GetPrimaryFrame());
 
   // If image map is not initialized yet then we trigger one time more later.
   nsImageMap* imageMapObj = imageFrame->GetExistingImageMap();
   if (!imageMapObj)
     return;
 
-  bool doReorderEvent = false;
+  bool treeChanged = false;
+  AutoTreeMutation mut(this);
   nsRefPtr<AccReorderEvent> reorderEvent = new AccReorderEvent(this);
 
   // Remove areas that are not a valid part of the image map anymore.
   for (int32_t childIdx = mChildren.Length() - 1; childIdx >= 0; childIdx--) {
     Accessible* area = mChildren.ElementAt(childIdx);
     if (area->GetContent()->GetPrimaryFrame())
       continue;
 
     if (aDoFireEvents) {
       nsRefPtr<AccHideEvent> event = new AccHideEvent(area, area->GetContent());
       mDoc->FireDelayedEvent(event);
       reorderEvent->AddSubMutationEvent(event);
-      doReorderEvent = true;
     }
 
     RemoveChild(area);
+    treeChanged = true;
   }
 
   // Insert new areas into the tree.
   uint32_t areaElmCount = imageMapObj->AreaCount();
   for (uint32_t idx = 0; idx < areaElmCount; idx++) {
     nsIContent* areaContent = imageMapObj->GetAreaAt(idx);
 
     Accessible* area = mChildren.SafeElementAt(idx);
@@ -118,24 +119,28 @@ HTMLImageMapAccessible::UpdateChildAreas
         mDoc->UnbindFromDocument(area);
         break;
       }
 
       if (aDoFireEvents) {
         nsRefPtr<AccShowEvent> event = new AccShowEvent(area, areaContent);
         mDoc->FireDelayedEvent(event);
         reorderEvent->AddSubMutationEvent(event);
-        doReorderEvent = true;
       }
+
+      treeChanged = true;
     }
   }
 
   // Fire reorder event if needed.
-  if (doReorderEvent)
+  if (treeChanged && aDoFireEvents)
     mDoc->FireDelayedEvent(reorderEvent);
+
+  if (!treeChanged)
+    mut.mInvalidationRequired = false;
 }
 
 Accessible*
 HTMLImageMapAccessible::GetChildAccessibleFor(const nsINode* aNode) const
 {
   uint32_t length = mChildren.Length();
   for (uint32_t i = 0; i < length; i++) {
     Accessible* area = mChildren[i];
--- a/accessible/jsat/EventManager.jsm
+++ b/accessible/jsat/EventManager.jsm
@@ -261,17 +261,18 @@ this.EventManager.prototype = {
        if (this.inTest) {
         this.sendMsgFunc("AccessFu:Focused");
        }
        break;
       }
       case Events.DOCUMENT_LOAD_COMPLETE:
       {
         let position = this.contentControl.vc.position;
-        if (position && Utils.isInSubtree(position, aEvent.accessible)) {
+        if (aEvent.accessible === aEvent.accessibleDocument ||
+            (position && Utils.isInSubtree(position, aEvent.accessible))) {
           // Do not automove into the document if the virtual cursor is already
           // positioned inside it.
           break;
         }
         this.contentControl.autoMove(
           aEvent.accessible, { delay: 500 });
         break;
       }
--- a/accessible/jsat/OutputGenerator.jsm
+++ b/accessible/jsat/OutputGenerator.jsm
@@ -380,16 +380,24 @@ let OutputGenerator = {
         }, {
           string: this._getOutputName('tblRowInfo'),
           count: table.rowCount
         });
         this._addName(output, aAccessible, aFlags);
         this._addLandmark(output, aAccessible);
         return output;
       }
+    },
+
+    gridcell: function gridcell(aAccessible, aRoleStr, aState, aFlags) {
+      let output = [];
+      this._addState(output, aState);
+      this._addName(output, aAccessible, aFlags);
+      this._addLandmark(output, aAccessible);
+      return output;
     }
   }
 };
 
 /**
  * Generates speech utterances from objects, actions and state changes.
  * An utterance is an array of speech data.
  *
--- a/accessible/tests/mochitest/jsat/test_output.html
+++ b/accessible/tests/mochitest/jsat/test_output.html
@@ -435,16 +435,22 @@ https://bugzilla.mozilla.org/show_bug.cg
           oldAccOrElmOrID: "grid",
           expectedUtterance: [["3"], ["3"]],
           expectedBraille: [["3"], ["3"]]
         }, {
           accOrElmOrID: "gridcell2",
           oldAccOrElmOrID: "grid",
           expectedUtterance: [["4", "7"], ["4", "7"]],
           expectedBraille: [["4", "7"], ["4", "7"]]
+        }, {
+          accOrElmOrID: "gridcell3",
+          oldAccOrElmOrID: "grid",
+          expectedUtterance: [[{"string": "stateSelected"}, "5"],
+                              ["5", {"string": "stateSelected"}]],
+          expectedBraille: [["5"], ["5"]],
         }];
 
         // Test all possible utterance order preference values.
         tests.forEach(function run(test) {
           var utteranceOrderValues = [0, 1];
           utteranceOrderValues.forEach(
             function testUtteranceOrder(utteranceOrder) {
               SpecialPowers.setIntPref(PREF_UTTERANCE_ORDER, utteranceOrder);
@@ -576,15 +582,15 @@ https://bugzilla.mozilla.org/show_bug.cg
         </ol>
         <ol role="row">
           <li id="rowheader" role="rowheader" aria-label="Week 1">1</li>
           <li id="gridcell1" role="gridcell"><span>3</span><div></div></li>
           <li id="gridcell2" role="gridcell"><span>4</span><div>7</div></li>
         </ol>
         <ol role="row">
           <li role="rowheader">2</li>
-          <li role="gridcell">5</li>
+          <li id="gridcell3" aria-selected="true" role="gridcell">5</li>
           <li role="gridcell">6</li>
         </ol>
       </section>
     </div>
   </body>
 </html>
--- a/addon-sdk/source/lib/sdk/content/sandbox.js
+++ b/addon-sdk/source/lib/sdk/content/sandbox.js
@@ -69,17 +69,20 @@ const WorkerSandbox = Class({
 
   /**
    * Synchronous version of `emit`.
    * /!\ Should only be used when it is strictly mandatory /!\
    *     Doesn't ensure passing only JSON values.
    *     Mainly used by context-menu in order to avoid breaking it.
    */
   emitSync: function emitSync(...args) {
-    return emitToContent(this, args);
+    // because the arguments could be also non JSONable values,
+    // we need to ensure the array instance is created from
+    // the content's sandbox
+    return emitToContent(this, new modelFor(this).sandbox.Array(...args));
   },
 
   /**
    * Configures sandbox and loads content scripts into it.
    * @param {Worker} worker
    *    content worker
    */
   initialize: function WorkerSandbox(worker, window) {
--- a/addon-sdk/source/lib/sdk/panel/window.js
+++ b/addon-sdk/source/lib/sdk/panel/window.js
@@ -30,20 +30,24 @@ function getWindow(anchor) {
     for (let enumWindow of windows) {
       // Check if the anchor is in this browser window.
       if (enumWindow == anchorWindow) {
         window = anchorWindow;
         break;
       }
 
       // Check if the anchor is in a browser tab in this browser window.
-      let browser = enumWindow.gBrowser.getBrowserForDocument(anchorDocument);
-      if (browser) {
-        window = enumWindow;
-        break;
+      try {
+        let browser = enumWindow.gBrowser.getBrowserForDocument(anchorDocument);
+        if (browser) {
+          window = enumWindow;
+          break;
+        }
+      }
+      catch (e) {
       }
 
       // Look in other subdocuments (sidebar, etc.)?
     }
   }
 
   // If we didn't find the anchor's window (or we have no anchor),
   // return the most recent browser window.
--- a/addon-sdk/source/test/jetpack-package.ini
+++ b/addon-sdk/source/test/jetpack-package.ini
@@ -1,15 +1,16 @@
 [DEFAULT]
 support-files =
   buffers/**
   commonjs-test-adapter/**
   event/**
   fixtures/**
   loader/**
+  libs/**
   modules/**
   private-browsing/**
   sidebar/**
   tabs/**
   traits/**
   windows/**
   zip/**
   fixtures.js
--- a/b2g/chrome/content/devtools/hud.js
+++ b/b2g/chrome/content/devtools/hud.js
@@ -51,18 +51,19 @@ let developerHUD = {
    * observed metrics with `target.register(metric)`, and keep them up-to-date
    * with `target.update(metric, message)` when necessary.
    */
   registerWatcher: function dwp_registerWatcher(watcher) {
     this._watchers.unshift(watcher);
   },
 
   init: function dwp_init() {
-    if (this._client)
+    if (this._client) {
       return;
+    }
 
     if (!DebuggerServer.initialized) {
       RemoteDebugger.initServer();
     }
 
     // We instantiate a local debugger connection so that watchers can use our
     // DebuggerClient to send requests to tab actors (e.g. the consoleActor).
     // Note the special usage of the private _serverConnection, which we need
@@ -86,36 +87,38 @@ let developerHUD = {
     }
 
     SettingsListener.observe('hud.logging', this._logging, enabled => {
       this._logging = enabled;
     });
   },
 
   uninit: function dwp_uninit() {
-    if (!this._client)
+    if (!this._client) {
       return;
+    }
 
     for (let frame of this._targets.keys()) {
       this.untrackFrame(frame);
     }
 
     AppFrames.removeObserver(this);
 
     this._client.close();
     delete this._client;
   },
 
   /**
    * This method will ask all registered watchers to track and update metrics
    * on an app frame.
    */
   trackFrame: function dwp_trackFrame(frame) {
-    if (this._targets.has(frame))
+    if (this._targets.has(frame)) {
       return;
+    }
 
     DebuggerServer.connectToChild(this._conn, frame).then(actor => {
       let target = new Target(frame, actor);
       this._targets.set(frame, target);
 
       for (let w of this._watchers) {
         w.trackTarget(target);
       }
@@ -334,56 +337,56 @@ let consoleWatcher = {
 
     switch (packet.type) {
 
       case 'pageError':
         let pageError = packet.pageError;
 
         if (pageError.warning || pageError.strict) {
           metric.name = 'warnings';
-          output += 'warning (';
+          output += 'Warning (';
         } else {
           metric.name = 'errors';
-          output += 'error (';
+          output += 'Error (';
         }
 
         if (this._security.indexOf(pageError.category) > -1) {
           metric.name = 'security';
         }
 
         let {errorMessage, sourceName, category, lineNumber, columnNumber} = pageError;
         output += category + '): "' + (errorMessage.initial || errorMessage) +
           '" in ' + sourceName + ':' + lineNumber + ':' + columnNumber;
         break;
 
       case 'consoleAPICall':
         switch (packet.message.level) {
 
           case 'error':
             metric.name = 'errors';
-            output += 'error (console)';
+            output += 'Error (console)';
             break;
 
           case 'warn':
             metric.name = 'warnings';
-            output += 'warning (console)';
+            output += 'Warning (console)';
             break;
 
           default:
             return;
         }
         break;
 
       case 'reflowActivity':
         metric.name = 'reflows';
 
         let {start, end, sourceURL, interruptible} = packet;
         metric.interruptible = interruptible;
         let duration = Math.round((end - start) * 100) / 100;
-        output += 'reflow: ' + duration + 'ms';
+        output += 'Reflow: ' + duration + 'ms';
         if (sourceURL) {
           output += ' ' + this.formatSourceURL(packet);
         }
         break;
 
       default:
         return;
     }
@@ -420,16 +423,17 @@ let eventLoopLagWatcher = {
 
     SettingsListener.observe('hud.jank', false, this.settingsListener.bind(this));
   },
 
   settingsListener: function(value) {
     if (this._active == value) {
       return;
     }
+
     this._active = value;
 
     // Toggle the state of existing fronts.
     let fronts = this._fronts;
     for (let target of fronts.keys()) {
       if (value) {
         fronts.get(target).start();
       } else {
@@ -441,17 +445,17 @@ let eventLoopLagWatcher = {
 
   trackTarget: function(target) {
     target.register('jank');
 
     let front = new EventLoopLagFront(this._client, target.actor);
     this._fronts.set(target, front);
 
     front.on('event-loop-lag', time => {
-      target.update({name: 'jank', value: time}, 'jank: ' + time + 'ms');
+      target.update({name: 'jank', value: time}, 'Jank: ' + time + 'ms');
     });
 
     if (this._active) {
       front.start();
     }
   },
 
   untrackTarget: function(target) {
@@ -495,17 +499,17 @@ let memoryWatcher = {
         watching[category] = watch;
         this.update();
       });
     }
   },
 
   update: function mw_update() {
     let watching = this._watching;
-    let active = watching.memory || watching.uss;
+    let active = watching.appmemory || watching.uss;
 
     if (this._active) {
       for (let target of this._fronts.keys()) {
         if (!watching.appmemory) target.clear({name: 'memory'});
         if (!watching.uss) target.clear({name: 'uss'});
         if (!active) clearTimeout(this._timers.get(target));
       }
     } else if (active) {
@@ -514,58 +518,69 @@ let memoryWatcher = {
       }
     }
     this._active = active;
   },
 
   measure: function mw_measure(target) {
     let watch = this._watching;
     let front = this._fronts.get(target);
+    let format = this.formatMemory;
 
     if (watch.uss) {
       front.residentUnique().then(value => {
-        target.update({name: 'uss', value: value});
+        target.update({name: 'uss', value: value}, 'USS: ' + format(value));
       }, err => {
         console.error(err);
       });
     }
 
     if (watch.appmemory) {
       front.measure().then(data => {
         let total = 0;
-        if (watch.jsobjects) {
-          total += parseInt(data.jsObjectsSize);
-        }
-        if (watch.jsstrings) {
-          total += parseInt(data.jsStringsSize);
-        }
-        if (watch.jsother) {
-          total += parseInt(data.jsOtherSize);
+        let details = [];
+
+        function item(name, condition, value) {
+          if (!condition) {
+            return;
+          }
+
+          let v = parseInt(value);
+          total += v;
+          details.push(name + ': ' + format(v));
         }
-        if (watch.dom) {
-          total += parseInt(data.domSize);
-        }
-        if (watch.style) {
-          total += parseInt(data.styleSize);
-        }
-        if (watch.other) {
-          total += parseInt(data.otherSize);
-        }
+
+        item('JS objects', watch.jsobjects, data.jsObjectsSize);
+        item('JS strings', watch.jsstrings, data.jsStringsSize);
+        item('JS other', watch.jsother, data.jsOtherSize);
+        item('DOM', watch.dom, data.domSize);
+        item('Style', watch.style, data.styleSize);
+        item('Other', watch.other, data.otherSize);
         // TODO Also count images size (bug #976007).
 
-        target.update({name: 'memory', value: total});
+        target.update({name: 'memory', value: total},
+          'App Memory: ' + format(total) + ' (' + details.join(', ') + ')');
       }, err => {
         console.error(err);
       });
     }
 
-    let timer = setTimeout(() => this.measure(target), 500);
+    let timer = setTimeout(() => this.measure(target), 800);
     this._timers.set(target, timer);
   },
 
+  formatMemory: function mw_formatMemory(bytes) {
+    var prefix = ['','K','M','G','T','P','E','Z','Y'];
+    var i = 0;
+    for (; bytes > 1024 && i < prefix.length; ++i) {
+      bytes /= 1024;
+    }
+    return (Math.round(bytes * 100) / 100) + ' ' + prefix[i] + 'B';
+  },
+
   trackTarget: function mw_trackTarget(target) {
     target.register('uss');
     target.register('memory');
     this._fronts.set(target, MemoryFront(this._client, target.actor));
     if (this._active) {
       this.measure(target);
     }
   },
--- a/b2g/components/FilePicker.js
+++ b/b2g/components/FilePicker.js
@@ -176,17 +176,17 @@ FilePicker.prototype = {
 
     let data = message.data;
     if (!data.success || !data.result.blob) {
       this.fireError();
       return;
     }
 
     // The name to be shown can be part of the message, or can be taken from
-    // the DOMFile (if the blob is a DOMFile).
+    // the File (if the blob is a File).
     let name = data.result.name;
     if (!name &&
         (data.result.blob instanceof this.mParent.File) &&
         data.result.blob.name) {
       name = data.result.blob.name;
     }
 
     // Let's try to remove the full path and take just the filename.
@@ -202,19 +202,19 @@ FilePicker.prototype = {
         let mimeSvc = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
         let mimeInfo = mimeSvc.getFromTypeAndExtension(data.result.blob.type, '');
         if (mimeInfo) {
           name += '.' + mimeInfo.primaryExtension;
         }
       }
     }
 
-    let file = new this.mParent.File(data.result.blob,
-                                     { name: name,
-                                       type: data.result.blob.type });
+    let file = new this.mParent.File([data.result.blob],
+                                     name,
+                                     { type: data.result.blob.type });
 
     if (file) {
       this.fireSuccess(file);
     } else {
       this.fireError();
     }
   }
 };
--- a/b2g/components/test/mochitest/test_filepicker_path.html
+++ b/b2g/components/test/mochitest/test_filepicker_path.html
@@ -62,17 +62,17 @@ var testCases = [
                               blob: new File(['1234567890'],
                                              'useless-name.txt',
                                              { type: 'text/plain' }),
                               name: 'test5.txt'
                             }
                 },
     fileName: 'test5.txt'},
   // case 6: returns file without name. This case may fail because we
-  //         need to make sure the DOMFile can be sent through
+  //         need to make sure the File can be sent through
   //         sendAsyncMessage API.
   { pickedResult: { success: true,
                     result: {
                               type: 'text/plain',
                               blob: new File(['1234567890'],
                                              'test6.txt',
                                              { type: 'text/plain' })
                             }
@@ -91,17 +91,17 @@ chromeScript.addMessageListener('file-pi
 function handleMessage(data) {
   var fileInput = document.getElementById('fileInput');
   switch (data.type) {
     case 'pick-result-updated':
       fileInput.click();
       break;
     case 'file-picked-posted':
       is(fileInput.value, activeTestCase.fileName,
-         'DOMFile should be able to send through message.');
+         'File should be able to send through message.');
       processTestCase();
       break;
   }
 }
 
 function processTestCase() {
   if (!testCases.length) {
     SimpleTest.finish();
@@ -122,9 +122,9 @@ function processTestCase() {
     expectedResult.result.name = name
   }
   chromeScript.sendAsyncMessage('update-pick-result', expectedResult);
 }
 
 </script>
 <input type="file" id="fileInput">
 </body>
-</html>
\ No newline at end of file
+</html>
--- 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="3a2947df41a480de1457a6dcdbf46ad0af70d8e0">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="83f495a1c12687970f7f2840c2729795c4b88177"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="cc5da7b055e2b06fdeb46fa94970550392ee571d"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <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="693382a5630079f9debfe55c59f8197d432f47ff"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <!-- 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"/>
@@ -128,16 +128,16 @@
   <project name="platform/external/icu4c" path="external/icu4c" revision="2bb01561780583cc37bc667f0ea79f48a122d8a2"/>
   <!-- dolphin specific things -->
   <project name="device/sprd" path="device/sprd" revision="3a0f1b51e3b27b36b9df484f3c286b6099889f6e"/>
   <project name="platform/external/wpa_supplicant_8" path="external/wpa_supplicant_8" revision="4e58336019b5cbcfd134caf55b142236cf986618"/>
   <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="f365109310138f85bb91884b7dee60f6f0da042d"/>
+  <project name="kernel/common" path="kernel" revision="250294fb70e018b0966402f744ff9705109f8635"/>
   <project name="platform/system/core" path="system/core" revision="53d584d4a4b4316e4de9ee5f210d662f89b44e7e"/>
   <project name="u-boot" path="u-boot" revision="982c1fd67b89d5573317c1796cf5b0143de44e8a"/>
   <project name="vendor/sprd/gps" path="vendor/sprd/gps" revision="6974f8e771d4d8e910357a6739ab124768891e8f"/>
   <project name="vendor/sprd/open-source" path="vendor/sprd/open-source" revision="1d4697b16ed039fd1de0a23bda150523e743e2ad"/>
   <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/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="84923f1940625c47ff4c1fdf01b10fde3b7d909e">
     <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="83f495a1c12687970f7f2840c2729795c4b88177"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="cc5da7b055e2b06fdeb46fa94970550392ee571d"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c058843242068d0df7c107e09da31b53d2e08fa6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="693382a5630079f9debfe55c59f8197d432f47ff"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
   <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/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="8986df0f82e15ac2798df0b6c2ee3435400677ac">
     <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="83f495a1c12687970f7f2840c2729795c4b88177"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="cc5da7b055e2b06fdeb46fa94970550392ee571d"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="693382a5630079f9debfe55c59f8197d432f47ff"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <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"/>
--- 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="3a2947df41a480de1457a6dcdbf46ad0af70d8e0">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="83f495a1c12687970f7f2840c2729795c4b88177"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="cc5da7b055e2b06fdeb46fa94970550392ee571d"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <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="693382a5630079f9debfe55c59f8197d432f47ff"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <!-- 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"/>
--- 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="84923f1940625c47ff4c1fdf01b10fde3b7d909e">
     <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="83f495a1c12687970f7f2840c2729795c4b88177"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="cc5da7b055e2b06fdeb46fa94970550392ee571d"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="cd88d860656c31c7da7bb310d6a160d0011b0961"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="c058843242068d0df7c107e09da31b53d2e08fa6"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="693382a5630079f9debfe55c59f8197d432f47ff"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
   <project name="platform/bionic" path="bionic" revision="c72b8f6359de7ed17c11ddc9dfdde3f615d188a9"/>
   <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/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="3a2947df41a480de1457a6dcdbf46ad0af70d8e0">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="83f495a1c12687970f7f2840c2729795c4b88177"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="cc5da7b055e2b06fdeb46fa94970550392ee571d"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <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="693382a5630079f9debfe55c59f8197d432f47ff"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <!-- 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"/>
--- 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="8986df0f82e15ac2798df0b6c2ee3435400677ac">
     <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="83f495a1c12687970f7f2840c2729795c4b88177"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="cc5da7b055e2b06fdeb46fa94970550392ee571d"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="693382a5630079f9debfe55c59f8197d432f47ff"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <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": "665042c75b7d6b0a1f4b8f07f79cd998abce68b0", 
+    "revision": "eeeae73691f91cd5042660b0f19c84747ebc7be2", 
     "repo_path": "/integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/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="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="84923f1940625c47ff4c1fdf01b10fde3b7d909e">
     <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="83f495a1c12687970f7f2840c2729795c4b88177"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="cc5da7b055e2b06fdeb46fa94970550392ee571d"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <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="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="693382a5630079f9debfe55c59f8197d432f47ff"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
   <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/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -10,18 +10,18 @@
   <!--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="84923f1940625c47ff4c1fdf01b10fde3b7d909e">
     <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="83f495a1c12687970f7f2840c2729795c4b88177"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="cc5da7b055e2b06fdeb46fa94970550392ee571d"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <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="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
   <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" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="575fdbf046e966a5915b1f1e800e5d6ad0ea14c0"/>
--- 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="8986df0f82e15ac2798df0b6c2ee3435400677ac">
     <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="83f495a1c12687970f7f2840c2729795c4b88177"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="cc5da7b055e2b06fdeb46fa94970550392ee571d"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="693382a5630079f9debfe55c59f8197d432f47ff"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <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"/>
--- 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="84923f1940625c47ff4c1fdf01b10fde3b7d909e">
     <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="83f495a1c12687970f7f2840c2729795c4b88177"/>
-  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="5883a99b6528ced9dafaed8d3ca2405fb285537e"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="cc5da7b055e2b06fdeb46fa94970550392ee571d"/>
+  <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="cc1f362ce43dce92ac786187ff4abf39060094bd"/>
   <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="562d357b72279a9e35d4af5aeecc8e1ffa2f44f1"/>
-  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="693382a5630079f9debfe55c59f8197d432f47ff"/>
+  <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6ca2008ac50b163d31244ef9f036cb224f4f229b"/>
   <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" revision="cd5dfce80bc3f0139a56b58aca633202ccaee7f8"/>
   <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/confvars.sh
+++ b/b2g/confvars.sh
@@ -55,16 +55,15 @@ MOZ_PAY=1
 MOZ_TOOLKIT_SEARCH=
 MOZ_PLACES=
 MOZ_B2G=1
 
 if test "$OS_TARGET" = "Android"; then
 MOZ_NUWA_PROCESS=1
 MOZ_B2G_LOADER=1
 fi
-MOZ_FOLD_LIBS=1
 
 MOZ_JSDOWNLOADS=1
 
 MOZ_BUNDLED_FONTS=1
 
 JSGC_GENERATIONAL=1
 JS_GC_SMALL_CHUNK_SIZE=1
--- a/b2g/installer/Makefile.in
+++ b/b2g/installer/Makefile.in
@@ -52,18 +52,20 @@ endif
 endif
 
 include $(topsrcdir)/toolkit/mozapps/installer/packager.mk
 
 # Note that JS_BINARY can be defined in packager.mk, so this test must come after
 # including that file. MOZ_PACKAGER_MINIFY_JS is used in packager.mk, but since
 # recipe evaluation is deferred, we can set it here after the inclusion.
 ifneq (,$(JS_BINARY))
+ifndef MOZ_DEBUG
 MOZ_PACKAGER_MINIFY_JS=1
 endif
+endif
 
 ifeq (bundle, $(MOZ_FS_LAYOUT))
 BINPATH = $(_BINPATH)
 DEFINES += -DAPPNAME=$(_APPNAME)
 else
 # Every other platform just winds up in dist/bin
 BINPATH = bin
 endif
--- a/browser/app/blocklist.xml
+++ b/browser/app/blocklist.xml
@@ -1,10 +1,10 @@
 <?xml version="1.0"?>
-<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1409700581000">
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1412277891000">
   <emItems>
       <emItem  blockID="i454" id="sqlmoz@facebook.com">
                         <versionRange  minVersion="0" maxVersion="*" severity="3">
                     </versionRange>
                                 <versionRange  minVersion="0" maxVersion="*" severity="3">
                     </versionRange>
                     <prefs>
               </prefs>
@@ -213,16 +213,22 @@
               </prefs>
     </emItem>
       <emItem  blockID="i77" id="{fa277cfc-1d75-4949-a1f9-4ac8e41b2dfd}">
                         <versionRange  minVersion="0" maxVersion="*">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
+      <emItem  blockID="i710" id="{e0352044-1439-48ba-99b6-b05ed1a4d2de}">
+                        <versionRange  minVersion="0" maxVersion="*" severity="3">
+                    </versionRange>
+                    <prefs>
+              </prefs>
+    </emItem>
       <emItem  blockID="i40" id="{28387537-e3f9-4ed7-860c-11e69af4a8a0}">
                         <versionRange  minVersion="0.1" maxVersion="4.3.1.00" severity="1">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i491" id="{515b2424-5911-40bd-8a2c-bdb20286d8f5}">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
@@ -278,16 +284,22 @@
       <emItem  blockID="i630" id="webbooster@iminent.com">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                     <prefs>
                   <pref>browser.startup.homepage</pref>
                   <pref>browser.search.defaultenginename</pref>
               </prefs>
     </emItem>
+      <emItem  blockID="i8" id="{B13721C7-F507-4982-B2E5-502A71474FED}">
+                        <versionRange  minVersion=" " severity="1">
+                    </versionRange>
+                    <prefs>
+              </prefs>
+    </emItem>
       <emItem  blockID="i7" id="{2224e955-00e9-4613-a844-ce69fccaae91}">
                           <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i174" id="info@thebflix.com">
                         <versionRange  minVersion="0" maxVersion="*" severity="3">
                     </versionRange>
                     <prefs>
@@ -474,16 +486,22 @@
               </prefs>
     </emItem>
       <emItem  blockID="i111" os="WINNT" id="{C3949AC2-4B17-43ee-B4F1-D26B9D42404D}">
                         <versionRange  minVersion="0" maxVersion="15.0.5" severity="1">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
+      <emItem  blockID="i716" id="{cc6cc772-f121-49e0-b1f0-c26583cb0c5e}">
+                        <versionRange  minVersion="0" maxVersion="*" severity="3">
+                    </versionRange>
+                    <prefs>
+              </prefs>
+    </emItem>
       <emItem  blockID="i136" id="Adobe@flash.com">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i672" id="/^(saamazon@mybrowserbar\.com)|(saebay@mybrowserbar\.com)$/">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
@@ -570,16 +588,24 @@
               </prefs>
     </emItem>
       <emItem  blockID="i42" id="{D19CA586-DD6C-4a0a-96F8-14644F340D60}">
                         <versionRange  minVersion="0.1" maxVersion="14.4.0" severity="1">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
+      <emItem  blockID="i628" id="ffxtlbr@iminent.com">
+                        <versionRange  minVersion="0" maxVersion="*" severity="1">
+                    </versionRange>
+                    <prefs>
+                  <pref>browser.startup.homepage</pref>
+                  <pref>browser.search.defaultenginename</pref>
+              </prefs>
+    </emItem>
       <emItem  blockID="i449" id="gystqfr@ylgga.com">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i502" id="{df6bb2ec-333b-4267-8c4f-3f27dc8c6e07}">
                         <versionRange  minVersion="0" maxVersion="*" severity="3">
@@ -630,22 +656,21 @@
               </prefs>
     </emItem>
       <emItem  blockID="i358" id="lfind@nijadsoft.net">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
-      <emItem  blockID="i628" id="ffxtlbr@iminent.com">
+      <emItem  blockID="i720" id="FXqG@xeeR.net">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                     <prefs>
                   <pref>browser.startup.homepage</pref>
-                  <pref>browser.search.defaultenginename</pref>
               </prefs>
     </emItem>
       <emItem  blockID="i228" id="crossriderapp5060@crossrider.com">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
@@ -797,18 +822,18 @@
               </prefs>
     </emItem>
       <emItem  blockID="i678" id="{C4A4F5A0-4B89-4392-AFAC-D58010E349AF}">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
-      <emItem  blockID="i8" id="{B13721C7-F507-4982-B2E5-502A71474FED}">
-                        <versionRange  minVersion=" " severity="1">
+      <emItem  blockID="i106" os="WINNT" id="{97E22097-9A2F-45b1-8DAF-36AD648C7EF4}">
+                        <versionRange  minVersion="0" maxVersion="15.0.5" severity="1">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i552" id="jid0-O6MIff3eO5dIGf5Tcv8RsJDKxrs@jetpack">
                         <versionRange  minVersion="0" maxVersion="*" severity="3">
                     </versionRange>
                     <prefs>
@@ -929,16 +954,23 @@
               </prefs>
     </emItem>
       <emItem  blockID="i477" id="mbrnovone@facebook.com">
                         <versionRange  minVersion="0" maxVersion="*" severity="3">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
+      <emItem  blockID="i718" id="G4Ce4@w.net">
+                        <versionRange  minVersion="0" maxVersion="*" severity="1">
+                    </versionRange>
+                    <prefs>
+                  <pref>browser.startup.homepage</pref>
+              </prefs>
+    </emItem>
       <emItem  blockID="i13" id="{E8E88AB0-7182-11DF-904E-6045E0D72085}">
                           <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i446" id="{E90FA778-C2B7-41D0-9FA9-3FEC1CA54D66}">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                     <prefs>
@@ -1443,18 +1475,18 @@
               </prefs>
     </emItem>
       <emItem  blockID="i354" id="{c0c2693d-2ee8-47b4-9df7-b67a0ee31988}">
                         <versionRange  minVersion="0" maxVersion="*" severity="1">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
-      <emItem  blockID="i106" os="WINNT" id="{97E22097-9A2F-45b1-8DAF-36AD648C7EF4}">
-                        <versionRange  minVersion="0" maxVersion="15.0.5" severity="1">
+      <emItem  blockID="i714" id="{25dd52dc-89a8-469d-9e8f-8d483095d1e8}">
+                        <versionRange  minVersion="0" maxVersion="*" severity="3">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i46" id="{841468a1-d7f4-4bd3-84e6-bb0f13a06c64}">
                         <versionRange  minVersion="0.1" maxVersion="*">
                       <targetApplication  id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
                               <versionRange  minVersion="9.0a1" maxVersion="9.0" />
@@ -1925,16 +1957,22 @@
               </prefs>
     </emItem>
       <emItem  blockID="i73" id="a1g0a9g219d@a1.com">
                         <versionRange  minVersion="0" maxVersion="*">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
+      <emItem  blockID="i712" id="{a2bfe612-4cf5-48ea-907c-f3fb25bc9d6b}">
+                        <versionRange  minVersion="0" maxVersion="*" severity="3">
+                    </versionRange>
+                    <prefs>
+              </prefs>
+    </emItem>
       <emItem  blockID="i96" id="youtubeee@youtuber3.com">
                         <versionRange  minVersion="0" maxVersion="*">
                     </versionRange>
                     <prefs>
               </prefs>
     </emItem>
       <emItem  blockID="i706" id="thefoxonlybetter@quicksaver">
                         <versionRange  minVersion="1.10" maxVersion="*" severity="3">
--- a/browser/app/firefox.exe.manifest
+++ b/browser/app/firefox.exe.manifest
@@ -28,15 +28,16 @@
 </ms_asmv3:trustInfo>
   <ms_asmv3:application xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
     <ms_asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
       <dpiAware>true</dpiAware>
     </ms_asmv3:windowsSettings>
   </ms_asmv3:application>
   <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
     <application>
+      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
       <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
       <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
       <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
       <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
     </application>
   </compatibility>
 </assembly>
--- a/browser/app/macbuild/Contents/MacOS-files.in
+++ b/browser/app/macbuild/Contents/MacOS-files.in
@@ -1,9 +1,10 @@
 /*.app/***
 /*.dylib
 /certutil
 /firefox-bin
+/gtest/***
 /pk12util
 /ssltunnel
 /webapprt-stub
 /xpcshell
 /XUL
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1285,16 +1285,19 @@ pref("services.sync.prefs.sync.security.
 pref("services.sync.prefs.sync.security.default_personal_cert", true);
 pref("services.sync.prefs.sync.security.tls.version.min", true);
 pref("services.sync.prefs.sync.security.tls.version.max", true);
 pref("services.sync.prefs.sync.signon.rememberSignons", true);
 pref("services.sync.prefs.sync.spellchecker.dictionary", true);
 pref("services.sync.prefs.sync.xpinstall.whitelist.required", true);
 #endif
 
+// Developer edition preferences
+pref("browser.devedition.theme.enabled", false);
+
 // Disable the error console
 pref("devtools.errorconsole.enabled", false);
 
 // Developer toolbar and GCLI preferences
 pref("devtools.toolbar.enabled", true);
 pref("devtools.toolbar.visible", false);
 pref("devtools.commands.dir", "");
 
@@ -1597,36 +1600,54 @@ pref("loop.throttled", false);
 pref("loop.enabled", true);
 pref("loop.throttled", true);
 pref("loop.soft_start_ticket_number", -1);
 pref("loop.soft_start_hostname", "soft-start.loop.services.mozilla.com");
 #endif
 
 pref("loop.server", "https://loop.services.mozilla.com");
 pref("loop.seenToS", "unseen");
+pref("loop.learnMoreUrl", "https://www.firefox.com/hello/");
 pref("loop.legal.ToS_url", "https://call.mozilla.com/legal/terms/");
 pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/");
 pref("loop.do_not_disturb", false);
 pref("loop.ringtone", "chrome://browser/content/loop/shared/sounds/Firefox-Long.ogg");
 pref("loop.retry_delay.start", 60000);
 pref("loop.retry_delay.limit", 300000);
 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:*");
+#else
+pref("loop.CSP", "default-src 'self' about: file: chrome:; 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");
+#endif
 pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto");
 pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds");
+pref("loop.rooms.enabled", false);
+pref("loop.fxa_oauth.tokendata", "");
+pref("loop.fxa_oauth.profile", "");
 
 // serverURL to be assigned by services team
 pref("services.push.serverURL", "wss://push.services.mozilla.com/");
 
 pref("social.sidebar.unload_timeout_ms", 10000);
 
+// activation from inside of share panel is possible if activationPanelEnabled
+// is true. Pref'd off for release while usage testing is done through beta.
+#ifdef EARLY_BETA_OR_EARLIER
+pref("social.share.activationPanelEnabled", true);
+#else
+pref("social.share.activationPanelEnabled", false);
+#endif
+pref("social.shareDirectory", "https://activations.cdn.mozilla.net/sharePanel.html");
+
 pref("dom.identity.enabled", false);
 
 // Block insecure active content on https pages
 pref("security.mixed_content.block_active_content", true);
 
 // 1 = allow MITM for certificate pinning checks.
 pref("security.cert_pinning.enforcement_level", 1);
 
new file mode 100644
--- /dev/null
+++ b/browser/base/content/aboutProviderDirectory.xhtml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!-- 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/. -->
+
+<!DOCTYPE html [
+  <!ENTITY % htmlDTD
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "DTD/xhtml1-strict.dtd">
+  %htmlDTD;
+  <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+  %brandDTD;
+  <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
+  %browserDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+  <head>
+    <title>&social.directory.label;</title>
+    <link rel="stylesheet" type="text/css" media="all"
+          href="chrome://browser/skin/aboutProviderDirectory.css"/>
+  </head>
+
+  <body>
+    <div id="activation-link" hidden="true">
+      <div id="message-box">
+        <p>&social.directory.text;</p>
+      </div>
+      <div id="button-box">
+        <button onclick="openDirectory()">&social.directory.button;</button>
+      </div>
+    </div>
+    <div id="activation" hidden="true">
+      <p>&social.directory.introText;</p>
+      <div><iframe id="activation-frame"/></div>
+      <p><a class="link" onclick="openDirectory()">&social.directory.viewmore.text;</a></p>
+    </div>
+  </body>
+
+  <script type="text/javascript;version=1.8"><![CDATA[
+    const Cu = Components.utils;
+
+    Cu.import("resource://gre/modules/Services.jsm");
+
+    function openDirectory() {
+      let url = Services.prefs.getCharPref("social.directories").split(',')[0];
+      window.open(url);
+      window.close();
+    }
+    
+    if (Services.prefs.getBoolPref("social.share.activationPanelEnabled")) {
+      let url = Services.prefs.getCharPref("social.shareDirectory");
+      document.getElementById("activation-frame").setAttribute("src", url);
+      document.getElementById("activation").removeAttribute("hidden");
+    } else {
+      document.getElementById("activation-link").removeAttribute("hidden");
+    }
+  ]]></script>
+</html>
--- a/browser/base/content/aboutSocialError.xhtml
+++ b/browser/base/content/aboutSocialError.xhtml
@@ -37,37 +37,36 @@
     Cu.import("resource://gre/modules/Services.jsm");
     Cu.import("resource:///modules/Social.jsm");
 
     let config = {
       tryAgainCallback: reloadProvider
     }
 
     function parseQueryString() {
-      let url = document.documentURI;
-      let queryString = url.replace(/^about:socialerror\??/, "");
-
-      let modeMatch = queryString.match(/mode=([^&]+)/);
-      let mode = modeMatch && modeMatch[1] ? modeMatch[1] : "";
-      let originMatch = queryString.match(/origin=([^&]+)/);
-      config.origin = originMatch && originMatch[1] ? decodeURIComponent(originMatch[1]) : "";
+      let searchParams = new URLSearchParams(location.href.split("?")[1]);
+      let mode = searchParams.get("mode");
+      config.directory = searchParams.get("directory");
+      config.origin = searchParams.get("origin");
+      let encodedURL = searchParams.get("url");
+      let url = decodeURIComponent(encodedURL);
+      if (config.directory) {
+        let URI = Services.io.newURI(url, null, null);
+        config.origin = Services.scriptSecurityManager.getNoAppCodebasePrincipal(URI).origin;
+      }
 
       switch (mode) {
         case "compactInfo":
           document.getElementById("btnTryAgain").style.display = 'none';
           document.getElementById("btnCloseSidebar").style.display = 'none';
           break;
         case "tryAgainOnly":
           document.getElementById("btnCloseSidebar").style.display = 'none';
           //intentional fall-through
         case "tryAgain":
-          let urlMatch = queryString.match(/url=([^&]+)/);
-          let encodedURL = urlMatch && urlMatch[1] ? urlMatch[1] : "";
-          url = decodeURIComponent(encodedURL);
-
           config.tryAgainCallback = loadQueryURL;
           config.queryURL = url;
           break;
         case "workerFailure":
           config.tryAgainCallback = reloadProvider;
           break;
         default:
           break;
@@ -75,17 +74,17 @@
     }
 
     function setUpStrings() {
       let brandBundle = Services.strings.createBundle("chrome://branding/locale/brand.properties");
       let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
 
       let productName = brandBundle.GetStringFromName("brandShortName");
       let provider = Social._getProviderFromOrigin(config.origin);
-      let providerName = provider && provider.name;
+      let providerName = provider ? provider.name : config.origin;
 
       // Sets up the error message
       let msg = browserBundle.formatStringFromName("social.error.message", [productName, providerName], 2);
       document.getElementById("main-error-msg").textContent = msg;
 
       // Sets up the buttons' labels and accesskeys
       let btnTryAgain = document.getElementById("btnTryAgain");
       btnTryAgain.textContent = browserBundle.GetStringFromName("social.error.tryAgain.label");
--- a/browser/base/content/browser-context.inc
+++ b/browser/base/content/browser-context.inc
@@ -67,18 +67,18 @@
                 accesskey="&openLinkInPrivateWindowCmd.accesskey;"
                 oncommand="gContextMenu.openLinkInPrivateWindow();"/>
       <menuseparator id="context-sep-open"/>
       <menuitem id="context-bookmarklink"
                 label="&bookmarkThisLinkCmd.label;"
                 accesskey="&bookmarkThisLinkCmd.accesskey;"
                 oncommand="gContextMenu.bookmarkLink();"/>
       <menuitem id="context-sharelink"
-                label="&shareLinkCmd.label;"
-                accesskey="&shareLinkCmd.accesskey;"
+                label="&shareLink.label;"
+                accesskey="&shareLink.accesskey;"
                 oncommand="gContextMenu.shareLink();"/>
       <menuitem id="context-savelink"
                 label="&saveLinkCmd.label;"
                 accesskey="&saveLinkCmd.accesskey;"
                 oncommand="gContextMenu.saveLink();"/>
       <menu id="context-marklinkMenu" label="&social.marklinkMenu.label;"
             accesskey="&social.marklinkMenu.accesskey;">
         <menupopup/>
@@ -195,18 +195,18 @@
                 accesskey="&copyAudioURLCmd.accesskey;"
                 oncommand="gContextMenu.copyMediaLocation();"/>
       <menuseparator id="context-sep-copyimage"/>
       <menuitem id="context-saveimage"
                 label="&saveImageCmd.label;"
                 accesskey="&saveImageCmd.accesskey;"
                 oncommand="gContextMenu.saveMedia();"/>
       <menuitem id="context-shareimage"
-                label="&shareImageCmd.label;"
-                accesskey="&shareImageCmd.accesskey;"
+                label="&shareImage.label;"
+                accesskey="&shareImage.accesskey;"
                 oncommand="gContextMenu.shareImage();"/>
       <menuitem id="context-sendimage"
                 label="&emailImageCmd.label;"
                 accesskey="&emailImageCmd.accesskey;"
                 oncommand="gContextMenu.sendMedia();"/>
       <menuitem id="context-setDesktopBackground"
                 label="&setDesktopBackgroundCmd.label;"
                 accesskey="&setDesktopBackgroundCmd.accesskey;"
@@ -220,18 +220,18 @@
                 accesskey="&viewImageDescCmd.accesskey;"
                 oncommand="gContextMenu.viewImageDesc(event);"
                 onclick="checkForMiddleClick(this, event);"/>
       <menuitem id="context-savevideo"
                 label="&saveVideoCmd.label;"
                 accesskey="&saveVideoCmd.accesskey;"
                 oncommand="gContextMenu.saveMedia();"/>
       <menuitem id="context-sharevideo"
-                label="&shareVideoCmd.label;"
-                accesskey="&shareVideoCmd.accesskey;"
+                label="&shareVideo.label;"
+                accesskey="&shareVideo.accesskey;"
                 oncommand="gContextMenu.shareVideo();"/>
       <menuitem id="context-saveaudio"
                 label="&saveAudioCmd.label;"
                 accesskey="&saveAudioCmd.accesskey;"
                 oncommand="gContextMenu.saveMedia();"/>
       <menuitem id="context-video-saveimage"
                 accesskey="&videoSaveImage.accesskey;"
                 label="&videoSaveImage.label;"
@@ -300,18 +300,18 @@
       <menuseparator id="context-sep-selectall"/>
       <menuitem id="context-keywordfield"
                 label="&keywordfield.label;"
                 accesskey="&keywordfield.accesskey;"
                 oncommand="AddKeywordForSearchField();"/>
       <menuitem id="context-searchselect"
                 oncommand="BrowserSearch.loadSearchFromContext(this.searchTerms);"/>
       <menuitem id="context-shareselect"
-                label="&shareSelectCmd.label;"
-                accesskey="&shareSelectCmd.accesskey;"
+                label="&shareSelect.label;"
+                accesskey="&shareSelect.accesskey;"
                 oncommand="gContextMenu.shareSelect(getBrowserSelection());"/>
       <menuseparator id="frame-sep"/>
       <menu id="frame" label="&thisFrameMenu.label;" accesskey="&thisFrameMenu.accesskey;">
         <menupopup>
           <menuitem id="context-showonlythisframe"
                     label="&showOnlyThisFrameCmd.label;"
                     accesskey="&showOnlyThisFrameCmd.accesskey;"
                     oncommand="gContextMenu.showOnlyThisFrame();"/>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/browser-devedition.js
@@ -0,0 +1,85 @@
+# 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/.
+
+/**
+ * Listeners for the DevEdition theme.  This adds an extra stylesheet
+ * to browser.xul if a pref is set and no other themes are applied.
+ */
+let DevEdition = {
+  _prefName: "browser.devedition.theme.enabled",
+  _themePrefName: "general.skins.selectedSkin",
+  _lwThemePrefName: "lightweightThemes.isThemeSelected",
+  _devtoolsThemePrefName: "devtools.theme",
+
+  styleSheetLocation: "chrome://browser/skin/devedition.css",
+  styleSheet: null,
+
+  init: function () {
+    this._updateDevtoolsThemeAttribute();
+    this._updateStyleSheet();
+
+    // Listen for changes to all prefs except for complete themes.
+    // No need for this since changing a complete theme requires a
+    // restart.
+    Services.prefs.addObserver(this._lwThemePrefName, this, false);
+    Services.prefs.addObserver(this._prefName, this, false);
+    Services.prefs.addObserver(this._devtoolsThemePrefName, this, false);
+  },
+
+  observe: function (subject, topic, data) {
+    if (topic == "nsPref:changed") {
+      if (data == this._devtoolsThemePrefName) {
+        this._updateDevtoolsThemeAttribute();
+      } else {
+        this._updateStyleSheet();
+      }
+    }
+  },
+
+  _updateDevtoolsThemeAttribute: function() {
+    // Set an attribute on root element to make it possible
+    // to change colors based on the selected devtools theme.
+    document.documentElement.setAttribute("devtoolstheme",
+      Services.prefs.getCharPref(this._devtoolsThemePrefName));
+  },
+
+  _updateStyleSheet: function() {
+    // Only try to apply the dev edition theme if it is preffered
+    // on and there are no other themes applied.
+    let lightweightThemeSelected = false;
+    try {
+      lightweightThemeSelected = Services.prefs.getBoolPref(this._lwThemePrefName);
+    } catch(e) {}
+
+    let defaultThemeSelected = false;
+    try {
+       defaultThemeSelected = Services.prefs.getCharPref(this._themePrefName) == "classic/1.0";
+    } catch(e) {}
+
+    let deveditionThemeEnabled = Services.prefs.getBoolPref(this._prefName) &&
+      !lightweightThemeSelected && defaultThemeSelected;
+
+    if (deveditionThemeEnabled && !this.styleSheet) {
+      let styleSheetAttr = `href="${this.styleSheetLocation}" type="text/css"`;
+      let styleSheet = this.styleSheet = document.createProcessingInstruction(
+        'xml-stylesheet', styleSheetAttr);
+      this.styleSheet.addEventListener("load", function onLoad() {
+        styleSheet.removeEventListener("load", onLoad);
+        ToolbarIconColor.inferFromText();
+      });
+      document.insertBefore(this.styleSheet, document.documentElement);
+    } else if (!deveditionThemeEnabled && this.styleSheet) {
+      this.styleSheet.remove();
+      this.styleSheet = null;
+      ToolbarIconColor.inferFromText();
+    }
+  },
+
+  uninit: function () {
+    Services.prefs.removeObserver(this._lwThemePrefName, this);
+    Services.prefs.removeObserver(this._prefName, this);
+    Services.prefs.removeObserver(this._devtoolsThemePrefName, this);
+    this.styleSheet = null;
+  }
+};
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -114,31 +114,32 @@
 #ifdef E10S_TESTING_ONLY
     <command id="Tools:RemoteWindow"
       oncommand="OpenBrowserWindow({remote: true});"/>
     <command id="Tools:NonRemoteWindow"
       oncommand="OpenBrowserWindow({remote: false});"/>
 #endif
     <command id="History:UndoCloseTab" oncommand="undoCloseTab();"/>
     <command id="History:UndoCloseWindow" oncommand="undoCloseWindow();"/>
-    <command id="Social:SharePage" oncommand="SocialShare.sharePage();" disabled="true"/>
+    <command id="Social:SharePage" oncommand="SocialShare.sharePage();"/>
     <command id="Social:ToggleSidebar" oncommand="SocialSidebar.toggleSidebar();" hidden="true"/>
     <command id="Social:ToggleNotifications" oncommand="Social.toggleNotifications();" hidden="true"/>
     <command id="Social:Addons" oncommand="BrowserOpenAddonsMgr('addons://list/service');"/>
     <command id="Chat:Focus" oncommand="Cu.import('resource:///modules/Chat.jsm', {}).Chat.focus(window);"/>
   </commandset>
 
   <commandset id="placesCommands">
     <command id="Browser:ShowAllBookmarks"
              oncommand="PlacesCommandHook.showPlacesOrganizer('AllBookmarks');"/>
     <command id="Browser:ShowAllHistory"
              oncommand="PlacesCommandHook.showPlacesOrganizer('History');"/>
   </commandset>
 
   <broadcasterset id="mainBroadcasterSet">
+    <broadcaster id="Social:PageShareOrMark" disabled="true"/>
     <broadcaster id="viewBookmarksSidebar" autoCheck="false" label="&bookmarksButton.label;"
                  type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/bookmarks/bookmarksPanel.xul"
                  oncommand="toggleSidebar('viewBookmarksSidebar');"/>
 
     <!-- for both places and non-places, the sidebar lives at
          chrome://browser/content/history/history-panel.xul so there are no
          problems when switching between versions -->
     <broadcaster id="viewHistorySidebar" autoCheck="false" sidebartitle="&historyButton.label;"
--- a/browser/base/content/browser-social.js
+++ b/browser/base/content/browser-social.js
@@ -43,16 +43,22 @@ XPCOMUtils.defineLazyGetter(this, "Creat
 });
 
 XPCOMUtils.defineLazyGetter(this, "CreateSocialMarkWidget", function() {
   let tmp = {};
   Cu.import("resource:///modules/Social.jsm", tmp);
   return tmp.CreateSocialMarkWidget;
 });
 
+XPCOMUtils.defineLazyGetter(this, "hookWindowCloseForPanelClose", function() {
+  let tmp = {};
+  Cu.import("resource://gre/modules/MozSocialAPI.jsm", tmp);
+  return tmp.hookWindowCloseForPanelClose;
+});
+
 SocialUI = {
   _initialized: false,
 
   // Called on delayed startup to initialize the UI
   init: function SocialUI_init() {
     if (this._initialized) {
       return;
     }
@@ -63,17 +69,17 @@ SocialUI = {
     Services.obs.addObserver(this, "social:providers-changed", false);
     Services.obs.addObserver(this, "social:provider-reload", false);
     Services.obs.addObserver(this, "social:provider-enabled", false);
     Services.obs.addObserver(this, "social:provider-disabled", false);
 
     Services.prefs.addObserver("social.toast-notifications.enabled", this, false);
 
     gBrowser.addEventListener("ActivateSocialFeature", this._activationEventHandler.bind(this), true, true);
-    PanelUI.panel.addEventListener("popupshown", SocialUI.updatePanelState, true);
+    CustomizableUI.addListener(this);
 
     // menupopups that list social providers. we only populate them when shown,
     // and if it has not been done already.
     document.getElementById("viewSidebarMenu").addEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
     document.getElementById("social-statusarea-popup").addEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
 
     Social.init().then((update) => {
       if (update)
@@ -96,18 +102,18 @@ SocialUI = {
     Services.obs.removeObserver(this, "social:profile-changed");
     Services.obs.removeObserver(this, "social:frameworker-error");
     Services.obs.removeObserver(this, "social:providers-changed");
     Services.obs.removeObserver(this, "social:provider-reload");
     Services.obs.removeObserver(this, "social:provider-enabled");
     Services.obs.removeObserver(this, "social:provider-disabled");
 
     Services.prefs.removeObserver("social.toast-notifications.enabled", this);
+    CustomizableUI.removeListener(this);
 
-    PanelUI.panel.removeEventListener("popupshown", SocialUI.updatePanelState, true);
     document.getElementById("viewSidebarMenu").removeEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
     document.getElementById("social-statusarea-popup").removeEventListener("popupshowing", SocialSidebar.populateSidebarMenu, true);
 
     this._initialized = false;
   },
 
   observe: function SocialUI_observe(subject, topic, data) {
     switch (topic) {
@@ -161,39 +167,40 @@ SocialUI = {
   },
 
   _providersChanged: function() {
     SocialSidebar.clearProviderMenus();
     SocialSidebar.update();
     SocialShare.populateProviderMenu();
     SocialStatus.populateToolbarPalette();
     SocialMarks.populateToolbarPalette();
-    SocialShare.update();
   },
 
   // This handles "ActivateSocialFeature" events fired against content documents
   // in this window.  If this activation happens from within Firefox, such as
   // about:home or the share panel, we bypass the enable prompt. Any website
   // activation, such as from the activations directory or a providers website
   // will still get the prompt.
-  _activationEventHandler: function SocialUI_activationHandler(e, aBypassUserEnable=false) {
+  _activationEventHandler: function SocialUI_activationHandler(e, options={}) {
     let targetDoc;
     let node;
     if (e.target instanceof HTMLDocument) {
       // version 0 support
       targetDoc = e.target;
       node = targetDoc.documentElement
     } else {
       targetDoc = e.target.ownerDocument;
       node = e.target;
     }
     if (!(targetDoc instanceof HTMLDocument))
       return;
 
-    if (!aBypassUserEnable && targetDoc.defaultView != content)
+    // The share panel iframe will not match "content" so it passes a bypass
+    // flag
+    if (!options.bypassContentCheck && targetDoc.defaultView != content)
       return;
 
     // If we are in PB mode, we silently do nothing (bug 829404 exists to
     // do something sensible here...)
     if (PrivateBrowsingUtils.isWindowPrivate(window))
       return;
 
     // If the last event was received < 1s ago, ignore this one
@@ -219,21 +226,46 @@ SocialUI = {
         return;
       }
     }
     Social.installProvider(targetDoc, data, function(manifest) {
       Social.activateFromOrigin(manifest.origin, function(provider) {
         if (provider.sidebarURL) {
           SocialSidebar.show(provider.origin);
         }
+        if (provider.shareURL) {
+          // Ensure that the share button is somewhere usable.
+          // SocialShare.shareButton may return null if it is in the menu-panel
+          // and has never been visible, so we check the widget directly. If
+          // there is no area for the widget we move it into the toolbar.
+          let widget = CustomizableUI.getWidget("social-share-button");
+          if (!widget.areaType) {
+            CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_NAVBAR);
+            // ensure correct state
+            SocialUI.onCustomizeEnd(window);
+          }
+
+          // make this new provider the selected provider. If the panel hasn't
+          // been opened, we need to make the frame first.
+          SocialShare._createFrame();
+          SocialShare.iframe.setAttribute('src', 'data:text/plain;charset=utf8,');
+          SocialShare.iframe.setAttribute('origin', provider.origin);
+          // get the right button selected
+          SocialShare.populateProviderMenu();
+          if (SocialShare.panel.state == "open") {
+            SocialShare.sharePage(provider.origin);
+          }
+        }
         if (provider.postActivationURL) {
-          openUILinkIn(provider.postActivationURL, "tab");
+          // if activated from an open share panel, we load the landing page in
+          // a background tab
+          gBrowser.loadOneTab(provider.postActivationURL, {inBackground: SocialShare.panel.state == "open"});
         }
       });
-    }, aBypassUserEnable);
+    }, options);
   },
 
   showLearnMore: function() {
     let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api";
     openUILinkIn(url, "tab");
   },
 
   closeSocialPanelForLinkTraversal: function (target, linkNode) {
@@ -273,31 +305,59 @@ SocialUI = {
 
   get enabled() {
     // Returns whether social is enabled *for this window*.
     if (this._chromeless || PrivateBrowsingUtils.isWindowPrivate(window))
       return false;
     return Social.providers.length > 0;
   },
 
-  updatePanelState :function(event) {
-    // we only want to update when the panel is initially opened, not during
-    // multiview changes
-    if (event.target != PanelUI.panel)
+  canShareOrMarkPage: function(aURI) {
+    // Bug 898706 we do not enable social in private sessions since frameworker
+    // would be shared between private and non-private windows
+    if (PrivateBrowsingUtils.isWindowPrivate(window))
+      return false;
+
+    return (aURI && (aURI.schemeIs('http') || aURI.schemeIs('https')));
+  },
+
+  onCustomizeEnd: function(aWindow) {
+    if (aWindow != window)
       return;
-    SocialUI.updateState();
+    // customization mode gets buttons out of sync with command updating, fix
+    // the disabled state
+    let canShare = this.canShareOrMarkPage(gBrowser.currentURI);
+    let shareButton = SocialShare.shareButton;
+    if (shareButton) {
+      if (canShare) {
+        shareButton.removeAttribute("disabled")
+      } else {
+        shareButton.setAttribute("disabled", "true")
+      }
+    }
+    // update the disabled state of the button based on the command
+    for (let node of SocialMarks.nodes) {
+      if (canShare) {
+        node.removeAttribute("disabled")
+      } else {
+        node.setAttribute("disabled", "true")
+      }
+    }
   },
 
   // called on tab/urlbar/location changes and after customization. Update
   // anything that is tab specific.
   updateState: function() {
+    if (location == "about:customizing")
+      return;
+    goSetCommandEnabled("Social:PageShareOrMark", this.canShareOrMarkPage(gBrowser.currentURI));
     if (!SocialUI.enabled)
       return;
+    // larger update that may change button icons
     SocialMarks.update();
-    SocialShare.update();
   }
 }
 
 SocialFlyout = {
   get panel() {
     return document.getElementById("social-flyout-panel");
   },
 
@@ -428,16 +488,22 @@ SocialFlyout = {
           Cu.reportError(e);
         }
       }
     });
   }
 }
 
 SocialShare = {
+  get _dynamicResizer() {
+    delete this._dynamicResizer;
+    this._dynamicResizer = new DynamicResizeWatcher();
+    return this._dynamicResizer;
+  },
+
   // Share panel may be attached to the overflow or menu button depending on
   // customization, we need to manage open state of the anchor.
   get anchor() {
     let widget = CustomizableUI.getWidget("social-share-button");
     return widget.forWindow(window).anchor;
   },
   get panel() {
     return document.getElementById("social-share-panel");
@@ -446,143 +512,107 @@ SocialShare = {
   get iframe() {
     // first element is our menu vbox.
     if (this.panel.childElementCount == 1)
       return null;
     else
       return this.panel.lastChild;
   },
 
+  _activationHandler: function(event) {
+    if (!Services.prefs.getBoolPref("social.share.activationPanelEnabled"))
+      return;
+    SocialUI._activationEventHandler(event, { bypassContentCheck: true, bypassInstallPanel: true });
+  },
+
   uninit: function () {
     if (this.iframe) {
+      this.iframe.removeEventListener("ActivateSocialFeature", this._activationHandler, true, true);
       this.iframe.remove();
     }
   },
 
   _createFrame: function() {
     let panel = this.panel;
-    if (!SocialUI.enabled || this.iframe)
+    if (this.iframe)
       return;
     this.panel.hidden = false;
     // create and initialize the panel for this window
     let iframe = document.createElement("browser");
     iframe.setAttribute("type", "content");
     iframe.setAttribute("class", "social-share-frame");
     iframe.setAttribute("context", "contentAreaContextMenu");
     iframe.setAttribute("tooltip", "aHTMLTooltip");
     iframe.setAttribute("disableglobalhistory", "true");
     iframe.setAttribute("flex", "1");
     panel.appendChild(iframe);
+    this.iframe.addEventListener("ActivateSocialFeature", this._activationHandler, true, true);
     this.populateProviderMenu();
   },
 
   getSelectedProvider: function() {
     let provider;
     let lastProviderOrigin = this.iframe && this.iframe.getAttribute("origin");
     if (lastProviderOrigin) {
       provider = Social._getProviderFromOrigin(lastProviderOrigin);
     }
-    // if they have a provider selected in the sidebar use that for the initial
-    // default in share
-    if (!provider)
-      provider = SocialSidebar.provider;
-    // if our provider has no shareURL, select the first one that does
-    if (!provider || !provider.shareURL) {
-      let providers = [p for (p of Social.providers) if (p.shareURL)];
-      provider = providers.length > 0  && providers[0];
-    }
     return provider;
   },
 
+  createTooltip: function(event) {
+    let tt = event.target;
+    let provider = Social._getProviderFromOrigin(tt.triggerNode.getAttribute("origin"));
+    tt.firstChild.setAttribute("value", provider.name);
+    tt.lastChild.setAttribute("value", provider.origin);
+  },
+
   populateProviderMenu: function() {
     if (!this.iframe)
       return;
     let providers = [p for (p of Social.providers) if (p.shareURL)];
     let hbox = document.getElementById("social-share-provider-buttons");
-    // selectable providers are inserted before the provider-menu seperator,
-    // remove any menuitems in that area
-    while (hbox.firstChild) {
+    // remove everything before the add-share-provider button (which should also
+    // be lastChild if any share providers were added)
+    let addButton = document.getElementById("add-share-provider");
+    while (hbox.firstChild != addButton) {
       hbox.removeChild(hbox.firstChild);
     }
-    // reset our share toolbar
-    // only show a selection if there is more than one
-    if (!SocialUI.enabled || providers.length < 2) {
-      this.panel.firstChild.hidden = true;
-      return;
-    }
     let selectedProvider = this.getSelectedProvider();
     for (let provider of providers) {
       let button = document.createElement("toolbarbutton");
       button.setAttribute("class", "toolbarbutton share-provider-button");
       button.setAttribute("type", "radio");
       button.setAttribute("group", "share-providers");
       button.setAttribute("image", provider.iconURL);
-      button.setAttribute("tooltiptext", provider.name);
+      button.setAttribute("tooltip", "share-button-tooltip");
       button.setAttribute("origin", provider.origin);
-      button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin')); this.checked=true;");
+      button.setAttribute("oncommand", "SocialShare.sharePage(this.getAttribute('origin'));");
       if (provider == selectedProvider) {
         this.defaultButton = button;
       }
-      hbox.appendChild(button);
+      hbox.insertBefore(button, addButton);
     }
     if (!this.defaultButton) {
-      this.defaultButton = hbox.firstChild
+      this.defaultButton = addButton;
     }
     this.defaultButton.setAttribute("checked", "true");
-    this.panel.firstChild.hidden = false;
   },
 
   get shareButton() {
     // web-panels (bookmark/sidebar) don't include customizableui, so
     // nsContextMenu fails when accessing shareButton, breaking
     // browser_bug409481.js.
     if (!window.CustomizableUI)
       return null;
     let widget = CustomizableUI.getWidget("social-share-button");
     if (!widget || !widget.areaType)
       return null;
     return widget.forWindow(window).node;
   },
 
-  canSharePage: function(aURI) {
-    // we do not enable sharing from private sessions
-    if (PrivateBrowsingUtils.isWindowPrivate(window))
-      return false;
-
-    if (!aURI || !(aURI.schemeIs('http') || aURI.schemeIs('https')))
-      return false;
-    return true;
-  },
-
-  update: function() {
-    let widget = CustomizableUI.getWidget("social-share-button");
-    if (!widget)
-      return;
-    let shareButton = widget.forWindow(window).node;
-    // hidden state is based on available share providers and location of
-    // button. It's always visible and disabled in the customization palette.
-    shareButton.hidden = !SocialUI.enabled || (widget.areaType &&
-                         [p for (p of Social.providers) if (p.shareURL)].length == 0);
-    let disabled = !widget.areaType || shareButton.hidden || !this.canSharePage(gBrowser.currentURI);
-
-    // 1. update the relevent command's disabled state so the keyboard
-    // shortcut only works when available.
-    // 2. If the button has been relocated to a place that is not visible by
-    // default (e.g. menu panel) then the disabled attribute will not update
-    // correctly based on the command, so we update the attribute directly as.
-    let cmd = document.getElementById("Social:SharePage");
-    if (disabled) {
-      cmd.setAttribute("disabled", "true");
-      shareButton.setAttribute("disabled", "true");
-    } else {
-      cmd.removeAttribute("disabled");
-      shareButton.removeAttribute("disabled");
-    }
-  },
-
   _onclick: function() {
     Services.telemetry.getHistogramById("SOCIAL_PANEL_CLICKS").add(0);
   },
   
   onShowing: function() {
     this.anchor.setAttribute("open", "true");
     this.iframe.addEventListener("click", this._onclick, true);
   },
@@ -602,46 +632,44 @@ SocialShare = {
     }
   },
 
   setErrorMessage: function() {
     let iframe = this.iframe;
     if (!iframe)
       return;
 
-    iframe.removeAttribute("src");
-    iframe.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" +
-                                 encodeURIComponent(iframe.getAttribute("origin")),
-                                 null, null, null, null);
+    let url;
+    let origin = iframe.getAttribute("origin");
+    if (!origin) {
+      // directory site is down
+      url = "about:socialerror?mode=tryAgainOnly&directory=1&url=" + encodeURIComponent(iframe.getAttribute("src"));
+    } else {
+      url = "about:socialerror?mode=compactInfo&origin=" + encodeURIComponent(origin);
+    }
+    iframe.webNavigation.loadURI(url, null, null, null, null);
     sizeSocialPanelToContent(this.panel, iframe);
   },
 
   sharePage: function(providerOrigin, graphData, target) {
     // if providerOrigin is undefined, we use the last-used provider, or the
     // current/default provider.  The provider selection in the share panel
     // will call sharePage with an origin for us to switch to.
     this._createFrame();
     let iframe = this.iframe;
-    let provider;
-    if (providerOrigin)
-      provider = Social._getProviderFromOrigin(providerOrigin);
-    else
-      provider = this.getSelectedProvider();
-    if (!provider || !provider.shareURL)
-      return;
 
     // graphData is an optional param that either defines the full set of data
     // to be shared, or partial data about the current page. It is set by a call
     // in mozSocial API, or via nsContentMenu calls. If it is present, it MUST
     // define at least url. If it is undefined, we're sharing the current url in
     // the browser tab.
     let pageData = graphData ? graphData : this.currentShare;
     let sharedURI = pageData ? Services.io.newURI(pageData.url, null, null) :
                                 gBrowser.currentURI;
-    if (!this.canSharePage(sharedURI))
+    if (!SocialUI.canShareOrMarkPage(sharedURI))
       return;
 
     // the point of this action type is that we can use existing share
     // endpoints (e.g. oexchange) that do not support additional
     // socialapi functionality.  One tweak is that we shoot an event
     // containing the open graph data.
     if (!pageData || sharedURI == gBrowser.currentURI) {
       pageData = OpenGraphBuilder.getData(gBrowser);
@@ -653,42 +681,48 @@ SocialShare = {
       }
     }
     // if this is a share of a selected item, get any microdata
     if (!pageData.microdata && target) {
       pageData.microdata = OpenGraphBuilder.getMicrodata(gBrowser, target);
     }
     this.currentShare = pageData;
 
+    let provider;
+    if (providerOrigin)
+      provider = Social._getProviderFromOrigin(providerOrigin);
+    else
+      provider = this.getSelectedProvider();
+    if (!provider || !provider.shareURL) {
+      this.showDirectory();
+      return;
+    }
+    // check the menu button
+    let hbox = document.getElementById("social-share-provider-buttons");
+    let btn = hbox.querySelector("[origin='" + provider.origin + "']");
+    if (btn)
+      btn.checked = true;
+
     let shareEndpoint = OpenGraphBuilder.generateEndpointURL(provider.shareURL, pageData);
 
     let size = provider.getPageSize("share");
     if (size) {
-      if (this._dynamicResizer) {
-        this._dynamicResizer.stop();
-        this._dynamicResizer = null;
-      }
-      let {width, height} = size;
-      width += this.panel.boxObject.width - iframe.boxObject.width;
-      height += this.panel.boxObject.height - iframe.boxObject.height;
-      this.panel.sizeTo(width, height);
-    } else {
-      this._dynamicResizer = new DynamicResizeWatcher();
+      this._dynamicResizer.stop();
     }
 
     // if we've already loaded this provider/page share endpoint, we don't want
     // to add another load event listener.
     let reload = true;
     let endpointMatch = shareEndpoint == iframe.getAttribute("src");
     let docLoaded = iframe.contentDocument && iframe.contentDocument.readyState == "complete";
     if (endpointMatch && docLoaded) {
       reload = shareEndpoint != iframe.contentDocument.location.spec;
     }
     if (!reload) {
-      if (this._dynamicResizer)
+      if (!size)
         this._dynamicResizer.start(this.panel, iframe);
       iframe.docShell.isActive = true;
       iframe.docShell.isAppTab = true;
       let evt = iframe.contentDocument.createEvent("CustomEvent");
       evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(pageData));
       iframe.contentDocument.documentElement.dispatchEvent(evt);
     } else {
       // first time load, wait for load and dispatch after load
@@ -696,17 +730,23 @@ SocialShare = {
         iframe.removeEventListener("load", panelBrowserOnload, true);
         iframe.docShell.isActive = true;
         iframe.docShell.isAppTab = true;
         // to support standard share endpoints mimick window.open by setting
         // window.opener, some share endpoints rely on w.opener to know they
         // should close the window when done.
         iframe.contentWindow.opener = iframe.contentWindow;
         setTimeout(function() {
-          if (SocialShare._dynamicResizer) { // may go null if hidden quickly
+          if (size) {
+            let panel = SocialShare.panel;
+            let {width, height} = size;
+            width += panel.boxObject.width - iframe.boxObject.width;
+            height += panel.boxObject.height - iframe.boxObject.height;
+            panel.sizeTo(width, height);
+          } else {
             SocialShare._dynamicResizer.start(iframe.parentNode, iframe);
           }
         }, 0);
         let evt = iframe.contentDocument.createEvent("CustomEvent");
         evt.initCustomEvent("OpenGraphData", true, true, JSON.stringify(pageData));
         iframe.contentDocument.documentElement.dispatchEvent(evt);
       }, true);
     }
@@ -717,20 +757,41 @@ SocialShare = {
       if (purge > 0)
         iframe.sessionHistory.PurgeHistory(purge);
     }
 
     // always ensure that origin belongs to the endpoint
     let uri = Services.io.newURI(shareEndpoint, null, null);
     iframe.setAttribute("origin", provider.origin);
     iframe.setAttribute("src", shareEndpoint);
+    this._openPanel();
+  },
 
+  showDirectory: function() {
+    this._createFrame();
+    let iframe = this.iframe;
+    iframe.removeAttribute("origin");
+    iframe.addEventListener("load", function panelBrowserOnload(e) {
+      iframe.removeEventListener("load", panelBrowserOnload, true);
+      hookWindowCloseForPanelClose(iframe.contentWindow);
+      SocialShare._dynamicResizer.start(iframe.parentNode, iframe);
+
+      iframe.addEventListener("unload", function panelBrowserOnload(e) {
+        iframe.removeEventListener("unload", panelBrowserOnload, true);
+        SocialShare._dynamicResizer.stop();
+      }, true);
+    }, true);
+    iframe.setAttribute("src", "about:providerdirectory");
+    this._openPanel();
+  },
+
+  _openPanel: function() {
     let anchor = document.getAnonymousElementByAttribute(this.anchor, "class", "toolbarbutton-icon");
     this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, false);
-    Social.setErrorListener(iframe, this.setErrorMessage.bind(this));
+    Social.setErrorListener(this.iframe, this.setErrorMessage.bind(this));
     Services.telemetry.getHistogramById("SOCIAL_TOOLBAR_BUTTONS").add(0);
   }
 };
 
 SocialSidebar = {
   _openStartTime: 0,
 
   // Whether the sidebar can be shown for this window.
@@ -1301,30 +1362,37 @@ SocialStatus = {
 
 
 /**
  * SocialMarks
  *
  * Handles updates to toolbox and signals all buttons to update when necessary.
  */
 SocialMarks = {
-  update: function() {
-    // querySelectorAll does not work on the menu panel the panel, so we have to
-    // do this the hard way.
-    let providers = SocialMarks.getProviders();
+  get nodes() {
+    let providers = [p for (p of Social.providers) if (p.markURL)];
     for (let p of providers) {
       let widgetId = SocialMarks._toolbarHelper.idFromOrigin(p.origin);
       let widget = CustomizableUI.getWidget(widgetId);
       if (!widget)
         continue;
       let node = widget.forWindow(window).node;
+      if (node)
+        yield node;
+    }
+  },
+  update: function() {
+    // querySelectorAll does not work on the menu panel, so we have to do this
+    // the hard way.
+    for (let node of this.nodes) {
       // xbl binding is not complete on startup when buttons are not in toolbar,
       // verify update is available
-      if (node && node.update)
+      if (node.update) {
         node.update();
+      }
     }
   },
 
   getProviders: function() {
     // only rely on providers that the user has placed in the UI somewhere. This
     // also means that populateToolbarPalette must be called prior to using this
     // method, otherwise you get a big fat zero. For our use case with context
     // menu's, this is ok.
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -393,18 +393,17 @@ panel[noactions] > richlistbox > richlis
 panel[noactions] > richlistbox > richlistitem[type~="action"] > .ac-url-box > .ac-action-icon {
   visibility: collapse;
 }
 
 panel[noactions] > richlistbox > richlistitem[type~="action"] > .ac-url-box > .ac-url > .ac-url-text {
   visibility: visible;
 }
 
-#urlbar:not([actiontype]) > #urlbar-display-box,
-#urlbar:not([actiontype="switchtab"]) > #urlbar-display-box > .urlbar-display-switchtab {
+#urlbar:not([actiontype="switchtab"]) > #urlbar-display-box {
   display: none;
 }
 
 #PopupAutoComplete {
   -moz-binding: url("chrome://browser/content/urlbarBindings.xml#browser-autocomplete-result-popup");
 }
 
 #PopupAutoCompleteRichResult {
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -194,16 +194,17 @@ let gInitialPages = [
   "about:home",
   "about:privatebrowsing",
   "about:welcomeback",
   "about:sessionrestore"
 ];
 
 #include browser-addons.js
 #include browser-customization.js
+#include browser-devedition.js
 #include browser-feeds.js
 #include browser-fullScreen.js
 #include browser-fullZoom.js
 #include browser-loop.js
 #include browser-places.js
 #include browser-plugins.js
 #include browser-safebrowsing.js
 #include browser-social.js
@@ -782,17 +783,17 @@ function gKeywordURIFixup({ target: brow
   };
 
   gDNSService.asyncResolve(hostName, 0, onLookupComplete, Services.tm.mainThread);
 }
 
 // Called when a docshell has attempted to load a page in an incorrect process.
 // This function is responsible for loading the page in the correct process.
 function RedirectLoad({ target: browser, data }) {
-  let tab = gBrowser._getTabForBrowser(browser);
+  let tab = gBrowser.getTabForBrowser(browser);
   // Flush the tab state before getting it
   TabState.flush(browser);
   let tabState = JSON.parse(SessionStore.getTabState(tab));
 
   if (data.historyIndex < 0) {
     // Add a pseudo-history state for the new url to load
     let newEntry = {
       url: data.uri,
@@ -829,16 +830,17 @@ var gBrowserInit = {
     // These routines add message listeners. They must run before
     // loading the frame script to ensure that we don't miss any
     // message sent between when the frame script is loaded and when
     // the listener is registered.
     DOMLinkHandler.init();
     gPageStyleMenu.init();
     LanguageDetectionListener.init();
     BrowserOnClick.init();
+    DevEdition.init();
 
     let mm = window.getGroupMessageManager("browsers");
     mm.loadFrameScript("chrome://browser/content/content.js", true);
 
     // initialize observers and listeners
     // and give C++ access to gBrowser
     XULBrowserWindow.init();
     window.QueryInterface(Ci.nsIInterfaceRequestor)
@@ -1386,16 +1388,18 @@ var gBrowserInit = {
     BookmarkingUI.uninit();
 
     TabsInTitlebar.uninit();
 
     ToolbarIconColor.uninit();
 
     BrowserOnClick.uninit();
 
+    DevEdition.uninit();
+
     var enumerator = Services.wm.getEnumerator(null);
     enumerator.getNext();
     if (!enumerator.hasMoreElements()) {
       document.persist("sidebar-box", "sidebarcommand");
       document.persist("sidebar-box", "width");
       document.persist("sidebar-box", "src");
       document.persist("sidebar-title", "value");
     }
@@ -2946,26 +2950,26 @@ const DOMLinkHandler = {
         break;
     }
   },
 
   setIcon: function(aBrowser, aURL) {
     if (gBrowser.isFailedIcon(aURL))
       return false;
 
-    let tab = gBrowser._getTabForBrowser(aBrowser);
+    let tab = gBrowser.getTabForBrowser(aBrowser);
     if (!tab)
       return false;
 
     gBrowser.setIcon(tab, aURL);
     return true;
   },
 
   addSearch: function(aBrowser, aEngine, aURL) {
-    let tab = gBrowser._getTabForBrowser(aBrowser);
+    let tab = gBrowser.getTabForBrowser(aBrowser);
     if (!tab)
       return false;
 
     BrowserSearch.addEngine(aBrowser, aEngine, makeURI(aURL));
   },
 }
 
 const BrowserSearch = {
@@ -3765,17 +3769,17 @@ var XULBrowserWindow = {
       } else {
         this.reloadCommand.removeAttribute("disabled");
       }
 
       if (gURLBar) {
         URLBarSetURI(aLocationURI);
 
         BookmarkingUI.onLocationChange();
-        SocialUI.updateState();
+        SocialUI.updateState(location);
       }
 
       // Utility functions for disabling find
       var shouldDisableFind = function shouldDisableFind(aDocument) {
         let docElt = aDocument.documentElement;
         return docElt && docElt.getAttribute("disablefastfind") == "true";
       }
 
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -241,17 +241,21 @@
     <panel id="social-share-panel"
            class="social-panel"
            type="arrow"
            orient="horizontal"
            onpopupshowing="SocialShare.onShowing()"
            onpopuphidden="SocialShare.onHidden()"
            hidden="true">
       <vbox class="social-share-toolbar">
-        <arrowscrollbox id="social-share-provider-buttons" orient="vertical" flex="1"/>
+        <arrowscrollbox id="social-share-provider-buttons" orient="vertical" flex="1">
+          <toolbarbutton id="add-share-provider" class="toolbarbutton share-provider-button" type="radio"
+                         group="share-providers" tooltiptext="&findShareServices.label;"
+                         oncommand="SocialShare.showDirectory()"/>
+        </arrowscrollbox>
       </vbox>
     </panel>
 
     <panel id="social-notification-panel"
            class="social-panel"
            type="arrow"
            hidden="true"
            noautofocus="true"/>
@@ -491,16 +495,21 @@
       <label class="tooltip-label" value="&forwardButton.tooltip;"/>
 #ifdef XP_MACOSX
       <label class="tooltip-label" value="&backForwardButtonMenuMac.tooltip;"/>
 #else
       <label class="tooltip-label" value="&backForwardButtonMenu.tooltip;"/>
 #endif
     </tooltip>
 
+    <tooltip id="share-button-tooltip" onpopupshowing="SocialShare.createTooltip(event);">
+      <label class="tooltip-label"/>
+      <label class="tooltip-label"/>
+    </tooltip>
+
 #include popup-notifications.inc
 
 #include ../../components/customizableui/content/panelUI.inc.xul
 
     <hbox id="downloads-animation-container" mousethrough="always">
       <vbox id="downloads-notification-anchor">
         <vbox id="downloads-indicator-notification"/>
       </vbox>
@@ -664,17 +673,17 @@
            Should you need to add items to the toolbar here, make sure to also add them
            to the default placements of buttons in CustomizableUI.jsm, so the
            customization code doesn't get confused.
       -->
     <toolbar id="nav-bar" class="toolbar-primary chromeclass-toolbar"
              aria-label="&navbarCmd.label;"
              fullscreentoolbar="true" mode="icons" customizable="true"
              iconsize="small"
-             defaultset="urlbar-container,search-container,bookmarks-menu-button,downloads-button,home-button,loop-call-button,social-share-button,social-toolbar-item"
+             defaultset="urlbar-container,search-container,bookmarks-menu-button,downloads-button,home-button,loop-call-button"
              customizationtarget="nav-bar-customization-target"
              overflowable="true"
              overflowbutton="nav-bar-overflow-button"
              overflowtarget="widget-overflow-list"
              overflowpanel="widget-overflow"
              context="toolbar-context-menu">
 
       <hbox id="nav-bar-customization-target" flex="1">
@@ -911,25 +920,16 @@
                        ondragover="homeButtonObserver.onDragOver(event)"
                        ondragenter="homeButtonObserver.onDragOver(event)"
                        ondrop="homeButtonObserver.onDrop(event)"
                        ondragexit="homeButtonObserver.onDragExit(event)"
                        key="goHome"
                        onclick="BrowserGoHome(event);"
                        cui-areatype="toolbar"
                        aboutHomeOverrideTooltip="&abouthome.pageTitle;"/>
-
-        <toolbarbutton id="social-share-button"
-                       class="toolbarbutton-1 chromeclass-toolbar-additional"
-                       label="&sharePageCmd.label;"
-                       tooltiptext="&sharePageCmd.label;"
-                       cui-areatype="toolbar"
-                       removable="true"
-                       hidden="true"
-                       command="Social:SharePage"/>
       </hbox>
 
       <toolbarbutton id="nav-bar-overflow-button"
                      class="toolbarbutton-1 chromeclass-toolbar-additional overflow-button"
                      skipintoolbarset="true"
                      tooltiptext="&navbarOverflow.label;"/>
 
       <toolbaritem id="PanelUI-button"
--- a/browser/base/content/newtab/intro.js
+++ b/browser/base/content/newtab/intro.js
@@ -2,18 +2,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/. */
 #endif
 
 const PREF_INTRO_SHOWN = "browser.newtabpage.introShown";
 
 let gIntro = {
-  _introShown: Services.prefs.getBoolPref(PREF_INTRO_SHOWN),
-
   _nodeIDSuffixes: [
     "panel",
     "what",
   ],
 
   _nodes: {},
 
   init: function() {
@@ -21,17 +19,17 @@ let gIntro = {
       this._nodes[idSuffix] = document.getElementById("newtab-intro-" + idSuffix);
     }
 
     this._nodes.panel.addEventListener("popupshowing", e => this._setUpPanel());
     this._nodes.what.addEventListener("click", e => this.showPanel());
   },
 
   showIfNecessary: function() {
-    if (!this._introShown) {
+    if (!Services.prefs.getBoolPref(PREF_INTRO_SHOWN)) {
       Services.prefs.setBoolPref(PREF_INTRO_SHOWN, true);
       this.showPanel();
     }
   },
 
   showPanel: function() {
     // Point the panel at the 'what' link
     this._nodes.panel.openPopup(this._nodes.what);
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -340,17 +340,17 @@ nsContextMenu.prototype = {
     linkmenus = document.getElementsByClassName("context-marklink");
     [m.hidden = !enableLinkMarkItems for (m of linkmenus)];
 
     // SocialShare
     let shareButton = SocialShare.shareButton;
     let shareEnabled = shareButton && !shareButton.disabled && !this.onSocial;
     let pageShare = shareEnabled && !(this.isContentSelected ||
                             this.onTextInput || this.onLink || this.onImage ||
-                            this.onVideo || this.onAudio);
+                            this.onVideo || this.onAudio || this.onCanvas);
     this.showItem("context-sharepage", pageShare);
     this.showItem("context-shareselect", shareEnabled && this.isContentSelected);
     this.showItem("context-sharelink", shareEnabled && (this.onLink || this.onPlainTextLink) && !this.onMailtoLink);
     this.showItem("context-shareimage", shareEnabled && this.onImage);
     this.showItem("context-sharevideo", shareEnabled && this.onVideo);
     this.setItemAttr("context-sharevideo", "disabled", !this.mediaURL);
   },
 
--- a/browser/base/content/socialmarks.xml
+++ b/browser/base/content/socialmarks.xml
@@ -3,17 +3,17 @@
 <bindings id="socialMarkBindings"
     xmlns="http://www.mozilla.org/xbl"
     xmlns:xbl="http://www.mozilla.org/xbl"
     xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
 
   <binding id="toolbarbutton-marks" display="xul:button"
            extends="chrome://global/content/bindings/toolbarbutton.xml#toolbarbutton">
-    <content disabled="true">
+    <content>
       <xul:panel anonid="panel" hidden="true" type="arrow" class="social-panel"/>
       <xul:image class="toolbarbutton-icon" xbl:inherits="validate,src=image,label"/>
       <xul:label class="toolbarbutton-text" crop="right" flex="1"
                  xbl:inherits="value=label,accesskey,crop,wrap"/>
       <xul:label class="toolbarbutton-multiline-text" flex="1"
                  xbl:inherits="xbl:text=label,accesskey,wrap"/>
     </content>
     <implementation implements="nsIDOMEventListener, nsIObserver">
@@ -108,26 +108,21 @@
           this._dynamicResizer.stop();
           this._dynamicResizer = null;
         }
         this.content.setAttribute("src", "about:blank");
         // called during onhidden, make sure the docshell is updated
         if (this._frame.docShell)
           this._frame.docShell.createAboutBlankContentViewer(null);
 
-        // do we have a savable page loaded?
-        let aURI = gBrowser.currentURI;
-        let disabled = !aURI || !(aURI.schemeIs('http') || aURI.schemeIs('https'));
-        // when overflowed in toolbar, we must have the attribute set
-        if (disabled) {
-          this.setAttribute("disabled", "true");
+        // disabled attr is set by Social:PageShareOrMark command
+        if (this.hasAttribute("disabled")) {
           this.isMarked = false;
         } else {
-          this.removeAttribute("disabled");
-          Social.isURIMarked(provider.origin, aURI, (isMarked) => {
+          Social.isURIMarked(provider.origin, gBrowser.currentURI, (isMarked) => {
             this.isMarked = isMarked;
           });
         }
 
         this.content.setAttribute("origin", provider.origin);
 
         let panel = this.panel;
         // if customization is currently happening, we may not have a panel
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -384,39 +384,55 @@
           // When not using remote browsers, we can take a fast path by getting
           // directly from the content window to the browser without looping
           // over all browsers.
           if (!gMultiProcessBrowser) {
             let browser = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
                                  .getInterface(Ci.nsIWebNavigation)
                                  .QueryInterface(Ci.nsIDocShell)
                                  .chromeEventHandler;
-            return this._getTabForBrowser(browser);
+            return this.getTabForBrowser(browser);
           }
 
           for (let i = 0; i < this.browsers.length; i++) {
             // NB: We use contentWindowAsCPOW so that this code works both
             // for remote browsers as well. aWindow may be a CPOW.
             if (this.browsers[i].contentWindowAsCPOW == aWindow)
               return this.tabs[i];
           }
           return null;
         ]]>
         </body>
       </method>
 
+      <!-- Binding from browser to tab -->
+      <field name="_tabForBrowser" readonly="true">
+      <![CDATA[
+        new WeakMap();
+      ]]>
+      </field>
+
       <method name="_getTabForBrowser">
+        <parameter name="aBrowser" />
+        <body>
+        <![CDATA[
+          let Deprecated = Components.utils.import("resource://gre/modules/Deprecated.jsm", {}).Deprecated;
+          let text = "_getTabForBrowser` is now deprecated, please use `getTabForBrowser";
+          let url = "https://developer.mozilla.org/docs/Mozilla/Tech/XUL/Method/getTabForBrowser";
+          Deprecated.warning(text, url);
+          return this.getTabForBrowser(aBrowser);
+        ]]>
+        </body>
+      </method>
+
+      <method name="getTabForBrowser">
         <parameter name="aBrowser"/>
         <body>
         <![CDATA[
-          for (let i = 0; i < this.tabs.length; i++) {
-            if (this.tabs[i].linkedBrowser == aBrowser)
-              return this.tabs[i];
-          }
-          return null;
+          return this._tabForBrowser.get(aBrowser);
         ]]>
         </body>
       </method>
 
       <method name="getNotificationBox">
         <parameter name="aBrowser"/>
         <body>
           <![CDATA[
@@ -455,17 +471,17 @@
             let promptBox = {
               appendPrompt : function(args, onCloseCallback) {
                 let newPrompt = document.createElementNS(XUL_NS, "tabmodalprompt");
                 stack.appendChild(newPrompt);
                 browser.setAttribute("tabmodalPromptShowing", true);
 
                 newPrompt.clientTop; // style flush to assure binding is attached
 
-                let tab = self._getTabForBrowser(browser);
+                let tab = self.getTabForBrowser(browser);
                 newPrompt.init(args, tab, onCloseCallback);
                 return newPrompt;
               },
 
               removePrompt : function(aPrompt) {
                 stack.removeChild(aPrompt);
 
                 let prompts = this.listPrompts();
@@ -1437,17 +1453,17 @@
           <![CDATA[
             let isRemote = aBrowser.getAttribute("remote") == "true";
             if (isRemote == aShouldBeRemote)
               return false;
 
             let wasActive = document.activeElement == aBrowser;
 
             // Unhook our progress listener.
-            let tab = this._getTabForBrowser(aBrowser);
+            let tab = this.getTabForBrowser(aBrowser);
             let index = tab._tPos;
             let filter = this.mTabFilters[index];
             aBrowser.webProgress.removeProgressListener(filter);
 
             // Change the "remote" attribute.
             let parent = aBrowser.parentNode;
             let permanentKey = aBrowser.permanentKey;
             parent.removeChild(aBrowser);
@@ -1604,16 +1620,17 @@
             notificationbox.setAttribute("flex", "1");
             notificationbox.appendChild(browserSidebarContainer);
 
             var position = this.tabs.length - 1;
             var uniqueId = this._generateUniquePanelID();
             notificationbox.id = uniqueId;
             t.linkedPanel = uniqueId;
             t.linkedBrowser = b;
+            this._tabForBrowser.set(b, t);
             t._tPos = position;
             this.tabContainer._setPositionalAttributes();
 
             // Prevent the superfluous initial load of a blank document
             // if we're going to load something other than about:blank.
             if (!uriIsAboutBlank) {
               b.setAttribute("nodefaultsrc", "true");
             }
@@ -2119,16 +2136,17 @@
 
             // This will unload the document. An unload handler could remove
             // dependant tabs, so it's important that the tabbrowser is now in
             // a consistent state (tab removed, tab positions updated, etc.).
             browser.parentNode.removeChild(browser);
 
             // Release the browser in case something is erroneously holding a
             // reference to the tab after its removal.
+            this._tabForBrowser.delete(aTab.linkedBrowser);
             aTab.linkedBrowser = null;
 
             // As the browser is removed, the removal of a dependent document can
             // cause the whole window to close. So at this point, it's possible
             // that the binding is destructed.
             if (this.mTabBox) {
               this.mPanelContainer.removeChild(panel);
             }
@@ -3023,31 +3041,31 @@
       <method name="receiveMessage">
         <parameter name="aMessage"/>
         <body><![CDATA[
           let json = aMessage.json;
           let browser = aMessage.target;
 
           switch (aMessage.name) {
             case "DOMTitleChanged": {
-              let tab = this._getTabForBrowser(browser);
+              let tab = this.getTabForBrowser(browser);
               if (!tab || tab.hasAttribute("pending"))
                 return;
               let titleChanged = this.setTabTitle(tab);
               if (titleChanged && !tab.selected && !tab.hasAttribute("busy"))
                 tab.setAttribute("titlechanged", "true");
               break;
             }
             case "DOMWindowClose": {
               if (this.tabs.length == 1) {
                 window.close();
                 return;
               }
 
-              let tab = this._getTabForBrowser(browser);
+              let tab = this.getTabForBrowser(browser);
               if (tab) {
                 this.removeTab(tab);
               }
               break;
             }
             case "contextmenu": {
               let spellInfo = aMessage.data.spellInfo;
               if (spellInfo)
@@ -3058,17 +3076,17 @@
                                           spellInfo: spellInfo };
               let popup = browser.ownerDocument.getElementById("contentAreaContextMenu");
               let event = gContextMenuContentData.event;
               let pos = browser.mapScreenCoordinatesFromContent(event.screenX, event.screenY);
               popup.openPopupAtScreen(pos.x, pos.y, true);
               break;
             }
             case "DOMWebNotificationClicked": {
-              let tab = this._getTabForBrowser(browser);
+              let tab = this.getTabForBrowser(browser);
               if (!tab)
                 return;
               this.selectedTab = tab;
               window.focus();
               break;
             }
           }
         ]]></body>
@@ -3089,16 +3107,17 @@
           window.addEventListener("sizemodechange", this, false);
 
           var uniqueId = this._generateUniquePanelID();
           this.mPanelContainer.childNodes[0].id = uniqueId;
           this.mCurrentTab.linkedPanel = uniqueId;
           this.mCurrentTab._tPos = 0;
           this.mCurrentTab._fullyOpen = true;
           this.mCurrentTab.linkedBrowser = this.mCurrentBrowser;
+          this._tabForBrowser.set(this.mCurrentBrowser, this.mCurrentTab);
 
           // set up the shared autoscroll popup
           this._autoScrollPopup = this.mCurrentBrowser._createAutoScrollPopup();
           this._autoScrollPopup.id = "autoscroller";
           this.appendChild(this._autoScrollPopup);
           this.mCurrentBrowser.setAttribute("autoscrollpopup", this._autoScrollPopup.id);
           this.mCurrentBrowser.droppedLinkHandler = handleDroppedLink;
           this.updateWindowResizers();
@@ -3377,17 +3396,17 @@
           // We're about to open a modal dialog, make sure the opening
           // tab is brought to the front.
           // If this is a same-process modal dialog, then we're given its DOM
           // window as the event's target. For remote dialogs, we're given the
           // browser, but that's in the originalTarget.
           // XXX Why originalTarget for the browser?
           this.selectedTab = (event.target instanceof Window) ?
                                this._getTabForContentWindow(event.target.top) :
-                               this._getTabForBrowser(event.originalTarget);
+                               this.getTabForBrowser(event.originalTarget);
         ]]>
       </handler>
       <handler event="DOMTitleChanged">
         <![CDATA[
           if (!event.isTrusted)
             return;
 
           var contentWin = event.target.defaultView;
@@ -3413,17 +3432,17 @@
           let uri = browser.currentURI;
           let icon = browser.mIconURL;
 
           this.updateBrowserRemotenessByURL(browser, "about:tabcrashed");
 
           browser.setAttribute("crashedPageTitle", title);
           browser.docShell.displayLoadError(Cr.NS_ERROR_CONTENT_CRASHED, uri, null);
           browser.removeAttribute("crashedPageTitle");
-          let tab = this._getTabForBrowser(browser);
+          let tab = this.getTabForBrowser(browser);
           this.setIcon(tab, icon);
         ]]>
       </handler>
     </handlers>
   </binding>
 
   <binding id="tabbrowser-tabbox"
            extends="chrome://global/content/bindings/tabbox.xml#tabbox">
@@ -4810,17 +4829,18 @@
                     class="tab-background-middle"/>
           <xul:hbox xbl:inherits="pinned,selected,titlechanged"
                     class="tab-background-end"/>
         </xul:hbox>
         <xul:hbox xbl:inherits="pinned,selected,titlechanged"
                   class="tab-content" align="center">
           <xul:image xbl:inherits="fadein,pinned,busy,progress,selected"
                      class="tab-throbber"
-                     role="presentation"/>
+                     role="presentation"
+                     layer="true" />
           <xul:image xbl:inherits="src=image,fadein,pinned,selected"
                      anonid="tab-icon-image"
                      class="tab-icon-image"
                      validate="never"
                      role="presentation"/>
           <xul:label flex="1"
                      anonid="tab-label"
                      xbl:inherits="value=visibleLabel,crop,accesskey,fadein,pinned,selected"
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -304,16 +304,17 @@ skip-if = e10s
 [browser_contextSearchTabPosition.js]
 skip-if = os == "mac" || e10s # bug 967013, bug 926729
 [browser_ctrlTab.js]
 skip-if = e10s # Bug ????? - thumbnail captures need e10s love (tabPreviews_capture fails with Argument 1 of CanvasRenderingContext2D.drawWindow does not implement interface Window.)
 [browser_customize_popupNotification.js]
 skip-if = e10s
 [browser_datareporting_notification.js]
 run-if = datareporting
+[browser_devedition.js]
 [browser_devices_get_user_media.js]
 skip-if = buildapp == 'mulet' || (os == "linux" && debug) || e10s # linux: bug 976544; e10s: Bug 973001 - appears user media notifications only happen in the child and don't make their way to the parent?
 [browser_devices_get_user_media_about_urls.js]
 skip-if = e10s # Bug 973001 - appears user media notifications only happen in the child and don't make their way to the parent?
 [browser_discovery.js]
 skip-if = e10s # Bug 918663 - DOMLinkAdded events don't make their way to chrome
 [browser_duplicateIDs.js]
 [browser_drag.js]
--- a/browser/base/content/test/general/browser_blob-channelname.js
+++ b/browser/base/content/test/general/browser_blob-channelname.js
@@ -1,11 +1,11 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 Cu.import("resource://gre/modules/NetUtil.jsm");
 
 function test() {
-    var file = new File(new Blob(['test'], {type: 'text/plain'}), {name: 'test-name'});
+    var file = new File([new Blob(['test'], {type: 'text/plain'})], "test-name");
     var url = URL.createObjectURL(file);
     var channel = NetUtil.newChannel(url);
 
     is(channel.contentDispositionFilename, 'test-name', "filename matches");
 }
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_devedition.js
@@ -0,0 +1,66 @@
+/*
+ * Testing changes for Developer Edition theme.
+ * A special stylesheet should be added to the browser.xul document
+ * when browser.devedition.theme.enabled is set to true and no themes
+ * are applied.
+ */
+
+const PREF_DEVEDITION_THEME = "browser.devedition.theme.enabled";
+const PREF_THEME = "general.skins.selectedSkin";
+const PREF_LWTHEME = "lightweightThemes.isThemeSelected";
+const PREF_DEVTOOLS_THEME = "devtools.theme";
+
+registerCleanupFunction(() => {
+  // Set preferences back to their original values
+  Services.prefs.clearUserPref(PREF_DEVEDITION_THEME);
+  Services.prefs.clearUserPref(PREF_THEME);
+  Services.prefs.clearUserPref(PREF_LWTHEME);
+  Services.prefs.clearUserPref(PREF_DEVTOOLS_THEME);
+});
+
+function test() {
+  waitForExplicitFinish();
+  startTests();
+}
+
+function startTests() {
+  ok (!DevEdition.styleSheet, "There is no devedition style sheet by default.");
+
+  info ("Setting browser.devedition.theme.enabled to true.");
+  Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true);
+  ok (DevEdition.styleSheet, "There is a devedition stylesheet when no themes are applied and pref is set.");
+
+  info ("Adding a lightweight theme.");
+  Services.prefs.setBoolPref(PREF_LWTHEME, true);
+  ok (!DevEdition.styleSheet, "The devedition stylesheet has been removed when a lightweight theme is applied.");
+
+  info ("Removing a lightweight theme.");
+  Services.prefs.setBoolPref(PREF_LWTHEME, false);
+  ok (DevEdition.styleSheet, "The devedition stylesheet has been added when a lightweight theme is removed.");
+
+  // There are no listeners for the complete theme pref since applying the theme
+  // requires a restart.
+  info ("Setting general.skins.selectedSkin to a custom string.");
+  Services.prefs.setCharPref(PREF_THEME, "custom-theme");
+  ok (DevEdition.styleSheet, "The devedition stylesheet is still here when a complete theme is added.");
+
+  info ("Resetting general.skins.selectedSkin to default value.");
+  Services.prefs.clearUserPref(PREF_THEME);
+  ok (DevEdition.styleSheet, "The devedition stylesheet is still here when a complete theme is removed.");
+
+  info ("Setting browser.devedition.theme.enabled to false.");
+  Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, false);
+  ok (!DevEdition.styleSheet, "The devedition stylesheet has been removed.");
+
+  info ("Checking :root attributes based on devtools theme.");
+  is (document.documentElement.getAttribute("devtoolstheme"), "light",
+    "The documentElement has an attribute based on devtools theme.");
+  Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark");
+  is (document.documentElement.getAttribute("devtoolstheme"), "dark",
+    "The documentElement has an attribute based on devtools theme.");
+  Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "light");
+  is (document.documentElement.getAttribute("devtoolstheme"), "light",
+    "The documentElement has an attribute based on devtools theme.");
+
+  finish();
+}
--- a/browser/base/content/test/general/browser_devices_get_user_media_about_urls.js
+++ b/browser/base/content/test/general/browser_devices_get_user_media_about_urls.js
@@ -7,16 +7,17 @@ const kObservedTopics = [
   "getUserMedia:revoke",
   "getUserMedia:response:deny",
   "getUserMedia:request",
   "recording-device-events",
   "recording-window-ended"
 ];
 
 const PREF_PERMISSION_FAKE = "media.navigator.permission.fake";
+const PREF_LOOP_CSP = "loop.CSP";
 
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
                                    "@mozilla.org/mediaManagerService;1",
                                    "nsIMediaManagerService");
 
 var gTab;
@@ -157,30 +158,34 @@ fakeLoopAboutModule.prototype = {
            Ci.nsIAboutModule.ALLOW_SCRIPT |
            Ci.nsIAboutModule.HIDE_FROM_ABOUTABOUT;
   }
 };
 
 let factory = XPCOMUtils._getFactory(fakeLoopAboutModule);
 let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
 
+let originalLoopCsp = Services.prefs.getCharPref(PREF_LOOP_CSP);
 registerCleanupFunction(function() {
   gBrowser.removeCurrentTab();
   kObservedTopics.forEach(topic => {
     Services.obs.removeObserver(observer, topic);
   });
   Services.prefs.clearUserPref(PREF_PERMISSION_FAKE);
+  Services.prefs.setCharPref(PREF_LOOP_CSP, originalLoopCsp);
 });
 
 
 let gTests = [
 
 {
   desc: "getUserMedia about:loopconversation shouldn't prompt",
   run: function checkAudioVideoLoop() {
+    Services.prefs.setCharPref(PREF_LOOP_CSP, "default-src 'unsafe-inline'");
+
     let classID = Cc["@mozilla.org/uuid-generator;1"]
                     .getService(Ci.nsIUUIDGenerator).generateUUID();
     registrar.registerFactory(classID, "",
                               "@mozilla.org/network/protocol/about;1?what=loopconversation",
                               factory);
 
     yield loadPage("about:loopconversation");
 
@@ -193,16 +198,17 @@ let gTests = [
     yield promisePopupNotification("webRTC-sharingDevices");
 
     is(getMediaCaptureState(), "CameraAndMicrophone",
        "expected camera and microphone to be shared");
 
     yield closeStream();
 
     registrar.unregisterFactory(classID, factory);
+    Services.prefs.setCharPref(PREF_LOOP_CSP, originalLoopCsp);
   }
 },
 
 {
   desc: "getUserMedia about:evil should prompt",
   run: function checkAudioVideoNonLoop() {
     let classID = Cc["@mozilla.org/uuid-generator;1"]
                     .getService(Ci.nsIUUIDGenerator).generateUUID();
--- a/browser/base/content/test/general/mochitest.ini
+++ b/browser/base/content/test/general/mochitest.ini
@@ -18,17 +18,16 @@ support-files =
   offlineChild.cacheManifest^headers^
   offlineChild.html
   offlineChild2.cacheManifest
   offlineChild2.cacheManifest^headers^
   offlineChild2.html
   offlineEvent.cacheManifest
   offlineEvent.cacheManifest^headers^
   offlineEvent.html
-  privateBrowsingMode.js
   subtst_contextmenu.html
   video.ogg
 
 [test_bug364677.html]
 [test_bug395533.html]
 [test_contextmenu.html]
 skip-if = toolkit == "gtk2" || toolkit == "gtk3" # disabled on Linux due to bug 513558
 [test_contextmenu_input.html]
deleted file mode 100644
--- a/browser/base/content/test/general/privateBrowsingMode.js
+++ /dev/null
@@ -1,3 +0,0 @@
-// This file is only present in per-window private browsing buikds.
-var perWindowPrivateBrowsing = true;
-
--- a/browser/base/content/test/general/test_contextmenu.html
+++ b/browser/base/content/test/general/test_contextmenu.html
@@ -10,18 +10,16 @@
 <body>
 Browser context menu tests.
 <p id="display"></p>
 
 <div id="content">
 </div>
 
 <pre id="test">
-<script> var perWindowPrivateBrowsing = false; </script>
-<script type="text/javascript" src="privateBrowsingMode.js"></script>
 <script type="text/javascript" src="contextmenu_common.js"></script>
 <script class="testbody" type="text/javascript">
 
 SpecialPowers.Cu.import("resource://gre/modules/InlineSpellChecker.jsm", window);
 
 const Ci = SpecialPowers.Ci;
 
 function executeCopyCommand(command, expectedValue)
@@ -106,36 +104,25 @@ function runTest(testNum) {
                          ].concat(inspectItems);
         checkContextMenu(plainTextItems);
         closeContextMenu();
         openContextMenuFor(link); // Invoke context menu for next test.
     },
 
     function () {
         // Context menu for text link
-        if (perWindowPrivateBrowsing) {
-          checkContextMenu(["context-openlinkintab", true,
-                            "context-openlink",      true,
-                            "context-openlinkprivate", true,
-                            "---",                   null,
-                            "context-bookmarklink",  true,
-                            "context-savelink",      true,
-                            "context-copylink",      true,
-                            "context-searchselect",  true
-                           ].concat(inspectItems));
-        } else {
-          checkContextMenu(["context-openlinkintab", true,
-                            "context-openlink",      true,
-                            "---",                   null,
-                            "context-bookmarklink",  true,
-                            "context-savelink",      true,
-                            "context-copylink",      true,
-                            "context-searchselect",  true
-                           ].concat(inspectItems));
-        }
+        checkContextMenu(["context-openlinkintab", true,
+                          "context-openlink",      true,
+                          "context-openlinkprivate", true,
+                          "---",                   null,
+                          "context-bookmarklink",  true,
+                          "context-savelink",      true,
+                          "context-copylink",      true,
+                          "context-searchselect",  true
+                         ].concat(inspectItems));
         closeContextMenu();
         openContextMenuFor(mailto); // Invoke context menu for next test.
     },
 
     function () {
         // Context menu for text mailto-link
         checkContextMenu(["context-copyemail", true,
                           "context-searchselect", true
@@ -582,90 +569,56 @@ function runTest(testNum) {
         selectText(selecttextlink); // Select text prior to opening context menu.
         openContextMenuFor(selecttextlink); // Invoke context menu for next test.
     },
 
     function () {
         // Context menu for selected text which matches valid URL pattern
         if (SpecialPowers.Services.appinfo.OS == "Darwin") {
           // This test is only enabled on Mac due to bug 736399.
-          if (perWindowPrivateBrowsing) {
-            checkContextMenu(["context-openlinkincurrent",           true,
-                              "context-openlinkintab",               true,
-                              "context-openlink",                    true,
-                              "context-openlinkprivate",             true,
-                              "---",                                 null,
-                              "context-bookmarklink",                true,
-                              "context-savelink",                    true,
-                              "context-copy",                        true,
-                              "context-selectall",                   true,
-                              "---",                                 null,
-                              "context-searchselect",                true,
-                              "context-viewpartialsource-selection", true
-                             ].concat(inspectItems));
-          } else {
-            checkContextMenu(["context-openlinkincurrent",           true,
-                              "context-openlinkintab",               true,
-                              "context-openlink",                    true,
-                              "---",                                 null,
-                              "context-bookmarklink",                true,
-                              "context-savelink",                    true,
-                              "context-copy",                        true,
-                              "context-selectall",                   true,
-                              "---",                                 null,
-                              "context-searchselect",                true,
-                              "context-viewpartialsource-selection", true
-                             ].concat(inspectItems));
-          }
+          checkContextMenu(["context-openlinkincurrent",           true,
+                            "context-openlinkintab",               true,
+                            "context-openlink",                    true,
+                            "context-openlinkprivate",             true,
+                            "---",                                 null,
+                            "context-bookmarklink",                true,
+                            "context-savelink",                    true,
+                            "context-copy",                        true,
+                            "context-selectall",                   true,
+                            "---",                                 null,
+                            "context-searchselect",                true,
+                            "context-viewpartialsource-selection", true
+                           ].concat(inspectItems));
         }
         closeContextMenu();
         // clear the selection because following tests don't expect any selection
         subwindow.getSelection().removeAllRanges();
 
         openContextMenuFor(imagelink)
     },
 
     function () {
         // Context menu for image link
-        if (perWindowPrivateBrowsing) {
-          checkContextMenu(["context-openlinkintab", true,
-                            "context-openlink",      true,
-                            "context-openlinkprivate", true,
-                            "---",                   null,
-                            "context-bookmarklink",  true,
-                            "context-savelink",      true,
-                            "context-copylink",      true,
-                            "---",                   null,
-                            "context-viewimage",            true,
-                            "context-copyimage-contents",   true,
-                            "context-copyimage",            true,
-                            "---",                          null,
-                            "context-saveimage",            true,
-                            "context-sendimage",            true,
-                            "context-setDesktopBackground", true,
-                            "context-viewimageinfo",        true
-                           ].concat(inspectItems));
-        } else {
-          checkContextMenu(["context-openlinkintab", true,
-                            "context-openlink",      true,
-                            "---",                   null,
-                            "context-bookmarklink",  true,
-                            "context-savelink",      true,
-                            "context-copylink",      true,
-                            "---",                   null,
-                            "context-viewimage",            true,
-                            "context-copyimage-contents",   true,
-                            "context-copyimage",            true,
-                            "---",                          null,
-                            "context-saveimage",            true,
-                            "context-sendimage",            true,
-                            "context-setDesktopBackground", true,
-                            "context-viewimageinfo",        true
-                           ].concat(inspectItems));
-        }
+        checkContextMenu(["context-openlinkintab", true,
+                          "context-openlink",      true,
+                          "context-openlinkprivate", true,
+                          "---",                   null,
+                          "context-bookmarklink",  true,
+                          "context-savelink",      true,
+                          "context-copylink",      true,
+                          "---",                   null,
+                          "context-viewimage",            true,
+                          "context-copyimage-contents",   true,
+                          "context-copyimage",            true,
+                          "---",                          null,
+                          "context-saveimage",            true,
+                          "context-sendimage",            true,
+                          "context-setDesktopBackground", true,
+                          "context-viewimageinfo",        true
+                         ].concat(inspectItems));
         closeContextMenu();
         selectInputText(select_inputtext); // Select text prior to opening context menu.
         openContextMenuFor(select_inputtext); // Invoke context menu for next test.
     },
 
     function () {
         // Context menu for selected text in input
         checkContextMenu(["context-undo",        false,
@@ -684,20 +637,20 @@ function runTest(testNum) {
         selectInputText(select_inputtext_password); // Select text prior to opening context menu.
         openContextMenuFor(select_inputtext_password); // Invoke context menu for next test.
     },
 
     function () {
         // Context menu for selected text in input[type="password"]
         checkContextMenu(["context-undo",        false,
                           "---",                 null,
-                          "context-cut",         true,
-                          "context-copy",        true,
+                          "context-cut",         false,
+                          "context-copy",        false,
                           "context-paste",       null, // ignore clipboard state
-                          "context-delete",      true,
+                          "context-delete",      false,
                           "---",                 null,
                           "context-selectall",   true,
                           "---",                 null,
                           "spell-check-enabled", true,
                           //spell checker is shown on input[type="password"] on this testcase
                           "spell-dictionaries",  true,
                               ["spell-check-dictionary-en-US", true,
                                "---",                          null,
--- a/browser/base/content/test/social/browser.ini
+++ b/browser/base/content/test/social/browser.ini
@@ -6,16 +6,17 @@ support-files =
   head.js
   opengraph/og_invalid_url.html
   opengraph/opengraph.html
   opengraph/shortlink_linkrel.html
   opengraph/shorturl_link.html
   opengraph/shorturl_linkrel.html
   microdata.html
   share.html
+  share_activate.html
   social_activate.html
   social_activate_iframe.html
   social_chat.html
   social_crash_content_helper.js
   social_flyout.html
   social_mark.html
   social_panel.html
   social_postActivation.html
--- a/browser/base/content/test/social/browser_aboutHome_activation.js
+++ b/browser/base/content/test/social/browser_aboutHome_activation.js
@@ -14,17 +14,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 let snippet =
 '     <script>' +
 '       var manifest = {' +
 '         "name": "Demo Social Service",' +
 '         "origin": "https://example.com",' +
 '         "iconURL": "chrome://branding/content/icon16.png",' +
 '         "icon32URL": "chrome://branding/content/favicon32.png",' +
 '         "icon64URL": "chrome://branding/content/icon64.png",' +
-'         "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",' +
+'         "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",' +
 '         "postActivationURL": "https://example.com/browser/browser/base/content/test/social/social_postActivation.html",' +
 '       };' +
 '       function activateProvider(node) {' +
 '         node.setAttribute("data-service", JSON.stringify(manifest));' +
 '         var event = new CustomEvent("ActivateSocialFeature");' +
 '         node.dispatchEvent(event);' +
 '       }' +
 '     </script>' +
@@ -36,17 +36,17 @@ let snippet =
 let snippet2 =
 '     <script>' +
 '       var manifest = {' +
 '         "name": "Demo Social Service",' +
 '         "origin": "https://example.com",' +
 '         "iconURL": "chrome://branding/content/icon16.png",' +
 '         "icon32URL": "chrome://branding/content/favicon32.png",' +
 '         "icon64URL": "chrome://branding/content/icon64.png",' +
-'         "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",' +
+'         "sidebarURL": "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",' +
 '         "postActivationURL": "https://example.com/browser/browser/base/content/test/social/social_postActivation.html",' +
 '         "oneclick": true' +
 '       };' +
 '       function activateProvider(node) {' +
 '         node.setAttribute("data-service", JSON.stringify(manifest));' +
 '         var event = new CustomEvent("ActivateSocialFeature");' +
 '         node.dispatchEvent(event);' +
 '       }' +
--- a/browser/base/content/test/social/browser_share.js
+++ b/browser/base/content/test/social/browser_share.js
@@ -5,21 +5,57 @@ let baseURL = "https://example.com/brows
 
 let manifest = { // normal provider
   name: "provider 1",
   origin: "https://example.com",
   workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js",
   iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png",
   shareURL: "https://example.com/browser/browser/base/content/test/social/share.html"
 };
+let activationPage = "https://example.com/browser/browser/base/content/test/social/share_activate.html";
+
+function sendActivationEvent(subframe) {
+  // hack Social.lastEventReceived so we don't hit the "too many events" check.
+  Social.lastEventReceived = 0;
+  let doc = subframe.contentDocument;
+  // if our test has a frame, use it
+  let button = doc.getElementById("activation");
+  ok(!!button, "got the activation button");
+  EventUtils.synthesizeMouseAtCenter(button, {}, doc.defaultView);
+}
+
+function promiseShareFrameEvent(iframe, eventName) {
+  let deferred = Promise.defer();
+  iframe.addEventListener(eventName, function load() {
+    info("page load is " + iframe.contentDocument.location.href);
+    if (iframe.contentDocument.location.href != "data:text/plain;charset=utf8,") {
+      iframe.removeEventListener(eventName, load, true);
+      deferred.resolve();
+    }
+  }, true);
+  return deferred.promise;
+}
 
 function test() {
   waitForExplicitFinish();
-
-  runSocialTests(tests);
+  Services.prefs.setCharPref("social.shareDirectory", activationPage);
+  registerCleanupFunction(function () {
+    Services.prefs.clearUserPref("social.directories");
+    Services.prefs.clearUserPref("social.shareDirectory");
+    Services.prefs.clearUserPref("social.share.activationPanelEnabled");
+  });
+  runSocialTests(tests, undefined, function(next) {
+    let shareButton = SocialShare.shareButton;
+    if (shareButton) {
+      CustomizableUI.removeWidgetFromArea("social-share-button", CustomizableUI.AREA_NAVBAR)
+      shareButton.remove();
+    }
+    ok(CustomizableUI.inDefaultState, "Should start in default state.");
+    next();
+  });
 }
 
 let corpus = [
   {
     url: baseURL+"opengraph/opengraph.html",
     options: {
       // og:title
       title: ">This is my title<",
@@ -67,26 +103,16 @@ let corpus = [
     options: {
       previews: ["http://example.com/1234/56789.jpg"],
       url: "http://www.example.com/photos/56789/",
       shortUrl: "http://imshort/p/abcde"
     }
   }
 ];
 
-function loadURLInTab(url, callback) {
-  info("Loading tab with "+url);
-  let tab = gBrowser.selectedTab = gBrowser.addTab(url);
-  tab.linkedBrowser.addEventListener("load", function listener() {
-    is(tab.linkedBrowser.currentURI.spec, url, "tab loaded")
-    tab.linkedBrowser.removeEventListener("load", listener, true);
-    executeSoon(function() { callback(tab) });
-  }, true);
-}
-
 function hasoptions(testOptions, options) {
   let msg;
   for (let option in testOptions) {
     let data = testOptions[option];
     info("data: "+JSON.stringify(data));
     let message_data = options[option];
     info("message_data: "+JSON.stringify(message_data));
     if (Array.isArray(data)) {
@@ -100,40 +126,49 @@ function hasoptions(testOptions, options
   }
 }
 
 var tests = {
   testShareDisabledOnActivation: function(next) {
     // starting on about:blank page, share should be visible but disabled when
     // adding provider
     is(gBrowser.contentDocument.location.href, "about:blank");
+
+    // initialize the button into the navbar
+    CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_NAVBAR);
+    // ensure correct state
+    SocialUI.onCustomizeEnd(window);
+
     SocialService.addProvider(manifest, function(provider) {
       is(SocialUI.enabled, true, "SocialUI is enabled");
       checkSocialUI();
       // share should not be enabled since we only have about:blank page
       let shareButton = SocialShare.shareButton;
-      is(shareButton.disabled, true, "share button is disabled");
       // verify the attribute for proper css
       is(shareButton.getAttribute("disabled"), "true", "share button attribute is disabled");
       // button should be visible
       is(shareButton.hidden, false, "share button is visible");
       SocialService.disableProvider(manifest.origin, next);
     });
   },
   testShareEnabledOnActivation: function(next) {
     // starting from *some* page, share should be visible and enabled when
     // activating provider
+    // initialize the button into the navbar
+    CustomizableUI.addWidgetToArea("social-share-button", CustomizableUI.AREA_NAVBAR);
+    // ensure correct state
+    SocialUI.onCustomizeEnd(window);
+
     let testData = corpus[0];
-    loadURLInTab(testData.url, function(tab) {
+    addTab(testData.url, function(tab) {
       SocialService.addProvider(manifest, function(provider) {
         is(SocialUI.enabled, true, "SocialUI is enabled");
         checkSocialUI();
         // share should not be enabled since we only have about:blank page
         let shareButton = SocialShare.shareButton;
-        is(shareButton.disabled, false, "share button is enabled");
         // verify the attribute for proper css
         ok(!shareButton.hasAttribute("disabled"), "share button is enabled");
         // button should be visible
         is(shareButton.hidden, false, "share button is visible");
         gBrowser.removeTab(tab);
         next();
       });
     });
@@ -142,19 +177,19 @@ var tests = {
     let provider = Social._getProviderFromOrigin(manifest.origin);
     let port = provider.getWorkerPort();
     ok(port, "provider has a port");
     let testTab;
     let testIndex = 0;
     let testData = corpus[testIndex++];
 
     function runOneTest() {
-      loadURLInTab(testData.url, function(tab) {
+      addTab(testData.url, function(tab) {
         testTab = tab;
-        SocialShare.sharePage();
+        SocialShare.sharePage(manifest.origin);
       });
     }
 
     port.onmessage = function (e) {
       let topic = e.data.topic;
       switch (topic) {
         case "got-share-data-message":
           gBrowser.removeTab(testTab);
@@ -236,10 +271,53 @@ var tests = {
       let url = "https://example.com/browser/browser/base/content/test/social/microdata.html"
       addTab(url, function(tab) {
         testTab = tab;
         let doc = tab.linkedBrowser.contentDocument;
         target = doc.getElementById("simple-hcard");
         SocialShare.sharePage(manifest.origin, null, target);
       });
     });
+  },
+  testSharePanelActivation: function(next) {
+    let testTab;
+    // cleared in the cleanup function
+    Services.prefs.setCharPref("social.directories", "https://example.com");
+    Services.prefs.setBoolPref("social.share.activationPanelEnabled", true);
+    // make the iframe so we can wait on the load
+    SocialShare._createFrame();
+    let iframe = SocialShare.iframe;
+
+    promiseShareFrameEvent(iframe, "load").then(() => {
+      let subframe = iframe.contentDocument.getElementById("activation-frame");
+      waitForCondition(() => {
+          // sometimes the iframe is ready before the panel is open, we need to
+          // wait for both conditions
+          return SocialShare.panel.state == "open"
+                 && subframe.contentDocument
+                 && subframe.contentDocument.readyState == "complete";
+        }, () => {
+        is(subframe.contentDocument.location.href, activationPage, "activation page loaded");
+        promiseObserverNotified("social:provider-enabled").then(() => {
+          let provider = Social._getProviderFromOrigin(manifest.origin);
+          let port = provider.getWorkerPort();
+          ok(!!port, "got port");
+          port.onmessage = function (e) {
+            let topic = e.data.topic;
+            switch (topic) {
+              case "got-share-data-message":
+                ok(true, "share completed");
+                gBrowser.removeTab(testTab);
+                SocialService.uninstallProvider(manifest.origin, next);
+                break;
+            }
+          }
+          port.postMessage({topic: "test-init"});
+        });
+        sendActivationEvent(subframe);
+      }, "share panel did not open and load share page");
+    });
+    addTab(activationPage, function(tab) {
+      testTab = tab;
+      SocialShare.sharePage();
+    });
   }
 }
--- a/browser/base/content/test/social/browser_social_chatwindow.js
+++ b/browser/base/content/test/social/browser_social_chatwindow.js
@@ -23,67 +23,80 @@ let manifests = [
     name: "provider@test2",
     origin: "https://test2.example.com",
     sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar.html?test2",
     workerURL: "https://test2.example.com/browser/browser/base/content/test/social/social_worker.js",
     iconURL: "chrome://branding/content/icon48.png"
   }
 ];
 
+let ports = [];
+function getProviderPort(provider) {
+  let port = provider.getWorkerPort();
+  ok(port, "provider has a port");
+  ports.push(port);
+  return port;
+}
 let chatId = 0;
 function openChat(provider, callback) {
   let chatUrl = provider.origin + "/browser/browser/base/content/test/social/social_chat.html";
-  let port = provider.getWorkerPort();
+  let port = getProviderPort(provider);
   port.onmessage = function(e) {
     if (e.data.topic == "got-chatbox-message") {
-      port.close();
       callback();
     }
   }
   let url = chatUrl + "?" + (chatId++);
   port.postMessage({topic: "test-init"});
   port.postMessage({topic: "test-worker-chat", data: url});
   gURLsNotRemembered.push(url);
+  return port;
 }
 
 function windowHasChats(win) {
   return !!getChatBar().firstElementChild;
 }
 
 function test() {
   requestLongerTimeout(2); // only debug builds seem to need more time...
   waitForExplicitFinish();
 
   let oldwidth = window.outerWidth; // we futz with these, so we restore them
   let oldleft = window.screenX;
   window.moveTo(0, window.screenY)
   let postSubTest = function(cb) {
+    // ensure ports are closed
+    for (let port of ports) {
+      port.close()
+      ok(port._closed, "port closed");
+    }
+    ports = [];
+
     let chats = document.getElementById("pinnedchats");
     ok(chats.children.length == 0, "no chatty children left behind");
     cb();
   };
   runSocialTestWithProvider(manifests, function (finishcb) {
     ok(Social.enabled, "Social is enabled");
-    ok(Social.providers[0].getWorkerPort(), "provider 0 has port");
-    ok(Social.providers[1].getWorkerPort(), "provider 1 has port");
-    ok(Social.providers[2].getWorkerPort(), "provider 2 has port");
+    ok(getProviderPort(Social.providers[0]), "provider 0 has port");
+    ok(getProviderPort(Social.providers[1]), "provider 1 has port");
+    ok(getProviderPort(Social.providers[2]), "provider 2 has port");
     SocialSidebar.show();
     runSocialTests(tests, undefined, postSubTest, function() {
       window.moveTo(oldleft, window.screenY)
       window.resizeTo(oldwidth, window.outerHeight);
       finishcb();
     });
   });
 }
 
 var tests = {
   testOpenCloseChat: function(next) {
     let chats = document.getElementById("pinnedchats");
-    let port = SocialSidebar.provider.getWorkerPort();
-    ok(port, "provider has a port");
+    let port = getProviderPort(SocialSidebar.provider);
     port.onmessage = function (e) {
       let topic = e.data.topic;
       switch (topic) {
         case "got-sidebar-message":
           port.postMessage({topic: "test-chatbox-open"});
           break;
         case "got-chatbox-visibility":
           if (e.data.result == "hidden") {
@@ -91,17 +104,16 @@ var tests = {
             chats.selectedChat.toggle();
           } else if (e.data.result == "shown") {
             ok(true, "chatbox got shown");
             // close it now
             let content = chats.selectedChat.content;
             content.addEventListener("unload", function chatUnload() {
               content.removeEventListener("unload", chatUnload, true);
               ok(true, "got chatbox unload on close");
-              port.close();
               next();
             }, true);
             chats.selectedChat.close();
           }
           break;
         case "got-chatbox-message":
           ok(true, "got chatbox message");
           ok(e.data.result == "ok", "got chatbox windowRef result: "+e.data.result);
@@ -109,42 +121,39 @@ var tests = {
           break;
       }
     }
     port.postMessage({topic: "test-init", data: { id: 1 }});
   },
   testWorkerChatWindow: function(next) {
     const chatUrl = SocialSidebar.provider.origin + "/browser/browser/base/content/test/social/social_chat.html";
     let chats = document.getElementById("pinnedchats");
-    let port = SocialSidebar.provider.getWorkerPort();
-    ok(port, "provider has a port");
+    let port = getProviderPort(SocialSidebar.provider);
     port.postMessage({topic: "test-init"});
     port.onmessage = function (e) {
       let topic = e.data.topic;
       switch (topic) {
         case "got-chatbox-message":
           ok(true, "got a chat window opened");
           ok(chats.selectedChat, "chatbox from worker opened");
           while (chats.selectedChat) {
             chats.selectedChat.close();
           }
           ok(!chats.selectedChat, "chats are all closed");
           gURLsNotRemembered.push(chatUrl);
-          port.close();
           next();
           break;
       }
     }
     ok(!chats.selectedChat, "chats are all closed");
     port.postMessage({topic: "test-worker-chat", data: chatUrl});
   },
   testCloseSelf: function(next) {
     let chats = document.getElementById("pinnedchats");
-    let port = SocialSidebar.provider.getWorkerPort();
-    ok(port, "provider has a port");
+    let port = getProviderPort(SocialSidebar.provider);
     port.onmessage = function (e) {
       let topic = e.data.topic;
       switch (topic) {
         case "test-init-done":
           port.postMessage({topic: "test-chatbox-open"});
           break;
         case "got-chatbox-visibility":
           is(e.data.result, "shown", "chatbox shown");
@@ -152,31 +161,32 @@ var tests = {
           let chat = chats.selectedChat;
           ok(chat.parentNode, "chat has a parent node before it is closed");
           // ask it to close itself.
           let doc = chat.contentDocument;
           let evt = doc.createEvent("CustomEvent");
           evt.initCustomEvent("socialTest-CloseSelf", true, true, {});
           doc.documentElement.dispatchEvent(evt);
           ok(!chat.parentNode, "chat is now closed");
-          port.close();
           next();
           break;
       }
     }
     port.postMessage({topic: "test-init", data: { id: 1 }});
   },
 
   // Check what happens when you close the only visible chat.
   testCloseOnlyVisible: function(next) {
     let chatbar = getChatBar();
     let chatWidth = undefined;
     let num = 0;
     is(chatbar.childNodes.length, 0, "chatbar starting empty");
     is(chatbar.menupopup.childNodes.length, 0, "popup starting empty");
+    let port = getProviderPort(SocialSidebar.provider);
+    port.postMessage({topic: "test-init"});
 
     makeChat("normal", "first chat", function() {
       // got the first one.
       checkPopup();
       ok(chatbar.menupopup.parentNode.collapsed, "menu selection isn't visible");
       // we kinda cheat here and get the width of the first chat, assuming
       // that all future chats will have the same width when open.
       chatWidth = chatbar.calcTotalWidthOf(chatbar.selectedChat);
@@ -197,107 +207,102 @@ var tests = {
           closeAllChats();
           next();
         });
       });
     });
   },
 
   testShowWhenCollapsed: function(next) {
-    let port = SocialSidebar.provider.getWorkerPort();
+    let port = getProviderPort(SocialSidebar.provider);
     port.postMessage({topic: "test-init"});
     get3ChatsForCollapsing("normal", function(first, second, third) {
       let chatbar = getChatBar();
       chatbar.showChat(first);
       ok(!first.collapsed, "first should no longer be collapsed");
-      ok(second.collapsed ||  third.collapsed, false, "one of the others should be collapsed");
+      is(second.collapsed ||  third.collapsed, true, "one of the others should be collapsed");
       closeAllChats();
-      port.close();
       next();
     });
   },
 
   testOnlyOneCallback: function(next) {
     let chats = document.getElementById("pinnedchats");
-    let port = SocialSidebar.provider.getWorkerPort();
+    let port = getProviderPort(SocialSidebar.provider);
     let numOpened = 0;
     port.onmessage = function (e) {
       let topic = e.data.topic;
       switch (topic) {
         case "test-init-done":
           port.postMessage({topic: "test-chatbox-open"});
           break;
         case "chatbox-opened":
           numOpened += 1;
           port.postMessage({topic: "ping"});
           break;
         case "pong":
           executeSoon(function() {
             is(numOpened, 1, "only got one open message");
             chats.selectedChat.close();
-            port.close();
             next();
           });
       }
     }
     port.postMessage({topic: "test-init", data: { id: 1 }});
   },
 
   testMultipleProviderChat: function(next) {
     // test incomming chats from all providers
-    openChat(Social.providers[0], function() {
-      openChat(Social.providers[1], function() {
-        openChat(Social.providers[2], function() {
+    let port0 = openChat(Social.providers[0], function() {
+      let port1 = openChat(Social.providers[1], function() {
+        let port2 = openChat(Social.providers[2], function() {
           let chats = document.getElementById("pinnedchats");
           waitForCondition(function() chats.children.length == Social.providers.length,
             function() {
               ok(true, "one chat window per provider opened");
               // test logout of a single provider
-              let provider = Social.providers[2];
-              let port = provider.getWorkerPort();
-              port.postMessage({topic: "test-logout"});
+              port2.postMessage({topic: "test-logout"});
               waitForCondition(function() chats.children.length == Social.providers.length - 1,
                 function() {
                   closeAllChats();
                   waitForCondition(function() chats.children.length == 0,
                                    function() {
                                     ok(!chats.selectedChat, "multiprovider chats are all closed");
-                                    port.close();
                                     next();
                                    },
                                    "chat windows didn't close");
                 },
                 "chat window didn't close");
             }, "chat windows did not open");
         });
       });
     });
   },
 
   // XXX - note this must be the last test until we restore the login state
   // between tests...
   testCloseOnLogout: function(next) {
     const chatUrl = SocialSidebar.provider.origin + "/browser/browser/base/content/test/social/social_chat.html";
     let port = SocialSidebar.provider.getWorkerPort();
+    ports.push(port);
     ok(port, "provider has a port");
     let opened = false;
     port.onmessage = function (e) {
       let topic = e.data.topic;
       switch (topic) {
         case "test-init-done":
           info("open first chat window");
           port.postMessage({topic: "test-worker-chat", data: chatUrl});
           break;
         case "got-chatbox-message":
           ok(true, "got a chat window opened");
           if (opened) {
             port.postMessage({topic: "test-logout"});
             waitForCondition(function() document.getElementById("pinnedchats").firstChild == null,
                              function() {
-                              port.close();
                               next();
                              },
                              "chat windows didn't close");
           } else {
             // open a second chat window
             opened = true;
             port.postMessage({topic: "test-worker-chat", data: chatUrl+"?id=1"});
           }
--- a/browser/base/content/test/social/browser_social_errorPage.js
+++ b/browser/base/content/test/social/browser_social_errorPage.js
@@ -10,208 +10,217 @@ function gc() {
 }
 
 let openChatWindow = Cu.import("resource://gre/modules/MozSocialAPI.jsm", {}).openChatWindow;
 
 // Support for going on and offline.
 // (via browser/base/content/test/browser_bookmark_titles.js)
 let origProxyType = Services.prefs.getIntPref('network.proxy.type');
 
+function toggleOfflineStatus(goOffline) {
+  // Bug 968887 fix.  when going on/offline, wait for notification before continuing
+  let deferred = Promise.defer();
+  if (!goOffline) {
+    Services.prefs.setIntPref('network.proxy.type', origProxyType);
+  }
+  if (goOffline != Services.io.offline) {
+    info("initial offline state " + Services.io.offline);
+    let expect = !Services.io.offline;
+    Services.obs.addObserver(function offlineChange(subject, topic, data) {
+      Services.obs.removeObserver(offlineChange, "network:offline-status-changed");
+      info("offline state changed to " + Services.io.offline);
+      is(expect, Services.io.offline, "network:offline-status-changed successful toggle");
+      deferred.resolve();
+    }, "network:offline-status-changed", false);
+    BrowserOffline.toggleOfflineStatus();
+  } else {
+    deferred.resolve();
+  }
+  if (goOffline) {
+    Services.prefs.setIntPref('network.proxy.type', 0);
+    // LOAD_FLAGS_BYPASS_CACHE isn't good enough. So clear the cache.
+    Services.cache2.clear();
+  }
+  return deferred.promise;
+}
+
 function goOffline() {
   // Simulate a network outage with offline mode. (Localhost is still
   // accessible in offline mode, so disable the test proxy as well.)
-  if (!Services.io.offline)
-    BrowserOffline.toggleOfflineStatus();
-  Services.prefs.setIntPref('network.proxy.type', 0);
-  // LOAD_FLAGS_BYPASS_CACHE isn't good enough. So clear the cache.
-  Services.cache2.clear();
+  return toggleOfflineStatus(true);
 }
 
 function goOnline(callback) {
-  Services.prefs.setIntPref('network.proxy.type', origProxyType);
-  if (Services.io.offline)
-    BrowserOffline.toggleOfflineStatus();
-  if (callback)
-    callback();
+  return toggleOfflineStatus(false);
 }
 
 function openPanel(url, panelCallback, loadCallback) {
   // open a flyout
   SocialFlyout.open(url, 0, panelCallback);
-  SocialFlyout.panel.firstChild.addEventListener("load", function panelLoad(evt) {
-    if (evt.target != SocialFlyout.panel.firstChild.contentDocument) {
-      return;
-    }
-    SocialFlyout.panel.firstChild.removeEventListener("load", panelLoad, true);
-    loadCallback();
-  }, true);
+  // wait for both open and loaded before callback. Since the test doesn't close
+  // the panel between opens, we cannot rely on events here. We need to ensure
+  // popupshown happens before we finish out the tests.
+  waitForCondition(function() {
+                    return SocialFlyout.panel.state == "open" &&
+                           SocialFlyout.iframe.contentDocument.readyState == "complete";
+                   },
+                   function () { executeSoon(loadCallback) },
+                   "flyout is open and loaded");
 }
 
 function openChat(url, panelCallback, loadCallback) {
   // open a chat window
   let chatbar = getChatBar();
   openChatWindow(null, SocialSidebar.provider, url, panelCallback);
   chatbar.firstChild.addEventListener("DOMContentLoaded", function panelLoad() {
     chatbar.firstChild.removeEventListener("DOMContentLoaded", panelLoad, true);
-    loadCallback();
+    executeSoon(loadCallback);
   }, true);
 }
 
 function onSidebarLoad(callback) {
   let sbrowser = document.getElementById("social-sidebar-browser");
   sbrowser.addEventListener("load", function load() {
     sbrowser.removeEventListener("load", load, true);
-    callback();
+    executeSoon(callback);
   }, true);
 }
 
-function ensureWorkerLoaded(provider, callback) {
-  // once the worker responds to a ping we know it must be up.
-  let port = provider.getWorkerPort();
-  port.onmessage = function(msg) {
-    if (msg.data.topic == "pong") {
-      port.close();
-      callback();
-    }
-  }
-  port.postMessage({topic: "ping"})
-}
-
 let manifest = { // normal provider
   name: "provider 1",
   origin: "https://example.com",
-  sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",
-  workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js",
+  sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar_empty.html",
   iconURL: "https://example.com/browser/browser/base/content/test/general/moz.png"
 };
 
 function test() {
   waitForExplicitFinish();
 
   runSocialTestWithProvider(manifest, function (finishcb) {
-    runSocialTests(tests, undefined, goOnline, finishcb);
+    runSocialTests(tests, undefined, function(next) { goOnline().then(next) }, finishcb);
   });
 }
 
 var tests = {
   testSidebar: function(next) {
     let sbrowser = document.getElementById("social-sidebar-browser");
     onSidebarLoad(function() {
-      ok(sbrowser.contentDocument.location.href.indexOf("about:socialerror?")==0, "is on social error page");
+      ok(sbrowser.contentDocument.location.href.indexOf("about:socialerror?")==0, "sidebar is on social error page");
       gc();
       // Add a new load listener, then find and click the "try again" button.
       onSidebarLoad(function() {
         // should still be on the error page.
-        ok(sbrowser.contentDocument.location.href.indexOf("about:socialerror?")==0, "is still on social error page");
+        ok(sbrowser.contentDocument.location.href.indexOf("about:socialerror?")==0, "sidebar is still on social error page");
         // go online and try again - this should work.
-        goOnline();
-        onSidebarLoad(function() {
-          // should now be on the correct page.
-          is(sbrowser.contentDocument.location.href, manifest.sidebarURL, "is now on social sidebar page");
-          next();
+        goOnline().then(function () {
+          onSidebarLoad(function() {
+            // should now be on the correct page.
+            is(sbrowser.contentDocument.location.href, manifest.sidebarURL, "sidebar is now on social sidebar page");
+            next();
+          });
+          sbrowser.contentDocument.getElementById("btnTryAgain").click();
         });
-        sbrowser.contentDocument.getElementById("btnTryAgain").click();
       });
       sbrowser.contentDocument.getElementById("btnTryAgain").click();
     });
-    // we want the worker to be fully loaded before going offline, otherwise
-    // it might fail due to going offline.
-    ensureWorkerLoaded(SocialSidebar.provider, function() {
-      // go offline then attempt to load the sidebar - it should fail.
-      goOffline();
+    // go offline then attempt to load the sidebar - it should fail.
+    goOffline().then(function() {
       SocialSidebar.show();
-  });
+    });
   },
 
   testFlyout: function(next) {
     let panelCallbackCount = 0;
     let panel = document.getElementById("social-flyout-panel");
-    // go offline and open a flyout.
-    goOffline();
-    openPanel(
-      "https://example.com/browser/browser/base/content/test/social/social_panel.html",
-      function() { // the panel api callback
-        panelCallbackCount++;
-      },
-      function() { // the "load" callback.
-        executeSoon(function() {
+    goOffline().then(function() {
+      openPanel(
+        manifest.sidebarURL, /* empty html page */
+        function() { // the panel api callback
+          panelCallbackCount++;
+        },
+        function() { // the "load" callback.
           todo_is(panelCallbackCount, 0, "Bug 833207 - should be no callback when error page loads.");
-          ok(panel.firstChild.contentDocument.location.href.indexOf("about:socialerror?")==0, "is on social error page");
+          let href = panel.firstChild.contentDocument.location.href;
+          ok(href.indexOf("about:socialerror?")==0, "flyout is on social error page");
           // Bug 832943 - the listeners previously stopped working after a GC, so
           // force a GC now and try again.
           gc();
           openPanel(
-            "https://example.com/browser/browser/base/content/test/social/social_panel.html",
+            manifest.sidebarURL, /* empty html page */
             function() { // the panel api callback
               panelCallbackCount++;
             },
             function() { // the "load" callback.
-              executeSoon(function() {
-                todo_is(panelCallbackCount, 0, "Bug 833207 - should be no callback when error page loads.");
-                ok(panel.firstChild.contentDocument.location.href.indexOf("about:socialerror?")==0, "is on social error page");
-                gc();
-                executeSoon(function() {
-                  SocialFlyout.unload();
-                  next();
-                });
-              });
+              todo_is(panelCallbackCount, 0, "Bug 833207 - should be no callback when error page loads.");
+              let href = panel.firstChild.contentDocument.location.href;
+              ok(href.indexOf("about:socialerror?")==0, "flyout is on social error page");
+              gc();
+              SocialFlyout.unload();
+              next();
             }
           );
-        });
-      }
-    );
+        }
+      );
+    });
   },
 
   testChatWindow: function(next) {
     let panelCallbackCount = 0;
-    // go offline and open a chat.
-    goOffline();
-    openChat(
-      "https://example.com/browser/browser/base/content/test/social/social_chat.html",
-      function() { // the panel api callback
-        panelCallbackCount++;
-      },
-      function() { // the "load" callback.
-        executeSoon(function() {
+    // chatwindow tests throw errors, which muddy test output, if the worker
+    // doesn't get test-init
+    goOffline().then(function() {
+      openChat(
+        manifest.sidebarURL, /* empty html page */
+        function() { // the panel api callback
+          panelCallbackCount++;
+        },
+        function() { // the "load" callback.
           todo_is(panelCallbackCount, 0, "Bug 833207 - should be no callback when error page loads.");
           let chat = getChatBar().selectedChat;
-          waitForCondition(function() chat.contentDocument.location.href.indexOf("about:socialerror?")==0,
+          waitForCondition(function() chat.content != null && chat.contentDocument.location.href.indexOf("about:socialerror?")==0,
                            function() {
                             chat.close();
                             next();
                             },
                            "error page didn't appear");
-        });
-      }
-    );
+        }
+      );
+    });
   },
 
   testChatWindowAfterTearOff: function(next) {
     // Ensure that the error listener survives the chat window being detached.
-    let url = "https://example.com/browser/browser/base/content/test/social/social_chat.html";
+    let url = manifest.sidebarURL; /* empty html page */
     let panelCallbackCount = 0;
+    // chatwindow tests throw errors, which muddy test output, if the worker
+    // doesn't get test-init
     // open a chat while we are still online.
     openChat(
       url,
       null,
       function() { // the "load" callback.
-        executeSoon(function() {
-          let chat = getChatBar().selectedChat;
-          is(chat.contentDocument.location.href, url, "correct url loaded");
-          // toggle to a detached window.
-          chat.swapWindows().then(
-            chat => {
+        let chat = getChatBar().selectedChat;
+        is(chat.contentDocument.location.href, url, "correct url loaded");
+        // toggle to a detached window.
+        chat.swapWindows().then(
+          chat => {
+            ok(!!chat.content, "we have chat content 1");
+            waitForCondition(function() chat.content != null && chat.contentDocument.readyState == "complete",
+                             function() {
               // now go offline and reload the chat - about:socialerror should be loaded.
-              goOffline();
-              chat.contentDocument.location.reload();
-              waitForCondition(function() chat.contentDocument.location.href.indexOf("about:socialerror?")==0,
-                               function() {
-                                chat.close();
-                                next();
-                                },
-                               "error page didn't appear");
-            }
-          );
-        });
+              goOffline().then(function() {
+                ok(!!chat.content, "we have chat content 2");
+                chat.contentDocument.location.reload();
+                info("chat reload called");
+                waitForCondition(function() chat.contentDocument.location.href.indexOf("about:socialerror?")==0,
+                                 function() {
+                                  chat.close();
+                                  next();
+                                  },
+                                 "error page didn't appear");
+              });
+            }, "swapped window loaded");
+          }
+        );
       }
     );
   }
 }
--- a/browser/base/content/test/social/head.js
+++ b/browser/base/content/test/social/head.js
@@ -28,16 +28,26 @@ function waitForCondition(condition, nex
     if (conditionPassed) {
       moveOn();
     }
     tries++;
   }, 100);
   var moveOn = function() { clearInterval(interval); nextTest(); };
 }
 
+
+function promiseObserverNotified(aTopic) {
+  let deferred = Promise.defer();
+  Services.obs.addObserver(function onNotification(aSubject, aTopic, aData) {
+    Services.obs.removeObserver(onNotification, aTopic);
+      deferred.resolve({subject: aSubject, data: aData});
+    }, aTopic, false);
+  return deferred.promise;
+}
+
 // Check that a specified (string) URL hasn't been "remembered" (ie, is not
 // in history, will not appear in about:newtab or auto-complete, etc.)
 function promiseSocialUrlNotRemembered(url) {
   let deferred = Promise.defer();
   let uri = Services.io.newURI(url, null, null);
   PlacesUtils.asyncHistory.isURIVisited(uri, function(aURI, aIsVisited) {
     ok(!aIsVisited, "social URL " + url + " should not be in global history");
     deferred.resolve();
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social/share_activate.html
@@ -0,0 +1,36 @@
+<html>
+<!-- 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/. -->
+<head>
+  <title>Activation test</title>
+</head>
+<script>
+
+var data = {
+  // currently required
+  "name": "Demo Social Service",
+  // browser_share.js serves this page from "https://example.com"
+  "origin": "https://example.com",
+  "iconURL": "chrome://branding/content/icon16.png",
+  "icon32URL": "chrome://branding/content/favicon32.png",
+  "icon64URL": "chrome://branding/content/icon64.png",
+  "workerURL": "/browser/browser/base/content/test/social/social_worker.js",
+  "shareURL": "/browser/browser/base/content/test/social/share.html"
+}
+
+function activate(node) {
+  node.setAttribute("data-service", JSON.stringify(data));
+  var event = new CustomEvent("ActivateSocialFeature");
+  node.dispatchEvent(event);
+}
+
+</script>
+<body>
+
+nothing to see here
+
+<button id="activation" onclick="activate(this, true)">Activate the share provider</button>
+
+</body>
+</html>
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -63,16 +63,17 @@ browser.jar:
         content/browser/aboutaccounts/images/graphic_sync_intro@2x.png        (content/aboutaccounts/images/graphic_sync_intro@2x.png)
 
         content/browser/certerror/aboutCertError.xhtml     (content/aboutcerterror/aboutCertError.xhtml)
         content/browser/certerror/aboutCertError.css       (content/aboutcerterror/aboutCertError.css)
 
         content/browser/aboutRobots-icon.png          (content/aboutRobots-icon.png)
         content/browser/aboutRobots-widget-left.png   (content/aboutRobots-widget-left.png)
         content/browser/aboutSocialError.xhtml        (content/aboutSocialError.xhtml)
+        content/browser/aboutProviderDirectory.xhtml  (content/aboutProviderDirectory.xhtml)
         content/browser/aboutTabCrashed.js            (content/aboutTabCrashed.js)
         content/browser/aboutTabCrashed.xhtml         (content/aboutTabCrashed.xhtml)
 *       content/browser/browser.css                   (content/browser.css)
 *       content/browser/browser.js                    (content/browser.js)
 *       content/browser/browser.xul                   (content/browser.xul)
 *       content/browser/browser-tabPreviews.xml       (content/browser-tabPreviews.xml)
 *       content/browser/chatWindow.xul                (content/chatWindow.xul)
         content/browser/content.js                    (content/content.js)
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -42,16 +42,19 @@ static RedirEntry kRedirMap[] = {
 #endif
   { "certerror", "chrome://browser/content/certerror/aboutCertError.xhtml",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::ALLOW_SCRIPT |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT },
   { "socialerror", "chrome://browser/content/aboutSocialError.xhtml",
     nsIAboutModule::ALLOW_SCRIPT |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT },
+  { "providerdirectory", "chrome://browser/content/aboutProviderDirectory.xhtml",
+    nsIAboutModule::ALLOW_SCRIPT |
+    nsIAboutModule::HIDE_FROM_ABOUTABOUT },
   { "tabcrashed", "chrome://browser/content/aboutTabCrashed.xhtml",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::ALLOW_SCRIPT |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT },
   { "feeds", "chrome://browser/content/feeds/subscribe.xhtml",
     nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
     nsIAboutModule::ALLOW_SCRIPT |
     nsIAboutModule::HIDE_FROM_ABOUTABOUT },
--- a/browser/components/build/nsModule.cpp
+++ b/browser/components/build/nsModule.cpp
@@ -85,16 +85,17 @@ static const mozilla::Module::ContractID
     { NS_SHELLSERVICE_CONTRACTID, &kNS_SHELLSERVICE_CID },
 #endif
     { NS_FEEDSNIFFER_CONTRACTID, &kNS_FEEDSNIFFER_CID },
 #ifdef MOZ_SAFE_BROWSING
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "blocked", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #endif
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "certerror", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "socialerror", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
+    { NS_ABOUT_MODULE_CONTRACTID_PREFIX "providerdirectory", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "tabcrashed", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "feeds", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "privatebrowsing", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "rights", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "robots", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "sessionrestore", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
     { NS_ABOUT_MODULE_CONTRACTID_PREFIX "welcomeback", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
 #ifdef MOZ_SERVICES_SYNC
--- a/browser/components/customizableui/CustomizableUI.jsm
+++ b/browser/components/customizableui/CustomizableUI.jsm
@@ -202,17 +202,16 @@ let CustomizableUIInternal = {
       overflowable: true,
       defaultPlacements: [
         "urlbar-container",
         "search-container",
         "bookmarks-menu-button",
         "downloads-button",
         "home-button",
         "loop-call-button",
-        "social-share-button",
       ],
       defaultCollapsed: false,
     }, true);
 #ifndef XP_MACOSX
     this.registerArea(CustomizableUI.AREA_MENUBAR, {
       legacy: true,
       type: CustomizableUI.TYPE_TOOLBAR,
       defaultPlacements: [
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -389,16 +389,34 @@ const CustomizableWidgets = [
 
       fillSubviewFromMenuItems([...menu.children], doc.getElementById("PanelUI-sidebarItems"));
     },
     onViewHiding: function(aEvent) {
       let doc = aEvent.target.ownerDocument;
       clearSubview(doc.getElementById("PanelUI-sidebarItems"));
     }
   }, {
+    id: "social-share-button",
+    tooltiptext: "social-share-button.label",
+    label: "social-share-button.tooltiptext",
+    // custom build our button so we can attach to the share command
+    type: "custom",
+    onBuild: function(aDocument) {
+      let node = aDocument.createElementNS(kNSXUL, "toolbarbutton");
+      node.setAttribute("id", this.id);
+      node.classList.add("toolbarbutton-1");
+      node.classList.add("chromeclass-toolbar-additional");
+      node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label"));
+      node.setAttribute("tooltiptext", CustomizableUI.getLocalizedProperty(this, "tooltiptext"));
+      node.setAttribute("removable", "true");
+      node.setAttribute("observes", "Social:PageShareOrMark");
+      node.setAttribute("command", "Social:SharePage");
+      return node;
+    }
+  }, {
     id: "add-ons-button",
     shortcutId: "key_openAddons",
     tooltiptext: "add-ons-button.tooltiptext3",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
       let win = aEvent.target &&
                 aEvent.target.ownerDocument &&
                 aEvent.target.ownerDocument.defaultView;
@@ -900,19 +918,18 @@ const CustomizableWidgets = [
     tooltiptext: "email-link-button.tooltiptext3",
     onCommand: function(aEvent) {
       let win = aEvent.view;
       win.MailIntegration.sendLinkForWindow(win.content);
     }
   }, {
     id: "loop-call-button",
     type: "custom",
-    // XXX Bug 1013989 will provide a label for the button
-    label: "loop-call-button.label",
-    tooltiptext: "loop-call-button.tooltiptext",
+    label: "loop-call-button2.label",
+    tooltiptext: "loop-call-button2.tooltiptext",
     defaultArea: CustomizableUI.AREA_NAVBAR,
     introducedInVersion: 1,
     onBuild: function(aDocument) {
       let node = aDocument.createElementNS(kNSXUL, "toolbarbutton");
       node.setAttribute("id", this.id);
       node.classList.add("toolbarbutton-1");
       node.classList.add("chromeclass-toolbar-additional");
       node.classList.add("badged-button");
--- a/browser/components/customizableui/CustomizeMode.jsm
+++ b/browser/components/customizableui/CustomizeMode.jsm
@@ -57,16 +57,20 @@ function CustomizeMode(aWindow) {
   // There are two palettes - there's the palette that can be overlayed with
   // toolbar items in browser.xul. This is invisible, and never seen by the
   // user. Then there's the visible palette, which gets populated and displayed
   // to the user when in customizing mode.
   this.visiblePalette = this.document.getElementById(kPaletteId);
   this.paletteEmptyNotice = this.document.getElementById("customization-empty");
   this.paletteSpacer = this.document.getElementById("customization-spacer");
   this.tipPanel = this.document.getElementById("customization-tipPanel");
+  let lwthemeButton = this.document.getElementById("customization-lwtheme-button");
+  if (Services.prefs.getCharPref("general.skins.selectedSkin") != "classic/1.0") {
+    lwthemeButton.setAttribute("hidden", "true");
+  }
 #ifdef CAN_DRAW_IN_TITLEBAR
   this._updateTitlebarButton();
   Services.prefs.addObserver(kDrawInTitlebarPref, this, false);
   this.window.addEventListener("unload", this);
 #endif
 };
 
 CustomizeMode.prototype = {
@@ -1274,17 +1278,24 @@ CustomizeMode.prototype = {
 
     AddonManager.getAddonByID(DEFAULT_THEME_ID, function(aDefaultTheme) {
       let doc = this.window.document;
 
       function buildToolbarButton(aTheme) {
         let tbb = doc.createElement("toolbarbutton");
         tbb.theme = aTheme;
         tbb.setAttribute("label", aTheme.name);
-        tbb.setAttribute("image", aTheme.iconURL);
+        if (aDefaultTheme == aTheme) {
+          // The actual icon is set up so it looks nice in about:addons, but
+          // we'd like the version that's correct for the OS we're on, so we set
+          // an attribute that our styling will then use to display the icon.
+          tbb.setAttribute("defaulttheme", "true");
+        } else {
+          tbb.setAttribute("image", aTheme.iconURL);
+        }
         if (aTheme.description)
           tbb.setAttribute("tooltiptext", aTheme.description);
         tbb.setAttribute("tabindex", "0");
         tbb.classList.add("customization-lwtheme-menu-theme");
         tbb.setAttribute("aria-checked", aTheme.isActive);
         tbb.setAttribute("role", "menuitemradio");
         if (aTheme.isActive) {
           tbb.setAttribute("active", "true");
@@ -1366,16 +1377,17 @@ CustomizeMode.prototype = {
     let footer = doc.getElementById("customization-lwtheme-menu-footer");
     let recommendedLabel = doc.getElementById("customization-lwtheme-menu-recommended");
     for (let element of [footer, recommendedLabel]) {
       while (element.previousSibling &&
              element.previousSibling.localName == "toolbarbutton") {
         element.previousSibling.remove();
       }
     }
+    aEvent.target.removeAttribute("height");
   },
 
   _onUIChange: function() {
     this._changed = true;
     if (!this.resetting) {
       this._updateResetButton();
       this._updateUndoResetButton();
       this._updateEmptyPaletteNotice();
--- a/browser/components/customizableui/content/customizeMode.inc.xul
+++ b/browser/components/customizableui/content/customizeMode.inc.xul
@@ -28,16 +28,18 @@
 #endif
       <button id="customization-toolbar-visibility-button" label="&customizeMode.toolbars;" class="customizationmode-button" type="menu">
         <menupopup id="customization-toolbar-menu" onpopupshowing="onViewToolbarsPopupShowing(event)"/>
       </button>
       <button id="customization-lwtheme-button" label="&customizeMode.lwthemes;" class="customizationmode-button" type="menu">
         <panel type="arrow" id="customization-lwtheme-menu"
                onpopupshowing="gCustomizeMode.onLWThemesMenuShowing(event);"
                onpopuphidden="gCustomizeMode.onLWThemesMenuHidden(event);"
+               position="topcenter bottomleft"
+               flip="none"
                role="menu">
           <label id="customization-lwtheme-menu-header" value="&customizeMode.lwthemes.myThemes;"/>
           <label id="customization-lwtheme-menu-recommended" value="&customizeMode.lwthemes.recommended;"/>
           <hbox id="customization-lwtheme-menu-footer">
             <toolbarbutton class="customization-lwtheme-menu-footeritem"
                            label="&customizeMode.lwthemes.menuManage;"
                            accesskey="&customizeMode.lwthemes.menuManage.accessKey;"
                            tabindex="0"
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -130,16 +130,17 @@ skip-if = os == "linux"
 
 [browser_985815_propagate_setToolbarVisibility.js]
 [browser_981305_separator_insertion.js]
 [browser_988072_sidebar_events.js]
 [browser_989338_saved_placements_not_resaved.js]
 [browser_989751_subviewbutton_class.js]
 [browser_987177_destroyWidget_xul.js]
 [browser_987177_xul_wrapper_updating.js]
+[browser_987185_syncButton.js]
 [browser_987492_window_api.js]
 [browser_987640_charEncoding.js]
 [browser_992747_toggle_noncustomizable_toolbar.js]
 [browser_993322_widget_notoolbar.js]
 [browser_995164_registerArea_during_customize_mode.js]
 [browser_996364_registerArea_different_properties.js]
 [browser_996635_remove_non_widgets.js]
 [browser_1003588_no_specials_in_panel.js]
new file mode 100755
--- /dev/null
+++ b/browser/components/customizableui/test/browser_987185_syncButton.js
@@ -0,0 +1,69 @@
+/* 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";
+
+let syncService = {};
+Components.utils.import("resource://services-sync/service.js", syncService);
+
+let needsSetup;
+let originalSync;
+let service = syncService.Service;
+let syncWasCalled = false;
+
+add_task(function* testSyncButtonFunctionality() {
+  info("Check Sync button functionality");
+  storeInitialValues();
+  mockFunctions();
+
+  // add the Sync button to the panel
+  CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
+
+  // check the button's functionality
+  yield PanelUI.show();
+  info("The panel menu was opened");
+
+  let syncButton = document.getElementById("sync-button");
+  ok(syncButton, "The Sync button was added to the Panel Menu");
+  syncButton.click();
+  info("The sync button was clicked");
+
+  yield waitForCondition(() => syncWasCalled);
+});
+
+add_task(function* asyncCleanup() {
+  // reset the panel UI to the default state
+  yield resetCustomization();
+  ok(CustomizableUI.inDefaultState, "The panel UI is in default state again.");
+
+  if (isPanelUIOpen()) {
+    let panelHidePromise = promisePanelHidden(window);
+    PanelUI.hide();
+    yield panelHidePromise;
+  }
+
+  restoreValues();
+});
+
+function mockFunctions() {
+  // mock needsSetup
+  gSyncUI._needsSetup = function() false;
+
+  // mock service.errorHandler.syncAndReportErrors()
+  service.errorHandler.syncAndReportErrors = mocked_syncAndReportErrors;
+}
+
+function mocked_syncAndReportErrors() {
+  syncWasCalled = true;
+}
+
+function restoreValues() {
+  gSyncUI._needsSetup = needsSetup;
+  service.sync = originalSync;
+}
+
+function storeInitialValues() {
+  needsSetup = gSyncUI._needsSetup;
+  originalSync = service.sync;
+}
--- a/browser/components/dirprovider/tests/unit/xpcshell.ini
+++ b/browser/components/dirprovider/tests/unit/xpcshell.ini
@@ -1,7 +1,8 @@
 [DEFAULT]
 head = head_dirprovider.js
 tail = 
 firefox-appdir = browser
+skip-if = toolkit == 'android' || toolkit == 'gonk'
 
 [test_bookmark_pref.js]
 [test_keys.js]
--- a/browser/components/downloads/test/unit/xpcshell.ini
+++ b/browser/components/downloads/test/unit/xpcshell.ini
@@ -1,6 +1,7 @@
 [DEFAULT]
 head = head.js
 tail =
 firefox-appdir = browser
+skip-if = toolkit == 'android' || toolkit == 'gonk'
 
 [test_DownloadsCommon.js]
--- a/browser/components/feeds/test/unit/xpcshell.ini
+++ b/browser/components/feeds/test/unit/xpcshell.ini
@@ -1,7 +1,8 @@
 [DEFAULT]
 head = head_feeds.js
 tail = 
 firefox-appdir = browser
+skip-if = toolkit == 'android' || toolkit == 'gonk'
 
 [test_355473.js]
 [test_758990.js]
--- a/browser/components/loop/GoogleImporter.jsm
+++ b/browser/components/loop/GoogleImporter.jsm
@@ -217,17 +217,17 @@ this.GoogleImporter.prototype = {
     // The following loops runs as long as the OAuth windows' titlebar doesn't
     // yield a response from the Google service. If an error occurs, the loop
     // will terminate early.
     while (!code) {
       if (!gAuthWindow || gAuthWindow.closed) {
         throw new Error("Popup window was closed before authentication succeeded");
       }
 
-      let matches = gAuthWindow.document.title.match(/(error|code)=(.*)$/);
+      let matches = gAuthWindow.document.title.match(/(error|code)=([^\s]+)/);
       if (matches && matches.length) {
         let [, type, message] = matches;
         gAuthWindow.close();
         gAuthWindow = null;
         if (type == "error") {
           throw new Error("Google authentication failed with error: " + message.trim());
         } else if (type == "code") {
           code = message.trim();
@@ -420,17 +420,17 @@ this.GoogleImporter.prototype = {
           ["locality", "city"],
           ["postalCode", "postcode"],
           ["region", "region"],
           ["streetAddress", "street"]
         ]), addressNode, kNS_GD);
         if (Object.keys(adr).length) {
           adr.pref = (addressNode.getAttribute("primary") == "true");
           adr.type = [getFieldType(addressNode)];
-          contacts.adr.push(adr);
+          contact.adr.push(adr);
         }
       }
     }
 
     // Process email addresses.
     let emailNodes = entry.getElementsByTagNameNS(kNS_GD, "email");
     if (emailNodes.length) {
       contact.email = [];
@@ -456,18 +456,20 @@ this.GoogleImporter.prototype = {
       }
     }
 
     let orgNodes = entry.getElementsByTagNameNS(kNS_GD, "organization");
     if (orgNodes.length) {
       contact.org = [];
       contact.jobTitle = [];
       for (let [,orgNode] of Iterator(orgNodes)) {
-        contact.org.push(orgNode.getElementsByTagNameNS(kNS_GD, "orgName")[0].firstChild.nodeValue);
-        contact.jobTitle.push(orgNode.getElementsByTagNameNS(kNS_GD, "orgTitle")[0].firstChild.nodeValue);
+        let orgElement = orgNode.getElementsByTagNameNS(kNS_GD, "orgName")[0];
+        let titleElement = orgNode.getElementsByTagNameNS(kNS_GD, "orgTitle")[0];
+        contact.org.push(orgElement ? orgElement.firstChild.nodeValue : "")
+        contact.jobTitle.push(titleElement ? titleElement.firstChild.nodeValue : "");
       }
     }
 
     contact.category = ["google"];
 
     // Basic sanity checking: make sure the name field isn't empty
     if (!("name" in contact) || contact.name[0].length == 0) {
       if (("familyName" in contact) && ("givenName" in contact)) {
--- a/browser/components/loop/LoopStorage.jsm
+++ b/browser/components/loop/LoopStorage.jsm
@@ -20,17 +20,19 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
   const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
   return new EventEmitter();
 });
 
 this.EXPORTED_SYMBOLS = ["LoopStorage"];
 
-const kDatabaseName = "loop";
+const kDatabasePrefix = "loop-";
+const kDefaultDatabaseName = "default";
+let gDatabaseName = kDatabasePrefix + kDefaultDatabaseName;
 const kDatabaseVersion = 1;
 
 let gWaitForOpenCallbacks = new Set();
 let gDatabase = null;
 let gClosed = false;
 
 /**
  * Properly shut the database instance down. This is done on application shutdown.
@@ -78,26 +80,26 @@ const ensureDatabaseOpen = function(onOp
 
   let invokeCallbacks = err => {
     for (let callback of gWaitForOpenCallbacks) {
       callback(err, gDatabase);
     }
     gWaitForOpenCallbacks.clear();
   };
 
-  let openRequest = indexedDB.open(kDatabaseName, kDatabaseVersion);
+  let openRequest = indexedDB.open(gDatabaseName, kDatabaseVersion);
 
   openRequest.onblocked = function(event) {
     invokeCallbacks(new Error("Database cannot be upgraded cause in use: " + event.target.error));
   };
 
   openRequest.onerror = function(event) {
     // Try to delete the old database so that we can start this process over
     // next time.
-    indexedDB.deleteDatabase(kDatabaseName);
+    indexedDB.deleteDatabase(gDatabaseName);
     invokeCallbacks(new Error("Error while opening database: " + event.target.errorCode));
   };
 
   openRequest.onupgradeneeded = function(event) {
     let db = event.target.result;
     eventEmitter.emit("upgrade", db, event.oldVersion, kDatabaseVersion);
   };
 
@@ -105,16 +107,43 @@ const ensureDatabaseOpen = function(onOp
     gDatabase = event.target.result;
     invokeCallbacks();
     // Close the database instance properly on application shutdown.
     Services.obs.addObserver(closeDatabase, "quit-application", false);
   };
 };
 
 /**
+ * Switch to a database with a different name by closing the current connection
+ * and making sure that the next connection attempt will be made using the updated
+ * name.
+ *
+ * @param {String} name New name of the database to switch to.
+ */
+const switchDatabase = function(name) {
+  if (!name) {
+    name = kDefaultDatabaseName;
+  }
+  name = kDatabasePrefix + name;
+  if (name == gDatabaseName) {
+    // This is already the current database, so there's no need to switch.
+    return;
+  }
+
+  gDatabaseName = name;
+  if (gDatabase) {
+    try {
+      gDatabase.close();
+    } finally {
+      gDatabase = null;
+    }
+  }
+};
+
+/**
  * Start a transaction on the loop database and return it.
  *
  * @param {String}   store    Name of the object store to start a transaction on
  * @param {Function} callback Callback to be invoked once a database connection
  *                            is established and a transaction can be started.
  *                            It takes an Error object as first argument and the
  *                            transaction object as second argument.
  * @param {String}   mode     Mode of the transaction. May be 'readonly' or 'readwrite'
@@ -175,28 +204,46 @@ const getStore = function(store, callbac
  *
  * LoopStorage implements the EventEmitter interface by exposing two methods, `on`
  * and `off`, to subscribe to events.
  * At this point only the `upgrade` event will be emitted. This happens when the
  * database is loaded in memory and consumers will be able to change its structure.
  */
 this.LoopStorage = Object.freeze({
   /**
+   * @var {String} databaseName The name of the database that is currently active,
+   *                            WITHOUT the prefix
+   */
+  get databaseName() {
+    return gDatabaseName.substr(kDatabasePrefix.length);
+  },
+
+  /**
    * Open a connection to the IndexedDB database and return the database object.
    *
    * @param {Function} callback Callback to be invoked once a database connection
    *                            is established. It takes an Error object as first
    *                            argument and the database connection object as
    *                            second argument, if successful.
    */
   getSingleton: function(callback) {
     ensureDatabaseOpen(callback);
   },
 
   /**
+   * Switch to a database with a different name.
+   *
+   * @param {String} name New name of the database to switch to. Defaults to
+   *                      `kDefaultDatabaseName`
+   */
+  switchDatabase: function(name = kDefaultDatabaseName) {
+    switchDatabase(name);
+  },
+
+  /**
    * Start a transaction on the loop database and return it.
    * If only two arguments are passed, the default mode will be assumed and the
    * second argument is assumed to be a callback.
    *
    * @param {String}   store    Name of the object store to start a transaction on
    * @param {Function} callback Callback to be invoked once a database connection
    *                            is established and a transaction can be started.
    *                            It takes an Error object as first argument and the
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -273,16 +273,43 @@ function injectLoopAPI(targetWindow) {
       enumerable: true,
       writable: true,
       value: function(num, str) {
         return PluralForm.get(num, str);
       }
     },
 
     /**
+     * Displays a confirmation dialog using the specified strings.
+     *
+     * Callback parameters:
+     * - err null on success, non-null on unexpected failure to show the prompt.
+     * - {Boolean} True if the user chose the OK button.
+     */
+    confirm: {
+      enumerable: true,
+      writable: true,
+      value: function(bodyMessage, okButtonMessage, cancelButtonMessage, callback) {
+        try {
+          let buttonFlags =
+            (Ci.nsIPrompt.BUTTON_POS_0 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING) +
+            (Ci.nsIPrompt.BUTTON_POS_1 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING);
+
+          let chosenButton = Services.prompt.confirmEx(null, "",
+            bodyMessage, buttonFlags, okButtonMessage, cancelButtonMessage,
+            null, null, {});
+
+          callback(null, chosenButton == 0);
+        } catch (ex) {
+          callback(cloneValueInto(ex, targetWindow));
+        }
+      }
+    },
+
+    /**
      * Call to ensure that any necessary registrations for the Loop Service
      * have taken place.
      *
      * Callback parameters:
      * - err null on successful registration, non-null otherwise.
      *
      * @param {Function} callback Will be called once registration is complete,
      *                            or straight away if registration has already
@@ -469,16 +496,23 @@ function injectLoopAPI(targetWindow) {
 
     LOOP_SESSION_TYPE: {
       enumerable: true,
       get: function() {
         return Cu.cloneInto(LOOP_SESSION_TYPE, targetWindow);
       }
     },
 
+    fxAEnabled: {
+      enumerable: true,
+      get: function() {
+        return MozLoopService.fxAEnabled;
+      },
+    },
+
     logInToFxA: {
       enumerable: true,
       writable: true,
       value: function() {
         return MozLoopService.logInToFxA();
       }
     },
 
@@ -580,21 +614,36 @@ function injectLoopAPI(targetWindow) {
      */
     generateUUID: {
       enumerable: true,
       writable: true,
       value: function() {
         return MozLoopService.generateUUID();
       }
     },
+
+    /**
+     * Starts a direct call to the contact addresses.
+     *
+     * @param {Object} contact The contact to call
+     * @param {String} callType The type of call, e.g. "audio-video" or "audio-only"
+     * @return true if the call is opened, false if it is not opened (i.e. busy)
+     */
+    startDirectCall: {
+      enumerable: true,
+      writable: true,
+      value: function(contact, callType) {
+        MozLoopService.startDirectCall(contact, callType);
+      }
+    },
   };
 
   function onStatusChanged(aSubject, aTopic, aData) {
     let event = new targetWindow.CustomEvent("LoopStatusChanged");
-    targetWindow.dispatchEvent(event)
+    targetWindow.dispatchEvent(event);
   };
 
   function onDOMWindowDestroyed(aSubject, aTopic, aData) {
     if (targetWindow && aSubject != targetWindow)
       return;
     Services.obs.removeObserver(onDOMWindowDestroyed, "dom-window-destroyed");
     Services.obs.removeObserver(onStatusChanged, "loop-status-changed");
   };
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -51,16 +51,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/FxAccountsProfileClient.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "HawkClient",
                                   "resource://services-common/hawkclient.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "deriveHawkCredentials",
                                   "resource://services-common/hawkrequest.js");
 
+XPCOMUtils.defineLazyModuleGetter(this, "LoopStorage",
+                                  "resource:///modules/loop/LoopStorage.jsm");
+
 XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
                                   "resource:///modules/loop/MozLoopPushHandler.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
                                    "@mozilla.org/uuid-generator;1",
                                    "nsIUUIDGenerator");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gDNSService",
@@ -72,28 +75,37 @@ XPCOMUtils.defineLazyGetter(this, "log",
   let ConsoleAPI = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).ConsoleAPI;
   let consoleOptions = {
     maxLogLevel: Services.prefs.getCharPref(PREF_LOG_LEVEL).toLowerCase(),
     prefix: "Loop",
   };
   return new ConsoleAPI(consoleOptions);
 });
 
+function setJSONPref(aName, aValue) {
+  let value = !!aValue ? JSON.stringify(aValue) : "";
+  Services.prefs.setCharPref(aName, value);
+}
+
+function getJSONPref(aName) {
+  let value = Services.prefs.getCharPref(aName);
+  return !!value ? JSON.parse(value) : null;
+}
+
 // The current deferred for the registration process. This is set if in progress
 // or the registration was successful. This is null if a registration attempt was
 // unsuccessful.
 let gRegisteredDeferred = null;
 let gPushHandler = null;
 let gHawkClient = null;
-let gLocalizedStrings =  null;
+let gLocalizedStrings = null;
 let gInitializeTimer = null;
+let gFxAEnabled = true;
 let gFxAOAuthClientPromise = null;
 let gFxAOAuthClient = null;
-let gFxAOAuthTokenData = null;
-let gFxAOAuthProfile = null;
 let gErrors = new Map();
 
  /**
  * 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
@@ -301,16 +313,48 @@ let MozLoopServiceInternal = {
   /**
    * Returns true if the expiry time is in the future.
    */
   urlExpiryTimeIsInFuture: function() {
     return this.expiryTimeSeconds * 1000 > Date.now();
   },
 
   /**
+   * Retrieves MozLoopService Firefox Accounts OAuth token.
+   *
+   * @return {Object} OAuth token
+   */
+  get fxAOAuthTokenData() {
+    return getJSONPref("loop.fxa_oauth.tokendata");
+  },
+
+  /**
+   * Sets MozLoopService Firefox Accounts OAuth token.
+   * If the tokenData is being cleared, will also clear the
+   * profile since the profile is dependent on the token data.
+   *
+   * @param {Object} aTokenData OAuth token
+   */
+  set fxAOAuthTokenData(aTokenData) {
+    setJSONPref("loop.fxa_oauth.tokendata", aTokenData);
+    if (!aTokenData) {
+      this.fxAOAuthProfile = null;
+    }
+  },
+
+  /**
+   * Sets MozLoopService Firefox Accounts Profile data.
+   *
+   * @param {Object} aProfileData Profile data
+   */
+  set fxAOAuthProfile(aProfileData) {
+    setJSONPref("loop.fxa_oauth.profile", aProfileData);
+  },
+
+  /**
    * Retrieves MozLoopService "do not disturb" pref value.
    *
    * @return {Boolean} aFlag
    */
   get doNotDisturb() {
     return Services.prefs.getBoolPref("loop.do_not_disturb");
   },
 
@@ -321,16 +365,18 @@ let MozLoopServiceInternal = {
    */
   set doNotDisturb(aFlag) {
     Services.prefs.setBoolPref("loop.do_not_disturb", Boolean(aFlag));
     this.notifyStatusChanged();
   },
 
   notifyStatusChanged: function(aReason = null) {
     log.debug("notifyStatusChanged with reason:", aReason);
+    let profile = MozLoopService.userProfile;
+    LoopStorage.switchDatabase(profile ? profile.uid : null);
     Services.obs.notifyObservers(null, "loop-status-changed", aReason);
   },
 
   /**
    * Record an error and notify interested UI with the relevant user-facing strings attached.
    *
    * @param {String} errorType a key to identify the type of error. Only one
    *                           error of a type will be saved at a time. This value may be used to
@@ -414,19 +460,18 @@ let MozLoopServiceInternal = {
     }
 
     gRegisteredDeferred = Promise.defer();
     // We grab the promise early in case .initialize or its results sets
     // it back to null on error.
     let result = gRegisteredDeferred.promise;
 
     gPushHandler = mockPushHandler || MozLoopPushHandler;
-
     gPushHandler.initialize(this.onPushRegistered.bind(this),
-      this.onHandleNotification.bind(this));
+                            this.onHandleNotification.bind(this));
 
     return result;
   },
 
   /**
    * Performs a hawk based request to the loop server.
    *
    * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request.
@@ -569,22 +614,25 @@ let MozLoopServiceInternal = {
       return;
     }
 
     this.registerWithLoopServer(LOOP_SESSION_TYPE.GUEST, pushUrl).then(() => {
       // storeSessionToken could have rejected and nulled the promise if the token was malformed.
       if (!gRegisteredDeferred) {
         return;
       }
-      gRegisteredDeferred.resolve();
+      gRegisteredDeferred.resolve("registered to guest status");
       // No need to clear the promise here, everything was good, so we don't need
       // to re-register.
-    }, (error) => {
+    }, error => {
       log.error("Failed to register with Loop server: ", error);
-      gRegisteredDeferred.reject(error.errno);
+      // registerWithLoopServer may have already made this null.
+      if (gRegisteredDeferred) {
+        gRegisteredDeferred.reject(error);
+      }
       gRegisteredDeferred = null;
     });
   },
 
   /**
    * Registers with the Loop server either as a guest or a FxA user.
    *
    * @param {LOOP_SESSION_TYPE} sessionType The type of session e.g. guest or FxA
@@ -610,16 +658,18 @@ let MozLoopServiceInternal = {
           // Authorization failed, invalid token, we need to try again with a new token.
           if (retry) {
             return this.registerWithLoopServer(sessionType, pushUrl, false);
           }
         }
 
         log.error("Failed to register with the loop server. Error: ", error);
         this.setError("registration", error);
+        gRegisteredDeferred.reject(error);
+        gRegisteredDeferred = null;
         throw error;
       }
     );
   },
 
   /**
    * Unregisters from the Loop server either as a guest or a FxA user.
    *
@@ -672,17 +722,19 @@ let MozLoopServiceInternal = {
     // bug 1046039 for background.
     Services.prefs.setCharPref("loop.seenToS", "seen");
 
     // Request the information on the new call(s) associated with this version.
     // The registered FxA session is checked first, then the anonymous session.
     // Make the call to get the GUEST session regardless of whether the FXA
     // request fails.
 
-    this._getCalls(LOOP_SESSION_TYPE.FXA, version).catch(() => {});
+    if (MozLoopService.userProfile) {
+      this._getCalls(LOOP_SESSION_TYPE.FXA, version).catch(() => {});
+    }
     this._getCalls(LOOP_SESSION_TYPE.GUEST, version).catch(
       error => {this._hawkRequestError(error);});
   },
 
   /**
    * Make a hawkRequest to GET/calls?=version for this session type.
    *
    * @param {LOOP_SESSION_TYPE} sessionType - type of hawk token used
@@ -711,35 +763,68 @@ let MozLoopServiceInternal = {
    */
 
   _processCalls: function(response, sessionType) {
     try {
       let respData = JSON.parse(response.body);
       if (respData.calls && Array.isArray(respData.calls)) {
         respData.calls.forEach((callData) => {
           if (!this.callsData.inUse) {
-            this.callsData.inUse = true;
             callData.sessionType = sessionType;
-            this.callsData.data = callData;
-            this.openChatWindow(
-              null,
-              this.localizedStrings["incoming_call_title2"].textContent,
-              "about:loopconversation#incoming/" + callData.callId);
+            this._startCall(callData, "incoming");
           } else {
             this._returnBusy(callData);
           }
         });
       } else {
         log.warn("Error: missing calls[] in response");
       }
     } catch (err) {
       log.warn("Error parsing calls info", err);
     }
   },
 
+  /**
+   * Starts a call, saves the call data, and opens a chat window.
+   *
+   * @param {Object} callData The data associated with the call including an id.
+   * @param {Boolean} conversationType Whether or not the call is "incoming"
+   *                                   or "outgoing"
+   */
+  _startCall: function(callData, conversationType) {
+    this.callsData.inUse = true;
+    this.callsData.data = callData;
+    this.openChatWindow(
+      null,
+      // No title, let the page set that, to avoid flickering.
+      "",
+      "about:loopconversation#" + conversationType + "/" + callData.callId);
+  },
+
+  /**
+   * Starts a direct call to the contact addresses.
+   *
+   * @param {Object} contact The contact to call
+   * @param {String} callType The type of call, e.g. "audio-video" or "audio-only"
+   * @return true if the call is opened, false if it is not opened (i.e. busy)
+   */
+  startDirectCall: function(contact, callType) {
+    if (this.callsData.inUse)
+      return false;
+
+    var callData = {
+      contact: contact,
+      callType: callType,
+      callId: Math.floor((Math.random() * 10))
+    };
+
+    this._startCall(callData, "outgoing");
+    return true;
+  },
+
    /**
    * Open call progress websocket and terminate with a reason of busy
    * the server.
    *
    * @param {callData} Must contain the progressURL, callId and websocketToken
    *                   returned by the LoopService.
    */
   _returnBusy: function(callData) {
@@ -1028,59 +1113,101 @@ let MozLoopServiceInternal = {
       deferred.resolve(result);
     } else {
       deferred.reject("Invalid token data");
     }
   },
 };
 Object.freeze(MozLoopServiceInternal);
 
-let gInitializeTimerFunc = () => {
-  // Kick off the push notification service into registering after a timeout
-  // this ensures we're not doing too much straight after the browser's finished
+let gInitializeTimerFunc = (deferredInitialization, mockPushHandler, mockWebSocket) => {
+  // Kick off the push notification service into registering after a timeout.
+  // This ensures we're not doing too much straight after the browser's finished
   // starting up.
   gInitializeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-  gInitializeTimer.initWithCallback(() => {
-    MozLoopService.register();
+  gInitializeTimer.initWithCallback(Task.async(function* initializationCallback() {
+    yield MozLoopService.register(mockPushHandler, mockWebSocket).then(Task.async(function*() {
+      if (!MozLoopServiceInternal.fxAOAuthTokenData) {
+        log.debug("MozLoopService: Initialized without an already logged-in account");
+        deferredInitialization.resolve("initialized to guest status");
+        return;
+      }
+
+      log.debug("MozLoopService: Initializing with already logged-in account");
+      let registeredPromise =
+            MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA,
+                                                          gPushHandler.pushUrl);
+      registeredPromise.then(() => {
+        deferredInitialization.resolve("initialized to logged-in status");
+      }, error => {
+        log.debug("MozLoopService: error logging in using cached auth token");
+        MozLoopServiceInternal.setError("login", error);
+        deferredInitialization.reject("error logging in using cached auth token");
+      });
+    }), error => {
+      log.debug("MozLoopService: Failure of initial registration", error);
+      deferredInitialization.reject(error);
+    });
     gInitializeTimer = null;
-  },
+  }),
   MozLoopServiceInternal.initialRegistrationDelayMilliseconds, Ci.nsITimer.TYPE_ONE_SHOT);
 };
 
 /**
  * Public API
  */
 this.MozLoopService = {
   _DNSService: gDNSService,
 
   set initializeTimerFunc(value) {
     gInitializeTimerFunc = value;
   },
 
   /**
    * Initialized the loop service, and starts registration with the
    * push and loop servers.
+   *
+   * @return {Promise}
    */
-  initialize: function() {
-
+  initialize: Task.async(function*(mockPushHandler, mockWebSocket) {
     // Do this here, rather than immediately after definition, so that we can
     // stub out API functions for unit testing
     Object.freeze(this);
 
     // Don't do anything if loop is not enabled.
     if (!Services.prefs.getBoolPref("loop.enabled") ||
         Services.prefs.getBoolPref("loop.throttled")) {
-      return;
+      return Promise.reject("loop is not enabled");
+    }
+
+    if (Services.prefs.getPrefType("loop.fxa.enabled") == Services.prefs.PREF_BOOL) {
+      gFxAEnabled = Services.prefs.getBoolPref("loop.fxa.enabled");
+      if (!gFxAEnabled) {
+        yield this.logOutFromFxA();
+      }
     }
 
-    // If expiresTime is in the future then kick-off registration.
-    if (MozLoopServiceInternal.urlExpiryTimeIsInFuture()) {
-      gInitializeTimerFunc();
+    // If expiresTime is not in the future and the user hasn't
+    // previously authenticated then skip registration.
+    if (!MozLoopServiceInternal.urlExpiryTimeIsInFuture() &&
+        !MozLoopServiceInternal.fxAOAuthTokenData) {
+      return Promise.resolve("registration not needed");
     }
-  },
+
+    let deferredInitialization = Promise.defer();
+    gInitializeTimerFunc(deferredInitialization, mockPushHandler, mockWebSocket);
+
+    return deferredInitialization.promise.catch(error => {
+      if (typeof(error) == "object") {
+        // This never gets cleared since there is no UI to recover. Only restarting will work.
+        MozLoopServiceInternal.setError("initialization", error);
+      }
+      throw error;
+    });
+  }),
 
   /**
    * If we're operating the service in "soft start" mode, and this browser
    * isn't already activated, check whether it's time for it to become active.
    * If so, activate the loop service.
    *
    * @param {Object} buttonNode DOM node representing the Loop button -- if we
    *                            change from inactive to active, we need this
@@ -1200,17 +1327,17 @@ this.MozLoopService = {
   },
 
   /**
    * Used to note a call url expiry time. If the time is later than the current
    * latest expiry time, then the stored expiry time is increased. For times
    * sooner, this function is a no-op; this ensures we always have the latest
    * expiry time for a url.
    *
-   * This is used to deterimine whether or not we should be registering with the
+   * This is used to determine whether or not we should be registering with the
    * push server on start.
    *
    * @param {Integer} expiryTimeSeconds The seconds since epoch of the expiry time
    *                                    of the url.
    */
   noteCallUrlExpiry: function(expiryTimeSeconds) {
     MozLoopServiceInternal.expiryTimeSeconds = expiryTimeSeconds;
   },
@@ -1221,17 +1348,17 @@ this.MozLoopService = {
    *
    * @param {key} The element id to get strings for.
    * @return {String} A JSON string containing the localized
    *                  attribute/value pairs for the element.
    */
   getStrings: function(key) {
       var stringData = MozLoopServiceInternal.localizedStrings;
       if (!(key in stringData)) {
-        Cu.reportError('No string for key: ' + key + 'found');
+        log.error("No string found for key: ", key);
         return "";
       }
 
       return JSON.stringify(stringData[key]);
   },
 
   /**
    * Returns a new GUID (UUID) in curly braces format.
@@ -1253,18 +1380,30 @@ this.MozLoopService = {
    * Sets MozLoopService "do not disturb" value.
    *
    * @param {Boolean} aFlag
    */
   set doNotDisturb(aFlag) {
     MozLoopServiceInternal.doNotDisturb = aFlag;
   },
 
+  get fxAEnabled() {
+    return gFxAEnabled;
+  },
+
+  /**
+   * Gets the user profile, but only if there is
+   * tokenData present. Without tokenData, the
+   * profile is meaningless.
+   *
+   * @return {Object}
+   */
   get userProfile() {
-    return gFxAOAuthProfile;
+    return getJSONPref("loop.fxa_oauth.tokendata") &&
+           getJSONPref("loop.fxa_oauth.profile");
   },
 
   get errors() {
     return MozLoopServiceInternal.errors;
   },
 
   get log() {
     return log;
@@ -1383,55 +1522,55 @@ this.MozLoopService = {
   /**
    * Start the FxA login flow using the OAuth client and params from the Loop server.
    *
    * The caller should be prepared to handle rejections related to network, server or login errors.
    *
    * @return {Promise} that resolves when the FxA login flow is complete.
    */
   logInToFxA: function() {
-    log.debug("logInToFxA with gFxAOAuthTokenData:", !!gFxAOAuthTokenData);
-    if (gFxAOAuthTokenData) {
-      return Promise.resolve(gFxAOAuthTokenData);
+    log.debug("logInToFxA with fxAOAuthTokenData:", !!MozLoopServiceInternal.fxAOAuthTokenData);
+    if (MozLoopServiceInternal.fxAOAuthTokenData) {
+      return Promise.resolve(MozLoopServiceInternal.fxAOAuthTokenData);
     }
 
     return MozLoopServiceInternal.promiseFxAOAuthAuthorization().then(response => {
       return MozLoopServiceInternal.promiseFxAOAuthToken(response.code, response.state);
     }).then(tokenData => {
-      gFxAOAuthTokenData = tokenData;
+      MozLoopServiceInternal.fxAOAuthTokenData = tokenData;
       return tokenData;
     }).then(tokenData => {
       return gRegisteredDeferred.promise.then(Task.async(function*() {
         if (gPushHandler.pushUrl) {
           yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, gPushHandler.pushUrl);
         } else {
           throw new Error("No pushUrl for FxA registration");
         }
         MozLoopServiceInternal.clearError("login");
         MozLoopServiceInternal.clearError("profile");
-        return gFxAOAuthTokenData;
+        return MozLoopServiceInternal.fxAOAuthTokenData;
       }));
     }).then(tokenData => {
       let client = new FxAccountsProfileClient({
         serverURL: gFxAOAuthClient.parameters.profile_uri,
         token: tokenData.access_token
       });
       client.fetchProfile().then(result => {
-        gFxAOAuthProfile = result;
+        MozLoopServiceInternal.fxAOAuthProfile = result;
         MozLoopServiceInternal.notifyStatusChanged("login");
       }, error => {
         log.error("Failed to retrieve profile", error);
         this.setError("profile", error);
-        gFxAOAuthProfile = null;
+        MozLoopServiceInternal.fxAOAuthProfile = null;
         MozLoopServiceInternal.notifyStatusChanged();
       });
       return tokenData;
     }).catch(error => {
-      gFxAOAuthTokenData = null;
-      gFxAOAuthProfile = null;
+      MozLoopServiceInternal.fxAOAuthTokenData = null;
+      MozLoopServiceInternal.fxAOAuthProfile = null;
       throw error;
     }).catch((error) => {
       MozLoopServiceInternal.setError("login", error);
       // Re-throw for testing
       throw error;
     });
   },
 
@@ -1446,18 +1585,18 @@ this.MozLoopService = {
     log.debug("logOutFromFxA");
     if (gPushHandler && gPushHandler.pushUrl) {
       yield MozLoopServiceInternal.unregisterFromLoopServer(LOOP_SESSION_TYPE.FXA,
                                                             gPushHandler.pushUrl);
     } else {
       MozLoopServiceInternal.clearSessionToken(LOOP_SESSION_TYPE.FXA);
     }
 
-    gFxAOAuthTokenData = null;
-    gFxAOAuthProfile = null;
+    MozLoopServiceInternal.fxAOAuthTokenData = null;
+    MozLoopServiceInternal.fxAOAuthProfile = null;
 
     // 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.
@@ -1486,9 +1625,20 @@ this.MozLoopService = {
    *        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.
    */
   hawkRequest: function(sessionType, path, method, payloadObj) {
     return MozLoopServiceInternal.hawkRequest(sessionType, path, method, payloadObj).catch(
       error => {MozLoopServiceInternal._hawkRequestError(error);});
   },
+
+    /**
+     * Starts a direct call to the contact addresses.
+     *
+     * @param {Object} contact The contact to call
+     * @param {String} callType The type of call, e.g. "audio-video" or "audio-only"
+     * @return true if the call is opened, false if it is not opened (i.e. busy)
+     */
+  startDirectCall: function(contact, callType) {
+    MozLoopServiceInternal.startDirectCall(contact, callType);
+  },
 };
--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -28,16 +28,17 @@
     <script type="text/javascript" src="loop/shared/js/utils.js"></script>
     <script type="text/javascript" src="loop/shared/js/models.js"></script>
     <script type="text/javascript" src="loop/shared/js/mixins.js"></script>
     <script type="text/javascript" src="loop/shared/js/views.js"></script>
     <script type="text/javascript" src="loop/shared/js/feedbackApiClient.js"></script>
     <script type="text/javascript" src="loop/shared/js/actions.js"></script>
     <script type="text/javascript" src="loop/shared/js/validate.js"></script>
     <script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
+    <script type="text/javascript" src="loop/shared/js/otSdkDriver.js"></script>
     <script type="text/javascript" src="loop/shared/js/conversationStore.js"></script>
     <script type="text/javascript" src="loop/js/conversationViews.js"></script>
     <script type="text/javascript" src="loop/shared/js/websocket.js"></script>
     <script type="text/javascript" src="loop/js/client.js"></script>
     <script type="text/javascript" src="loop/js/conversationViews.js"></script>
     <script type="text/javascript" src="loop/js/conversation.js"></script>
   </body>
 </html>
--- a/browser/components/loop/content/js/client.js
+++ b/browser/components/loop/content/js/client.js
@@ -222,16 +222,18 @@ loop.Client = (function($) {
      * - err null on successful registration, non-null otherwise.
      * - result an object of the obtained data for starting the call, if successful
      *
      * @param {Array} calleeIds an array of emails and phone numbers.
      * @param {String} callType the type of call.
      * @param {Function} cb Callback(err, result)
      */
     setupOutgoingCall: function(calleeIds, callType, cb) {
+      // For direct calls, we only ever use the logged-in session. Direct
+      // calls by guests aren't valid.
       this.mozLoop.hawkRequest(this.mozLoop.LOOP_SESSION_TYPE.FXA,
         "/calls", "POST", {
           calleeId: calleeIds,
           callType: callType
         },
         function (err, responseText) {
           if (err) {
             this._failureHandler(cb, err);
--- a/browser/components/loop/content/js/contacts.js
+++ b/browser/components/loop/content/js/contacts.js
@@ -8,20 +8,44 @@
 /*global loop:true, React */
 
 var loop = loop || {};
 loop.contacts = (function(_, mozL10n) {
   "use strict";
 
   const Button = loop.shared.views.Button;
   const ButtonGroup = loop.shared.views.ButtonGroup;
+  const CALL_TYPES = loop.shared.utils.CALL_TYPES;
 
   // Number of contacts to add to the list at the same time.
   const CONTACTS_CHUNK_SIZE = 100;
 
+  // At least this number of contacts should be present for the filter to appear.
+  const MIN_CONTACTS_FOR_FILTERING = 7;
+
+  let getContactNames = function(contact) {
+    // The model currently does not enforce a name to be present, but we're
+    // going to assume it is awaiting more advanced validation of required fields
+    // by the model. (See bug 1069918)
+    // NOTE: this method of finding a firstname and lastname is not i18n-proof.
+    let names = contact.name[0].split(" ");
+    return {
+      firstName: names.shift(),
+      lastName: names.join(" ")
+    };
+  };
+
+  let getPreferredEmail = function(contact) {
+    // A contact may not contain email addresses, but only a phone number.
+    if (!contact.email || contact.email.length == 0) {
+      return { value: "" };
+    }
+    return contact.email.find(e => e.pref) || contact.email[0];
+  };
+
   const ContactDropdown = React.createClass({displayName: 'ContactDropdown',
     propTypes: {
       handleAction: React.PropTypes.func.isRequired,
       canEdit: React.PropTypes.bool
     },
 
     getInitialState: function () {
       return {
@@ -57,24 +81,22 @@ loop.contacts = (function(_, mozL10n) {
 
       let blockAction = this.props.blocked ? "unblock" : "block";
       let blockLabel = this.props.blocked ? "unblock_contact_menu_button"
                                           : "block_contact_menu_button";
 
       return (
         React.DOM.ul({className: cx({ "dropdown-menu": true,
                             "dropdown-menu-up": this.state.openDirUp })}, 
-          React.DOM.li({className: cx({ "dropdown-menu-item": true,
-                              "disabled": true }), 
+          React.DOM.li({className: cx({ "dropdown-menu-item": true }), 
               onClick: this.onItemClick, 'data-action': "video-call"}, 
             React.DOM.i({className: "icon icon-video-call"}), 
             mozL10n.get("video_call_menu_button")
           ), 
-          React.DOM.li({className: cx({ "dropdown-menu-item": true,
-                              "disabled": true }), 
+          React.DOM.li({className: cx({ "dropdown-menu-item": true }), 
               onClick: this.onItemClick, 'data-action': "audio-call"}, 
             React.DOM.i({className: "icon icon-audio-call"}), 
             mozL10n.get("audio_call_menu_button")
           ), 
           React.DOM.li({className: cx({ "dropdown-menu-item": true,
                               "disabled": !this.props.canEdit }), 
               onClick: this.onItemClick, 'data-action': "edit"}, 
             React.DOM.i({className: "icon icon-edit"}), 
@@ -126,69 +148,42 @@ loop.contacts = (function(_, mozL10n) {
         this.setState({showMenu: false});
       }
     },
 
     componentWillUnmount: function() {
       document.body.removeEventListener("click", this._onBodyClick);
     },
 
-    componentShouldUpdate: function(nextProps, nextState) {
+    shouldComponentUpdate: function(nextProps, nextState) {
       let currContact = this.props.contact;
       let nextContact = nextProps.contact;
       return (
         currContact.name[0] !== nextContact.name[0] ||
         currContact.blocked !== nextContact.blocked ||
-        this.getPreferredEmail(currContact).value !== this.getPreferredEmail(nextContact).value
+        getPreferredEmail(currContact).value !== getPreferredEmail(nextContact).value ||
+        nextState.showMenu !== this.state.showMenu
       );
     },
 
     handleAction: function(actionName) {
       if (this.props.handleContactAction) {
         this.props.handleContactAction(this.props.contact, actionName);
       }
     },
 
-    getContactNames: function() {
-      // The model currently does not enforce a name to be present, but we're
-      // going to assume it is awaiting more advanced validation of required fields
-      // by the model. (See bug 1069918)
-      // NOTE: this method of finding a firstname and lastname is not i18n-proof.
-      let names = this.props.contact.name[0].split(" ");
-      return {
-        firstName: names.shift(),
-        lastName: names.join(" ")
-      };
-    },
-
-    getPreferredEmail: function(contact = this.props.contact) {
-      let email;
-      // A contact may not contain email addresses, but only a phone number instead.
-      if (contact.email) {
-        email = contact.email[0];
-        contact.email.some(function(address) {
-          if (address.pref) {
-            email = address;
-            return true;
-          }
-          return false;
-        });
-      }
-      return email || { value: "" };
-    },
-
     canEdit: function() {
       // We cannot modify imported contacts.  For the moment, the check for
       // determining whether the contact is imported is based on its category.
       return this.props.contact.category[0] != "google";
     },
 
     render: function() {
-      let names = this.getContactNames();
-      let email = this.getPreferredEmail();
+      let names = getContactNames(this.props.contact);
+      let email = getPreferredEmail(this.props.contact);
       let cx = React.addons.classSet;
       let contactCSSClass = cx({
         contact: true,
         blocked: this.props.contact.blocked
       });
 
       return (
         React.DOM.li({className: contactCSSClass, onMouseLeave: this.hideDropdownMenu}, 
@@ -213,44 +208,80 @@ loop.contacts = (function(_, mozL10n) {
             : null
           
         )
       );
     }
   });
 
   const ContactsList = React.createClass({displayName: 'ContactsList',
+    mixins: [React.addons.LinkedStateMixin],
+
+    /**
+     * Contacts collection object
+     */
+    contacts: null,
+
+    /**
+     * User profile
+     */
+    _userProfile: null,
+
     getInitialState: function() {
       return {
-        contacts: {},
-        importBusy: false
+        importBusy: false,
+        filter: "",
       };
     },
 
-    componentDidMount: function() {
+    refresh: function(callback = function() {}) {
       let contactsAPI = navigator.mozLoop.contacts;
 
+      this.handleContactRemoveAll();
+
       contactsAPI.getAll((err, contacts) => {
         if (err) {
-          throw err;
+          callback(err);
+          return;
         }
 
         // Add contacts already present in the DB. We do this in timed chunks to
         // circumvent blocking the main event loop.
         let addContactsInChunks = () => {
           contacts.splice(0, CONTACTS_CHUNK_SIZE).forEach(contact => {
             this.handleContactAddOrUpdate(contact, false);
           });
           if (contacts.length) {
             setTimeout(addContactsInChunks, 0);
+          } else {
+            callback();
           }
           this.forceUpdate();
         };
 
         addContactsInChunks(contacts);
+      });
+    },
+
+    componentWillMount: function() {
+      // Take the time to initialize class variables that are used outside
+      // `this.state`.
+      this.contacts = {};
+      this._userProfile = navigator.mozLoop.userProfile;
+    },
+
+    componentDidMount: function() {
+      window.addEventListener("LoopStatusChanged", this._onStatusChanged);
+
+      this.refresh(err => {
+        if (err) {
+          throw err;
+        }
+
+        let contactsAPI = navigator.mozLoop.contacts;
 
         // Listen for contact changes/ updates.
         contactsAPI.on("add", (eventName, contact) => {
           this.handleContactAddOrUpdate(contact);
         });
         contactsAPI.on("remove", (eventName, contact) => {
           this.handleContactRemove(contact);
         });
@@ -258,37 +289,55 @@ loop.contacts = (function(_, mozL10n) {
           this.handleContactRemoveAll();
         });
         contactsAPI.on("update", (eventName, contact) => {
           this.handleContactAddOrUpdate(contact);
         });
       });
     },
 
+    componentWillUnmount: function() {
+      window.removeEventListener("LoopStatusChanged", this._onStatusChanged);
+    },
+
+    _onStatusChanged: function() {
+      let profile = navigator.mozLoop.userProfile;
+      let currUid = this._userProfile ? this._userProfile.uid : null;
+      let newUid = profile ? profile.uid : null;
+      if (currUid != newUid) {
+        // On profile change (login, logout), reload all contacts.
+        this._userProfile = profile;
+        // The following will do a forceUpdate() for us.
+        this.refresh();
+      }
+    },
+
     handleContactAddOrUpdate: function(contact, render = true) {
-      let contacts = this.state.contacts;
+      let contacts = this.contacts;
       let guid = String(contact._guid);
       contacts[guid] = contact;
       if (render) {
         this.forceUpdate();
       }
     },
 
     handleContactRemove: function(contact) {
-      let contacts = this.state.contacts;
+      let contacts = this.contacts;
       let guid = String(contact._guid);
       if (!contacts[guid]) {
         return;
       }
       delete contacts[guid];
       this.forceUpdate();
     },
 
     handleContactRemoveAll: function() {
-      this.setState({contacts: {}});
+      // Do not allow any race conditions when removing all contacts.
+      this.contacts = {};
+      this.forceUpdate();
     },
 
     handleImportButtonClick: function() {
       this.setState({ importBusy: true });
       navigator.mozLoop.startImport({
         service: "google"
       }, (err, stats) => {
         this.setState({ importBusy: false });
@@ -304,25 +353,51 @@ loop.contacts = (function(_, mozL10n) {
     },
 
     handleContactAction: function(contact, actionName) {
       switch (actionName) {
         case "edit":
           this.props.startForm("contacts_edit", contact);
           break;
         case "remove":
+          navigator.mozLoop.confirm(
+            mozL10n.get("confirm_delete_contact_alert"),
+            mozL10n.get("confirm_delete_contact_remove_button"),
+            mozL10n.get("confirm_delete_contact_cancel_button"),
+            (err, result) => {
+              if (err) {
+                throw err;
+              }
+
+              if (!result) {
+                return;
+              }
+
+              navigator.mozLoop.contacts.remove(contact._guid, err => {
+                if (err) {
+                  throw err;
+                }
+              });
+            });
+          break;
         case "block":
         case "unblock":
           // Invoke the API named like the action.
           navigator.mozLoop.contacts[actionName](contact._guid, err => {
             if (err) {
               throw err;
             }
           });
           break;
+        case "video-call":
+          navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
+          break;
+        case "audio-call":
+          navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
+          break;
         default:
           console.error("Unrecognized action: " + actionName);
           break;
       }
     },
 
     sortContacts: function(contact1, contact2) {
       let comp = contact1.name[0].localeCompare(contact2.name[0]);
@@ -335,39 +410,62 @@ loop.contacts = (function(_, mozL10n) {
     },
 
     render: function() {
       let viewForItem = item => {
         return ContactDetail({key: item._guid, contact: item, 
                               handleContactAction: this.handleContactAction})
       };
 
-      let shownContacts = _.groupBy(this.state.contacts, function(contact) {
+      let shownContacts = _.groupBy(this.contacts, function(contact) {
         return contact.blocked ? "blocked" : "available";
       });
 
+      let showFilter = Object.getOwnPropertyNames(this.contacts).length >=
+                       MIN_CONTACTS_FOR_FILTERING;
+      if (showFilter) {
+        let filter = this.state.filter.trim().toLocaleLowerCase();
+        if (filter) {
+          let filterFn = contact => {
+            return contact.name[0].toLocaleLowerCase().contains(filter) ||
+                   getPreferredEmail(contact).value.toLocaleLowerCase().contains(filter);
+          };
+          if (shownContacts.available) {
+            shownContacts.available = shownContacts.available.filter(filterFn);
+          }
+          if (shownContacts.blocked) {
+            shownContacts.blocked = shownContacts.blocked.filter(filterFn);
+          }
+        }
+      }
+
       // TODO: bug 1076767 - add a spinner whilst importing contacts.
       return (
         React.DOM.div(null, 
           React.DOM.div({className: "content-area"}, 
             ButtonGroup(null, 
               Button({caption: this.state.importBusy
                                ? mozL10n.get("importing_contacts_progress_button")
                                : mozL10n.get("import_contacts_button"), 
                       disabled: this.state.importBusy, 
                       onClick: this.handleImportButtonClick}), 
               Button({caption: mozL10n.get("new_contact_button"), 
                       onClick: this.handleAddContactButtonClick})
-            )
+            ), 
+            showFilter ?
+            React.DOM.input({className: "contact-filter", 
+                   placeholder: mozL10n.get("contacts_search_placesholder"), 
+                   valueLink: this.linkState("filter")})
+            : null
           ), 
           React.DOM.ul({className: "contact-list"}, 
             shownContacts.available ?
               shownContacts.available.sort(this.sortContacts).map(viewForItem) :
               null, 
-            shownContacts.blocked ?
+            shownContacts.blocked && shownContacts.blocked.length > 0 ?
               React.DOM.div({className: "contact-separator"}, mozL10n.get("contacts_blocked_contacts")) :
               null, 
             shownContacts.blocked ?
               shownContacts.blocked.sort(this.sortContacts).map(viewForItem) :
               null
           )
         )
       );
--- a/browser/components/loop/content/js/contacts.jsx
+++ b/browser/components/loop/content/js/contacts.jsx
@@ -8,20 +8,44 @@
 /*global loop:true, React */
 
 var loop = loop || {};
 loop.contacts = (function(_, mozL10n) {
   "use strict";
 
   const Button = loop.shared.views.Button;
   const ButtonGroup = loop.shared.views.ButtonGroup;
+  const CALL_TYPES = loop.shared.utils.CALL_TYPES;
 
   // Number of contacts to add to the list at the same time.
   const CONTACTS_CHUNK_SIZE = 100;
 
+  // At least this number of contacts should be present for the filter to appear.
+  const MIN_CONTACTS_FOR_FILTERING = 7;
+
+  let getContactNames = function(contact) {
+    // The model currently does not enforce a name to be present, but we're
+    // going to assume it is awaiting more advanced validation of required fields
+    // by the model. (See bug 1069918)
+    // NOTE: this method of finding a firstname and lastname is not i18n-proof.
+    let names = contact.name[0].split(" ");
+    return {
+      firstName: names.shift(),
+      lastName: names.join(" ")
+    };
+  };
+
+  let getPreferredEmail = function(contact) {
+    // A contact may not contain email addresses, but only a phone number.
+    if (!contact.email || contact.email.length == 0) {
+      return { value: "" };
+    }
+    return contact.email.find(e => e.pref) || contact.email[0];
+  };
+
   const ContactDropdown = React.createClass({
     propTypes: {
       handleAction: React.PropTypes.func.isRequired,
       canEdit: React.PropTypes.bool
     },
 
     getInitialState: function () {
       return {
@@ -57,24 +81,22 @@ loop.contacts = (function(_, mozL10n) {
 
       let blockAction = this.props.blocked ? "unblock" : "block";
       let blockLabel = this.props.blocked ? "unblock_contact_menu_button"
                                           : "block_contact_menu_button";
 
       return (
         <ul className={cx({ "dropdown-menu": true,
                             "dropdown-menu-up": this.state.openDirUp })}>
-          <li className={cx({ "dropdown-menu-item": true,
-                              "disabled": true })}
+          <li className={cx({ "dropdown-menu-item": true })}
               onClick={this.onItemClick} data-action="video-call">
             <i className="icon icon-video-call" />
             {mozL10n.get("video_call_menu_button")}
           </li>
-          <li className={cx({ "dropdown-menu-item": true,
-                              "disabled": true })}
+          <li className={cx({ "dropdown-menu-item": true })}
               onClick={this.onItemClick} data-action="audio-call">
             <i className="icon icon-audio-call" />
             {mozL10n.get("audio_call_menu_button")}
           </li>
           <li className={cx({ "dropdown-menu-item": true,
                               "disabled": !this.props.canEdit })}
               onClick={this.onItemClick} data-action="edit">
             <i className="icon icon-edit" />
@@ -126,69 +148,42 @@ loop.contacts = (function(_, mozL10n) {
         this.setState({showMenu: false});
       }
     },
 
     componentWillUnmount: function() {
       document.body.removeEventListener("click", this._onBodyClick);
     },
 
-    componentShouldUpdate: function(nextProps, nextState) {
+    shouldComponentUpdate: function(nextProps, nextState) {
       let currContact = this.props.contact;
       let nextContact = nextProps.contact;
       return (
         currContact.name[0] !== nextContact.name[0] ||
         currContact.blocked !== nextContact.blocked ||
-        this.getPreferredEmail(currContact).value !== this.getPreferredEmail(nextContact).value
+        getPreferredEmail(currContact).value !== getPreferredEmail(nextContact).value ||
+        nextState.showMenu !== this.state.showMenu
       );
     },
 
     handleAction: function(actionName) {
       if (this.props.handleContactAction) {
         this.props.handleContactAction(this.props.contact, actionName);
       }
     },
 
-    getContactNames: function() {
-      // The model currently does not enforce a name to be present, but we're
-      // going to assume it is awaiting more advanced validation of required fields
-      // by the model. (See bug 1069918)
-      // NOTE: this method of finding a firstname and lastname is not i18n-proof.
-      let names = this.props.contact.name[0].split(" ");
-      return {
-        firstName: names.shift(),
-        lastName: names.join(" ")
-      };
-    },
-
-    getPreferredEmail: function(contact = this.props.contact) {
-      let email;
-      // A contact may not contain email addresses, but only a phone number instead.
-      if (contact.email) {
-        email = contact.email[0];
-        contact.email.some(function(address) {
-          if (address.pref) {
-            email = address;
-            return true;
-          }
-          return false;
-        });
-      }
-      return email || { value: "" };
-    },
-
     canEdit: function() {
       // We cannot modify imported contacts.  For the moment, the check for
       // determining whether the contact is imported is based on its category.
       return this.props.contact.category[0] != "google";
     },
 
     render: function() {
-      let names = this.getContactNames();
-      let email = this.getPreferredEmail();
+      let names = getContactNames(this.props.contact);
+      let email = getPreferredEmail(this.props.contact);
       let cx = React.addons.classSet;
       let contactCSSClass = cx({
         contact: true,
         blocked: this.props.contact.blocked
       });
 
       return (
         <li className={contactCSSClass} onMouseLeave={this.hideDropdownMenu}>
@@ -213,44 +208,80 @@ loop.contacts = (function(_, mozL10n) {
             : null
           }
         </li>
       );
     }
   });
 
   const ContactsList = React.createClass({
+    mixins: [React.addons.LinkedStateMixin],
+
+    /**
+     * Contacts collection object
+     */
+    contacts: null,
+
+    /**
+     * User profile
+     */
+    _userProfile: null,
+
     getInitialState: function() {
       return {
-        contacts: {},
-        importBusy: false
+        importBusy: false,
+        filter: "",
       };
     },
 
-    componentDidMount: function() {
+    refresh: function(callback = function() {}) {
       let contactsAPI = navigator.mozLoop.contacts;
 
+      this.handleContactRemoveAll();
+
       contactsAPI.getAll((err, contacts) => {
         if (err) {
-          throw err;
+          callback(err);
+          return;
         }
 
         // Add contacts already present in the DB. We do this in timed chunks to
         // circumvent blocking the main event loop.
         let addContactsInChunks = () => {
           contacts.splice(0, CONTACTS_CHUNK_SIZE).forEach(contact => {
             this.handleContactAddOrUpdate(contact, false);
           });
           if (contacts.length) {
             setTimeout(addContactsInChunks, 0);
+          } else {
+            callback();
           }
           this.forceUpdate();
         };
 
         addContactsInChunks(contacts);
+      });
+    },
+
+    componentWillMount: function() {
+      // Take the time to initialize class variables that are used outside
+      // `this.state`.
+      this.contacts = {};
+      this._userProfile = navigator.mozLoop.userProfile;
+    },
+
+    componentDidMount: function() {
+      window.addEventListener("LoopStatusChanged", this._onStatusChanged);
+
+      this.refresh(err => {
+        if (err) {
+          throw err;
+        }
+
+        let contactsAPI = navigator.mozLoop.contacts;
 
         // Listen for contact changes/ updates.
         contactsAPI.on("add", (eventName, contact) => {
           this.handleContactAddOrUpdate(contact);
         });
         contactsAPI.on("remove", (eventName, contact) => {
           this.handleContactRemove(contact);
         });
@@ -258,37 +289,55 @@ loop.contacts = (function(_, mozL10n) {
           this.handleContactRemoveAll();
         });
         contactsAPI.on("update", (eventName, contact) => {
           this.handleContactAddOrUpdate(contact);
         });
       });
     },
 
+    componentWillUnmount: function() {
+      window.removeEventListener("LoopStatusChanged", this._onStatusChanged);
+    },
+
+    _onStatusChanged: function() {
+      let profile = navigator.mozLoop.userProfile;
+      let currUid = this._userProfile ? this._userProfile.uid : null;
+      let newUid = profile ? profile.uid : null;
+      if (currUid != newUid) {
+        // On profile change (login, logout), reload all contacts.
+        this._userProfile = profile;
+        // The following will do a forceUpdate() for us.
+        this.refresh();
+      }
+    },
+
     handleContactAddOrUpdate: function(contact, render = true) {
-      let contacts = this.state.contacts;
+      let contacts = this.contacts;
       let guid = String(contact._guid);
       contacts[guid] = contact;
       if (render) {
         this.forceUpdate();
       }
     },
 
     handleContactRemove: function(contact) {
-      let contacts = this.state.contacts;
+      let contacts = this.contacts;
       let guid = String(contact._guid);
       if (!contacts[guid]) {
         return;
       }
       delete contacts[guid];
       this.forceUpdate();
     },
 
     handleContactRemoveAll: function() {
-      this.setState({contacts: {}});
+      // Do not allow any race conditions when removing all contacts.
+      this.contacts = {};
+      this.forceUpdate();
     },
 
     handleImportButtonClick: function() {
       this.setState({ importBusy: true });
       navigator.mozLoop.startImport({
         service: "google"
       }, (err, stats) => {
         this.setState({ importBusy: false });
@@ -304,25 +353,51 @@ loop.contacts = (function(_, mozL10n) {
     },
 
     handleContactAction: function(contact, actionName) {
       switch (actionName) {
         case "edit":
           this.props.startForm("contacts_edit", contact);
           break;
         case "remove":
+          navigator.mozLoop.confirm(
+            mozL10n.get("confirm_delete_contact_alert"),
+            mozL10n.get("confirm_delete_contact_remove_button"),
+            mozL10n.get("confirm_delete_contact_cancel_button"),
+            (err, result) => {
+              if (err) {
+                throw err;
+              }
+
+              if (!result) {
+                return;
+              }
+
+              navigator.mozLoop.contacts.remove(contact._guid, err => {
+                if (err) {
+                  throw err;
+                }
+              });
+            });
+          break;
         case "block":
         case "unblock":
           // Invoke the API named like the action.
           navigator.mozLoop.contacts[actionName](contact._guid, err => {
             if (err) {
               throw err;
             }
           });
           break;
+        case "video-call":
+          navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
+          break;
+        case "audio-call":
+          navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
+          break;
         default:
           console.error("Unrecognized action: " + actionName);
           break;
       }
     },
 
     sortContacts: function(contact1, contact2) {
       let comp = contact1.name[0].localeCompare(contact2.name[0]);
@@ -335,39 +410,62 @@ loop.contacts = (function(_, mozL10n) {
     },
 
     render: function() {
       let viewForItem = item => {
         return <ContactDetail key={item._guid} contact={item}
                               handleContactAction={this.handleContactAction} />
       };
 
-      let shownContacts = _.groupBy(this.state.contacts, function(contact) {
+      let shownContacts = _.groupBy(this.contacts, function(contact) {
         return contact.blocked ? "blocked" : "available";
       });
 
+      let showFilter = Object.getOwnPropertyNames(this.contacts).length >=
+                       MIN_CONTACTS_FOR_FILTERING;
+      if (showFilter) {
+        let filter = this.state.filter.trim().toLocaleLowerCase();
+        if (filter) {
+          let filterFn = contact => {
+            return contact.name[0].toLocaleLowerCase().contains(filter) ||
+                   getPreferredEmail(contact).value.toLocaleLowerCase().contains(filter);
+          };
+          if (shownContacts.available) {
+            shownContacts.available = shownContacts.available.filter(filterFn);
+          }
+          if (shownContacts.blocked) {
+            shownContacts.blocked = shownContacts.blocked.filter(filterFn);
+          }
+        }
+      }
+
       // TODO: bug 1076767 - add a spinner whilst importing contacts.
       return (
         <div>
           <div className="content-area">
             <ButtonGroup>
               <Button caption={this.state.importBusy
                                ? mozL10n.get("importing_contacts_progress_button")
                                : mozL10n.get("import_contacts_button")}
                       disabled={this.state.importBusy}
                       onClick={this.handleImportButtonClick} />
               <Button caption={mozL10n.get("new_contact_button")}
                       onClick={this.handleAddContactButtonClick} />
             </ButtonGroup>
+            {showFilter ?
+            <input className="contact-filter"
+                   placeholder={mozL10n.get("contacts_search_placesholder")}
+                   valueLink={this.linkState("filter")} />
+            : null }
           </div>
           <ul className="contact-list">
             {shownContacts.available ?
               shownContacts.available.sort(this.sortContacts).map(viewForItem) :
               null}
-            {shownContacts.blocked ?
+            {shownContacts.blocked && shownContacts.blocked.length > 0 ?
               <div className="contact-separator">{mozL10n.get("contacts_blocked_contacts")}</div> :
               null}
             {shownContacts.blocked ?
               shownContacts.blocked.sort(this.sortContacts).map(viewForItem) :
               null}
           </ul>
         </div>
       );
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -535,59 +535,72 @@ loop.conversation = (function(mozL10n) {
       set: function(guid, callback) {
         navigator.mozLoop.setLoopCharPref("ot.guid", guid);
         callback(null);
       }
     });
 
     var dispatcher = new loop.Dispatcher();
     var client = new loop.Client();
+    var sdkDriver = new loop.OTSdkDriver({
+      dispatcher: dispatcher,
+      sdk: OT
+    });
+
     var conversationStore = new loop.store.ConversationStore({}, {
       client: client,
-      dispatcher: dispatcher
+      dispatcher: dispatcher,
+      sdkDriver: sdkDriver
     });
 
-    // XXX For now key this on the pref, but this should really be
-    // set by the information from the mozLoop API when we can get it (bug 1072323).
-    var outgoingEmail = navigator.mozLoop.getLoopCharPref("outgoingemail");
-
     // XXX Old class creation for the incoming conversation view, whilst
     // we transition across (bug 1072323).
     var conversation = new sharedModels.ConversationModel(
       {},                // Model attributes
       {sdk: window.OT}   // Model dependencies
     );
 
     // Obtain the callId and pass it through
     var helper = new loop.shared.utils.Helper();
     var locationHash = helper.locationHash();
     var callId;
-    if (locationHash) {
-      callId = locationHash.match(/\#incoming\/(.*)/)[1]
-      conversation.set("callId", callId);
+    var outgoing;
+
+    var hash = locationHash.match(/\#incoming\/(.*)/);
+    if (hash) {
+      callId = hash[1];
+      outgoing = false;
+    } else {
+      hash = locationHash.match(/\#outgoing\/(.*)/);
+      if (hash) {
+        callId = hash[1];
+        outgoing = true;
+      }
     }
 
+    conversation.set({callId: callId});
+
     window.addEventListener("unload", function(event) {
       // Handle direct close of dialog box via [x] control.
-      navigator.mozLoop.releaseCallData(conversation.get("callId"));
+      navigator.mozLoop.releaseCallData(callId);
     });
 
     document.body.classList.add(loop.shared.utils.getTargetPlatform());
 
     React.renderComponent(ConversationControllerView({
       store: conversationStore, 
       client: client, 
       conversation: conversation, 
       dispatcher: dispatcher, 
       sdk: window.OT}
     ), document.querySelector('#main'));
 
     dispatcher.dispatch(new loop.shared.actions.GatherCallData({
       callId: callId,
-      calleeId: outgoingEmail
+      outgoing: outgoing
     }));
   }
 
   return {
     ConversationControllerView: ConversationControllerView,
     IncomingConversationView: IncomingConversationView,
     IncomingCallView: IncomingCallView,
     init: init
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -535,59 +535,72 @@ loop.conversation = (function(mozL10n) {
       set: function(guid, callback) {
         navigator.mozLoop.setLoopCharPref("ot.guid", guid);
         callback(null);
       }
     });
 
     var dispatcher = new loop.Dispatcher();
     var client = new loop.Client();
+    var sdkDriver = new loop.OTSdkDriver({
+      dispatcher: dispatcher,
+      sdk: OT
+    });
+
     var conversationStore = new loop.store.ConversationStore({}, {
       client: client,
-      dispatcher: dispatcher
+      dispatcher: dispatcher,
+      sdkDriver: sdkDriver
     });
 
-    // XXX For now key this on the pref, but this should really be
-    // set by the information from the mozLoop API when we can get it (bug 1072323).
-    var outgoingEmail = navigator.mozLoop.getLoopCharPref("outgoingemail");
-
     // XXX Old class creation for the incoming conversation view, whilst
     // we transition across (bug 1072323).
     var conversation = new sharedModels.ConversationModel(
       {},                // Model attributes
       {sdk: window.OT}   // Model dependencies
     );
 
     // Obtain the callId and pass it through
     var helper = new loop.shared.utils.Helper();
     var locationHash = helper.locationHash();
     var callId;
-    if (locationHash) {
-      callId = locationHash.match(/\#incoming\/(.*)/)[1]
-      conversation.set("callId", callId);
+    var outgoing;
+
+    var hash = locationHash.match(/\#incoming\/(.*)/);
+    if (hash) {
+      callId = hash[1];
+      outgoing = false;
+    } else {
+      hash = locationHash.match(/\#outgoing\/(.*)/);
+      if (hash) {
+        callId = hash[1];
+        outgoing = true;
+      }
     }
 
+    conversation.set({callId: callId});
+
     window.addEventListener("unload", function(event) {
       // Handle direct close of dialog box via [x] control.
-      navigator.mozLoop.releaseCallData(conversation.get("callId"));
+      navigator.mozLoop.releaseCallData(callId);
     });
 
     document.body.classList.add(loop.shared.utils.getTargetPlatform());
 
     React.renderComponent(<ConversationControllerView
       store={conversationStore}
       client={client}
       conversation={conversation}
       dispatcher={dispatcher}
       sdk={window.OT}
     />, document.querySelector('#main'));
 
     dispatcher.dispatch(new loop.shared.actions.GatherCallData({
       callId: callId,
-      calleeId: outgoingEmail
+      outgoing: outgoing
     }));
   }
 
   return {
     ConversationControllerView: ConversationControllerView,
     IncomingConversationView: IncomingConversationView,
     IncomingCallView: IncomingCallView,
     init: init
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -18,40 +18,60 @@ loop.conversationViews = (function(mozL1
    * Displays details of the incoming/outgoing conversation
    * (name, link, audio/video type etc).
    *
    * Allows the view to be extended with different buttons and progress
    * via children properties.
    */
   var ConversationDetailView = React.createClass({displayName: 'ConversationDetailView',
     propTypes: {
-      calleeId: React.PropTypes.string,
+      contact: React.PropTypes.object
+    },
+
+    // This duplicates a similar function in contacts.jsx that isn't used in the
+    // conversation window. If we get too many of these, we might want to consider
+    // finding a logical place for them to be shared.
+    _getPreferredEmail: function(contact) {
+      // A contact may not contain email addresses, but only a phone number.
+      if (!contact.email || contact.email.length == 0) {
+        return { value: "" };
+      }
+      return contact.email.find(e => e.pref) || contact.email[0];
     },
 
     render: function() {
-      document.title = this.props.calleeId;
+      var contactName;
+
+      if (this.props.contact.name &&
+          this.props.contact.name[0]) {
+        contactName = this.props.contact.name[0];
+      } else {
+        contactName = this._getPreferredEmail(this.props.contact).value;
+      }
+
+      document.title = contactName;
 
       return (
         React.DOM.div({className: "call-window"}, 
-          React.DOM.h2(null, this.props.calleeId), 
+          React.DOM.h2(null, contactName), 
           React.DOM.div(null, this.props.children)
         )
       );
     }
   });
 
   /**
    * View for pending conversations. Displays a cancel button and appropriate
    * pending/ringing strings.
    */
   var PendingConversationView = React.createClass({displayName: 'PendingConversationView',
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       callState: React.PropTypes.string,
-      calleeId: React.PropTypes.string,
+      contact: React.PropTypes.object,
       enableCancelButton: React.PropTypes.bool
     },
 
     getDefaultProps: function() {
       return {
         enableCancelButton: false
       };
     },
@@ -71,17 +91,17 @@ loop.conversationViews = (function(mozL1
 
       var btnCancelStyles = cx({
         "btn": true,
         "btn-cancel": true,
         "disabled": !this.props.enableCancelButton
       });
 
       return (
-        ConversationDetailView({calleeId: this.props.calleeId}, 
+        ConversationDetailView({contact: this.props.contact}, 
 
           React.DOM.p({className: "btn-label"}, pendingStateString), 
 
           React.DOM.div({className: "btn-group call-action-group"}, 
             React.DOM.div({className: "fx-embedded-call-button-spacer"}), 
               React.DOM.button({className: btnCancelStyles, 
                       onClick: this.cancelCall}, 
                 mozL10n.get("initiate_call_cancel_button")
@@ -153,41 +173,95 @@ loop.conversationViews = (function(mozL1
       /**
        * OT inserts inline styles into the markup. Using a listener for
        * resize events helps us trigger a full width/height on the element
        * so that they update to the correct dimensions.
        * XXX: this should be factored as a mixin.
        */
       window.addEventListener('orientationchange', this.updateVideoContainer);
       window.addEventListener('resize', this.updateVideoContainer);
+
+      // The SDK needs to know about the configuration and the elements to use
+      // for display. So the best way seems to pass the information here - ideally
+      // the sdk wouldn't need to know this, but we can't change that.
+      this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
+        publisherConfig: this._getPublisherConfig(),
+        getLocalElementFunc: this._getElement.bind(this, ".local"),
+        getRemoteElementFunc: this._getElement.bind(this, ".remote")
+      }));
     },
 
     componentWillUnmount: function() {
       window.removeEventListener('orientationchange', this.updateVideoContainer);
       window.removeEventListener('resize', this.updateVideoContainer);
     },
 
+    /**
+     * Returns either the required DOMNode
+     *
+     * @param {String} className The name of the class to get the element for.
+     */
+    _getElement: function(className) {
+      return this.getDOMNode().querySelector(className);
+    },
+
+    /**
+     * Returns the required configuration for publishing video on the sdk.
+     */
+    _getPublisherConfig: function() {
+      // height set to 100%" to fix video layout on Google Chrome
+      // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
+      return {
+        insertMode: "append",
+        width: "100%",
+        height: "100%",
+        publishVideo: this.props.video.enabled,
+        style: {
+          bugDisplayMode: "off",
+          buttonDisplayMode: "off",
+          nameDisplayMode: "off"
+        }
+      }
+    },
+
+    /**
+     * Used to update the video container whenever the orientation or size of the
+     * display area changes.
+     */
     updateVideoContainer: function() {
-      var localStreamParent = document.querySelector('.local .OT_publisher');
-      var remoteStreamParent = document.querySelector('.remote .OT_subscriber');
+      var localStreamParent = this._getElement('.local .OT_publisher');
+      var remoteStreamParent = this._getElement('.remote .OT_subscriber');
       if (localStreamParent) {
         localStreamParent.style.width = "100%";
       }
       if (remoteStreamParent) {
         remoteStreamParent.style.height = "100%";
       }
     },
 
+    /**
+     * Hangs up the call.
+     */
     hangup: function() {
       this.props.dispatcher.dispatch(
         new sharedActions.HangupCall());
     },
 
+    /**
+     * Used to control publishing a stream - i.e. to mute a stream
+     *
+     * @param {String} type The type of stream, e.g. "audio" or "video".
+     * @param {Boolean} enabled True to enable the stream, false otherwise.
+     */
     publishStream: function(type, enabled) {
-      // XXX Add this as part of bug 972017.
+      this.props.dispatcher.dispatch(
+        new sharedActions.SetMute({
+          type: type,
+          enabled: enabled
+        }));
     },
 
     render: function() {
       var localStreamClasses = React.addons.classSet({
         local: true,
         "local-stream": true,
         "local-stream-audio": !this.props.video.enabled
       });
@@ -281,28 +355,29 @@ loop.conversationViews = (function(mozL1
         case CALL_STATES.TERMINATED: {
           return (CallFailedView({
             dispatcher: this.props.dispatcher}
           ));
         }
         case CALL_STATES.ONGOING: {
           return (OngoingConversationView({
             dispatcher: this.props.dispatcher, 
-            video: {enabled: this.state.callType === CALL_TYPES.AUDIO_VIDEO}}
+            video: {enabled: !this.state.videoMuted}, 
+            audio: {enabled: !this.state.audioMuted}}
             )
           );
         }
         case CALL_STATES.FINISHED: {
           return this._renderFeedbackView();
         }
         default: {
           return (PendingConversationView({
             dispatcher: this.props.dispatcher, 
             callState: this.state.callState, 
-            calleeId: this.state.calleeId, 
+            contact: this.state.contact, 
             enableCancelButton: this._isCancellable()}
           ))
         }
       }
     },
   });
 
   return {
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -18,40 +18,60 @@ loop.conversationViews = (function(mozL1
    * Displays details of the incoming/outgoing conversation
    * (name, link, audio/video type etc).
    *
    * Allows the view to be extended with different buttons and progress
    * via children properties.
    */
   var ConversationDetailView = React.createClass({
     propTypes: {
-      calleeId: React.PropTypes.string,
+      contact: React.PropTypes.object
+    },
+
+    // This duplicates a similar function in contacts.jsx that isn't used in the
+    // conversation window. If we get too many of these, we might want to consider
+    // finding a logical place for them to be shared.
+    _getPreferredEmail: function(contact) {
+      // A contact may not contain email addresses, but only a phone number.
+      if (!contact.email || contact.email.length == 0) {
+        return { value: "" };
+      }
+      return contact.email.find(e => e.pref) || contact.email[0];
     },
 
     render: function() {
-      document.title = this.props.calleeId;
+      var contactName;
+
+      if (this.props.contact.name &&
+          this.props.contact.name[0]) {
+        contactName = this.props.contact.name[0];
+      } else {
+        contactName = this._getPreferredEmail(this.props.contact).value;
+      }
+
+      document.title = contactName;
 
       return (
         <div className="call-window">
-          <h2>{this.props.calleeId}</h2>
+          <h2>{contactName}</h2>
           <div>{this.props.children}</div>
         </div>
       );
     }
   });
 
   /**
    * View for pending conversations. Displays a cancel button and appropriate
    * pending/ringing strings.
    */
   var PendingConversationView = React.createClass({
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       callState: React.PropTypes.string,
-      calleeId: React.PropTypes.string,
+      contact: React.PropTypes.object,
       enableCancelButton: React.PropTypes.bool
     },
 
     getDefaultProps: function() {
       return {
         enableCancelButton: false
       };
     },
@@ -71,17 +91,17 @@ loop.conversationViews = (function(mozL1
 
       var btnCancelStyles = cx({
         "btn": true,
         "btn-cancel": true,
         "disabled": !this.props.enableCancelButton
       });
 
       return (
-        <ConversationDetailView calleeId={this.props.calleeId}>
+        <ConversationDetailView contact={this.props.contact}>
 
           <p className="btn-label">{pendingStateString}</p>
 
           <div className="btn-group call-action-group">
             <div className="fx-embedded-call-button-spacer"></div>
               <button className={btnCancelStyles}
                       onClick={this.cancelCall}>
                 {mozL10n.get("initiate_call_cancel_button")}
@@ -153,41 +173,95 @@ loop.conversationViews = (function(mozL1
       /**
        * OT inserts inline styles into the markup. Using a listener for
        * resize events helps us trigger a full width/height on the element
        * so that they update to the correct dimensions.
        * XXX: this should be factored as a mixin.
        */
       window.addEventListener('orientationchange', this.updateVideoContainer);
       window.addEventListener('resize', this.updateVideoContainer);
+
+      // The SDK needs to know about the configuration and the elements to use
+      // for display. So the best way seems to pass the information here - ideally
+      // the sdk wouldn't need to know this, but we can't change that.
+      this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
+        publisherConfig: this._getPublisherConfig(),
+        getLocalElementFunc: this._getElement.bind(this, ".local"),
+        getRemoteElementFunc: this._getElement.bind(this, ".remote")
+      }));
     },
 
     componentWillUnmount: function() {
       window.removeEventListener('orientationchange', this.updateVideoContainer);
       window.removeEventListener('resize', this.updateVideoContainer);
     },
 
+    /**
+     * Returns either the required DOMNode
+     *
+     * @param {String} className The name of the class to get the element for.
+     */
+    _getElement: function(className) {
+      return this.getDOMNode().querySelector(className);
+    },
+
+    /**
+     * Returns the required configuration for publishing video on the sdk.
+     */
+    _getPublisherConfig: function() {
+      // height set to 100%" to fix video layout on Google Chrome
+      // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445
+      return {
+        insertMode: "append",
+        width: "100%",
+        height: "100%",
+        publishVideo: this.props.video.enabled,
+        style: {
+          bugDisplayMode: "off",
+          buttonDisplayMode: "off",
+          nameDisplayMode: "off"
+        }
+      }
+    },
+
+    /**
+     * Used to update the video container whenever the orientation or size of the
+     * display area changes.
+     */
     updateVideoContainer: function() {
-      var localStreamParent = document.querySelector('.local .OT_publisher');
-      var remoteStreamParent = document.querySelector('.remote .OT_subscriber');
+      var localStreamParent = this._getElement('.local .OT_publisher');
+      var remoteStreamParent = this._getElement('.remote .OT_subscriber');
       if (localStreamParent) {
         localStreamParent.style.width = "100%";
       }
       if (remoteStreamParent) {
         remoteStreamParent.style.height = "100%";
       }
     },
 
+    /**
+     * Hangs up the call.
+     */
     hangup: function() {
       this.props.dispatcher.dispatch(
         new sharedActions.HangupCall());
     },
 
+    /**
+     * Used to control publishing a stream - i.e. to mute a stream
+     *
+     * @param {String} type The type of stream, e.g. "audio" or "video".
+     * @param {Boolean} enabled True to enable the stream, false otherwise.
+     */
     publishStream: function(type, enabled) {
-      // XXX Add this as part of bug 972017.
+      this.props.dispatcher.dispatch(
+        new sharedActions.SetMute({
+          type: type,
+          enabled: enabled
+        }));
     },
 
     render: function() {
       var localStreamClasses = React.addons.classSet({
         local: true,
         "local-stream": true,
         "local-stream-audio": !this.props.video.enabled
       });
@@ -281,28 +355,29 @@ loop.conversationViews = (function(mozL1
         case CALL_STATES.TERMINATED: {
           return (<CallFailedView
             dispatcher={this.props.dispatcher}
           />);
         }
         case CALL_STATES.ONGOING: {
           return (<OngoingConversationView
             dispatcher={this.props.dispatcher}
-            video={{enabled: this.state.callType === CALL_TYPES.AUDIO_VIDEO}}
+            video={{enabled: !this.state.videoMuted}}
+            audio={{enabled: !this.state.audioMuted}}
             />
           );
         }
         case CALL_STATES.FINISHED: {
           return this._renderFeedbackView();
         }
         default: {
           return (<PendingConversationView
             dispatcher={this.props.dispatcher}
             callState={this.state.callState}
-            calleeId={this.state.calleeId}
+            contact={this.state.contact}
             enableCancelButton={this._isCancellable()}
           />)
         }
       }
     },
   });
 
   return {
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -9,39 +9,55 @@
 
 var loop = loop || {};
 loop.panel = (function(_, mozL10n) {
   "use strict";
 
   var sharedViews = loop.shared.views;
   var sharedModels = loop.shared.models;
   var sharedMixins = loop.shared.mixins;
+  var sharedActions = loop.shared.actions;
   var Button = sharedViews.Button;
   var ButtonGroup = sharedViews.ButtonGroup;
   var ContactsList = loop.contacts.ContactsList;
   var ContactDetailsForm = loop.contacts.ContactDetailsForm;
   var __ = mozL10n.get; // aliasing translation function as __ for concision
 
   var TabView = React.createClass({displayName: 'TabView',
-    getInitialState: function() {
+    propTypes: {
+      buttonsHidden: React.PropTypes.bool,
+      // The selectedTab prop is used by the UI showcase.
+      selectedTab: React.PropTypes.string
+    },
+
+    getDefaultProps: function() {
       return {
+        buttonsHidden: false,
         selectedTab: "call"
       };
     },
 
+    getInitialState: function() {
+      return {selectedTab: this.props.selectedTab};
+    },
+
     handleSelectTab: function(event) {
       var tabName = event.target.dataset.tabName;
       this.setState({selectedTab: tabName});
     },
 
     render: function() {
       var cx = React.addons.classSet;
       var tabButtons = [];
       var tabs = [];
       React.Children.forEach(this.props.children, function(tab, i) {
+        // Filter out null tabs (eg. rooms when the feature is disabled)
+        if (!tab) {
+          return;
+        }
         var tabName = tab.props.name;
         var isSelected = (this.state.selectedTab == tabName);
         if (!tab.props.hidden) {
           tabButtons.push(
             React.DOM.li({className: cx({selected: isSelected}), 
                 key: i, 
                 'data-tab-name': tabName, 
                 onClick: this.handleSelectTab})
@@ -50,17 +66,19 @@ loop.panel = (function(_, mozL10n) {
         tabs.push(
           React.DOM.div({key: i, className: cx({tab: true, selected: isSelected})}, 
             tab.props.children
           )
         );
       }, this);
       return (
         React.DOM.div({className: "tab-view-container"}, 
-          React.DOM.ul({className: "tab-view"}, tabButtons), 
+          !this.props.buttonsHidden
+            ? React.DOM.ul({className: "tab-view"}, tabButtons)
+            : null, 
           tabs
         )
       );
     }
   });
 
   var Tab = React.createClass({displayName: 'Tab',
     render: function() {
@@ -145,17 +163,17 @@ loop.panel = (function(_, mozL10n) {
       return {seenToS: navigator.mozLoop.getLoopCharPref('seenToS')};
     },
 
     render: function() {
       if (this.state.seenToS == "unseen") {
         var terms_of_use_url = navigator.mozLoop.getLoopCharPref('legal.ToS_url');
         var privacy_notice_url = navigator.mozLoop.getLoopCharPref('legal.privacy_url');
         var tosHTML = __("legal_text_and_links3", {
-          "clientShortname": __("client_shortname_fallback"),
+          "clientShortname": __("clientShortname2"),
           "terms_of_use": React.renderComponentToStaticMarkup(
             React.DOM.a({href: terms_of_use_url, target: "_blank"}, 
               __("legal_text_tos")
             )
           ),
           "privacy_notice": React.renderComponentToStaticMarkup(
             React.DOM.a({href: privacy_notice_url, target: "_blank"}, 
               __("legal_text_privacy")
@@ -223,16 +241,22 @@ loop.panel = (function(_, mozL10n) {
     },
 
     _isSignedIn: function() {
       return !!navigator.mozLoop.userProfile;
     },
 
     render: function() {
       var cx = React.addons.classSet;
+
+      // For now all of the menu entries require FxA so hide the whole gear if FxA is disabled.
+      if (!navigator.mozLoop.fxAEnabled) {
+        return null;
+      }
+
       return (
         React.DOM.div({className: "settings-menu dropdown"}, 
           React.DOM.a({className: "button-settings", onClick: this.showDropdownMenu, 
              title: __("settings_menu_button_tooltip")}), 
           React.DOM.ul({className: cx({"dropdown-menu": true, hide: !this.state.showMenu}), 
               onMouseLeave: this.hideDropdownMenu}, 
             SettingsDropdownEntry({label: __("settings_menu_item_settings"), 
                                    onClick: this.handleClickSettingsEntry, 
@@ -241,16 +265,17 @@ loop.panel = (function(_, mozL10n) {
             SettingsDropdownEntry({label: __("settings_menu_item_account"), 
                                    onClick: this.handleClickAccountEntry, 
                                    icon: "account", 
                                    displayed: this._isSignedIn()}), 
             SettingsDropdownEntry({label: this._isSignedIn() ?
                                           __("settings_menu_item_signout") :
                                           __("settings_menu_item_signin"), 
                                    onClick: this.handleClickAuthEntry, 
+                                   displayed: navigator.mozLoop.fxAEnabled, 
                                    icon: this._isSignedIn() ? "signout" : "signin"})
           )
         )
       );
     }
   });
 
   /**
@@ -322,44 +347,62 @@ loop.panel = (function(_, mozL10n) {
       } else {
         try {
           var callUrl = new window.URL(callUrlData.callUrl);
           // XXX the current server vers does not implement the callToken field
           // but it exists in the API. This workaround should be removed in the future
           var token = callUrlData.callToken ||
                       callUrl.pathname.split('/').pop();
 
+          // Now that a new URL is available, indicate it has not been shared.
+          this.linkExfiltrated = false;
+
           this.setState({pending: false, copied: false,
                          callUrl: callUrl.href,
                          callUrlExpiry: callUrlData.expiresAt});
         } catch(e) {
           console.log(e);
           this.props.notifications.errorL10n("unable_retrieve_url");
           this.setState(this.getInitialState());
         }
       }
     },
 
     handleEmailButtonClick: function(event) {
       this.handleLinkExfiltration(event);
 
-      navigator.mozLoop.composeEmail(__("share_email_subject3"),
-        __("share_email_body3", { callUrl: this.state.callUrl }));
+      navigator.mozLoop.composeEmail(
+        __("share_email_subject4", { clientShortname: __("clientShortname2")}),
+        __("share_email_body4", { callUrl: this.state.callUrl,
+                                  clientShortname: __("clientShortname2"),
+                                  learnMoreUrl: navigator.mozLoop.getLoopCharPref("learnMoreUrl") }));
     },
 
     handleCopyButtonClick: function(event) {
       this.handleLinkExfiltration(event);
       // XXX the mozLoop object should be passed as a prop, to ease testing and
       //     using a fake implementation in UI components showcase.
       navigator.mozLoop.copyString(this.state.callUrl);
       this.setState({copied: true});
     },
 
+    linkExfiltrated: false,
+
     handleLinkExfiltration: function(event) {
-      // TODO Bug 1015988 -- Increase link exfiltration telemetry count
+      // Update the count of shared URLs only once per generated URL.
+      if (!this.linkExfiltrated) {
+        this.linkExfiltrated = true;
+        try {
+          navigator.mozLoop.telemetryAdd("LOOP_CLIENT_CALL_URL_SHARED", true);
+        } catch (err) {
+          console.error("Error recording telemetry", err);
+        }
+      }
+
+      // Note URL expiration every time it is shared.
       if (this.state.callUrlExpiry) {
         navigator.mozLoop.noteCallUrlExpiry(this.state.callUrlExpiry);
       }
     },
 
     render: function() {
       // XXX setting elem value from a state (in the callUrl input)
       // makes it immutable ie read only but that is fine in our case.
@@ -398,17 +441,17 @@ loop.panel = (function(_, mozL10n) {
    * FxA sign in/up link component.
    */
   var AuthLink = React.createClass({displayName: 'AuthLink',
     handleSignUpLinkClick: function() {
       navigator.mozLoop.logInToFxA();
     },
 
     render: function() {
-      if (navigator.mozLoop.userProfile) {
+      if (!navigator.mozLoop.fxAEnabled || navigator.mozLoop.userProfile) {
         return null;
       }
       return (
         React.DOM.p({className: "signin-link"}, 
           React.DOM.a({href: "#", onClick: this.handleSignUpLinkClick}, 
             __("panel_footer_signin_or_signup_link")
           )
         )
@@ -425,25 +468,145 @@ loop.panel = (function(_, mozL10n) {
         React.DOM.p({className: "user-identity"}, 
           this.props.displayName
         )
       );
     }
   });
 
   /**
+   * Room list entry.
+   */
+  var RoomEntry = React.createClass({displayName: 'RoomEntry',
+    propTypes: {
+      openRoom: React.PropTypes.func.isRequired,
+      room:     React.PropTypes.instanceOf(loop.store.Room).isRequired
+    },
+
+    shouldComponentUpdate: function(nextProps, nextState) {
+      return nextProps.room.ctime > this.props.room.ctime;
+    },
+
+    handleClickRoom: function(event) {
+      event.preventDefault();
+      this.props.openRoom(this.props.room);
+    },
+
+    _isActive: function() {
+      // XXX bug 1074679 will implement this properly
+      return this.props.room.currSize > 0;
+    },
+
+    render: function() {
+      var room = this.props.room;
+      var roomClasses = React.addons.classSet({
+        "room-entry": true,
+        "room-active": this._isActive()
+      });
+
+      return (
+        React.DOM.div({className: roomClasses}, 
+          React.DOM.h2(null, 
+            React.DOM.span({className: "room-notification"}), 
+            room.roomName
+          ), 
+          React.DOM.p(null, 
+            React.DOM.a({ref: "room", href: "#", onClick: this.handleClickRoom}, 
+              room.roomUrl
+            )
+          )
+        )
+      );
+    }
+  });
+
+  /**
+   * Room list.
+   */
+  var RoomList = React.createClass({displayName: 'RoomList',
+    mixins: [Backbone.Events],
+
+    propTypes: {
+      store: React.PropTypes.instanceOf(loop.store.RoomListStore).isRequired,
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      rooms: React.PropTypes.array
+    },
+
+    getInitialState: function() {
+      var storeState = this.props.store.getStoreState();
+      return {
+        error: this.props.error || storeState.error,
+        rooms: this.props.rooms || storeState.rooms,
+      };
+    },
+
+    componentWillMount: function() {
+      this.listenTo(this.props.store, "change", this._onRoomListChanged);
+
+      this.props.dispatcher.dispatch(new sharedActions.GetAllRooms());
+    },
+
+    componentWillUnmount: function() {
+      this.stopListening(this.props.store);
+    },
+
+    _onRoomListChanged: function() {
+      var storeState = this.props.store.getStoreState();
+      this.setState({
+        error: storeState.error,
+        rooms: storeState.rooms
+      });
+    },
+
+    _getListHeading: function() {
+      var numRooms = this.state.rooms.length;
+      if (numRooms === 0) {
+        return mozL10n.get("rooms_list_no_current_conversations");
+      }
+      return mozL10n.get("rooms_list_current_conversations", {num: numRooms});
+    },
+
+    openRoom: function(room) {
+      // XXX implement me; see bug 1074678
+    },
+
+    render: function() {
+      if (this.state.error) {
+        // XXX Better end user reporting of errors.
+        console.error(this.state.error);
+      }
+
+      return (
+        React.DOM.div({className: "room-list"}, 
+          React.DOM.h1(null, this._getListHeading()), 
+          
+            this.state.rooms.map(function(room, i) {
+              return RoomEntry({key: i, room: room, openRoom: this.openRoom});
+            }, this)
+          
+        )
+      );
+    }
+  });
+
+  /**
    * Panel view.
    */
   var PanelView = React.createClass({displayName: 'PanelView',
     propTypes: {
       notifications: React.PropTypes.object.isRequired,
       client: React.PropTypes.object.isRequired,
       // Mostly used for UI components showcase and unit tests
       callUrl: React.PropTypes.string,
       userProfile: React.PropTypes.object,
+      showTabButtons: React.PropTypes.bool,
+      selectedTab: React.PropTypes.string,
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      roomListStore:
+        React.PropTypes.instanceOf(loop.store.RoomListStore).isRequired
     },
 
     getInitialState: function() {
       return {
         userProfile: this.props.userProfile || navigator.mozLoop.userProfile,
       };
     },
 
@@ -470,20 +633,43 @@ loop.panel = (function(_, mozL10n) {
           detailsButtonLabel: serviceError.error.friendlyDetailsButtonLabel,
         });
       } else {
         this.props.notifications.remove(this.props.notifications.get("service-error"));
       }
     },
 
     _onStatusChanged: function() {
-      this.setState({userProfile: navigator.mozLoop.userProfile});
+      var profile = navigator.mozLoop.userProfile;
+      var currUid = this.state.userProfile ? this.state.userProfile.uid : null;
+      var newUid = profile ? profile.uid : null;
+      if (currUid != newUid) {
+        // On profile change (login, logout), switch back to the default tab.
+        this.selectTab("call");
+      }
+      this.setState({userProfile: profile});
       this.updateServiceErrors();
     },
 
+    /**
+     * The rooms feature is hidden by default for now. Once it gets mainstream,
+     * this method can be safely removed.
+     */
+    _renderRoomsTab: function() {
+      if (!navigator.mozLoop.getLoopBoolPref("rooms.enabled")) {
+        return null;
+      }
+      return (
+        Tab({name: "rooms"}, 
+          RoomList({dispatcher: this.props.dispatcher, 
+                    store: this.props.roomListStore})
+        )
+      );
+    },
+
     startForm: function(name, contact) {
       this.refs[name].initForm(contact);
       this.selectTab(name);
     },
 
     selectTab: function(name) {
       this.refs.tabView.setState({ selectedTab: name });
     },
@@ -503,25 +689,27 @@ loop.panel = (function(_, mozL10n) {
     render: function() {
       var NotificationListView = sharedViews.NotificationListView;
       var displayName = this.state.userProfile && this.state.userProfile.email ||
                         __("display_name_guest");
       return (
         React.DOM.div(null, 
           NotificationListView({notifications: this.props.notifications, 
                                 clearOnDocumentHidden: true}), 
-          TabView({ref: "tabView"}, 
+          TabView({ref: "tabView", selectedTab: this.props.selectedTab, 
+            buttonsHidden: !this.state.userProfile && !this.props.showTabButtons}, 
             Tab({name: "call"}, 
               React.DOM.div({className: "content-area"}, 
                 CallUrlResult({client: this.props.client, 
                                notifications: this.props.notifications, 
                                callUrl: this.props.callUrl}), 
                 ToSView(null)
               )
             ), 
+            this._renderRoomsTab(), 
             Tab({name: "contacts"}, 
               ContactsList({selectTab: this.selectTab, 
                             startForm: this.startForm})
             ), 
             Tab({name: "contacts_add", hidden: true}, 
               ContactDetailsForm({ref: "contacts_add", mode: "add", 
                                   selectTab: this.selectTab})
             ), 
@@ -551,35 +739,45 @@ loop.panel = (function(_, mozL10n) {
    * Panel initialisation.
    */
   function init() {
     // Do the initial L10n setup, we do this before anything
     // else to ensure the L10n environment is setup correctly.
     mozL10n.initialize(navigator.mozLoop);
 
     var client = new loop.Client();
-    var notifications = new sharedModels.NotificationCollection()
+    var notifications = new sharedModels.NotificationCollection();
+    var dispatcher = new loop.Dispatcher();
+    var roomListStore = new loop.store.RoomListStore({
+      mozLoop: navigator.mozLoop,
+      dispatcher: dispatcher
+    });
 
     React.renderComponent(PanelView({
       client: client, 
-      notifications: notifications}), document.querySelector("#main"));
+      notifications: notifications, 
+      roomListStore: roomListStore, 
+      dispatcher: dispatcher}
+    ), document.querySelector("#main"));
 
     document.body.classList.add(loop.shared.utils.getTargetPlatform());
     document.body.setAttribute("dir", mozL10n.getDirection());
 
     // Notify the window that we've finished initalization and initial layout
     var evtObject = document.createEvent('Event');
     evtObject.initEvent('loopPanelInitialized', true, false);
     window.dispatchEvent(evtObject);
   }
 
   return {
     init: init,
     UserIdentity: UserIdentity,
+    AuthLink: AuthLink,
     AvailabilityDropdown: AvailabilityDropdown,
     CallUrlResult: CallUrlResult,
     PanelView: PanelView,
+    RoomList: RoomList,
     SettingsDropdown: SettingsDropdown,
     ToSView: ToSView
   };
 })(_, document.mozL10n);
 
 document.addEventListener('DOMContentLoaded', loop.panel.init);
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -9,39 +9,55 @@
 
 var loop = loop || {};
 loop.panel = (function(_, mozL10n) {
   "use strict";
 
   var sharedViews = loop.shared.views;
   var sharedModels = loop.shared.models;
   var sharedMixins = loop.shared.mixins;
+  var sharedActions = loop.shared.actions;
   var Button = sharedViews.Button;
   var ButtonGroup = sharedViews.ButtonGroup;
   var ContactsList = loop.contacts.ContactsList;
   var ContactDetailsForm = loop.contacts.ContactDetailsForm;
   var __ = mozL10n.get; // aliasing translation function as __ for concision
 
   var TabView = React.createClass({
-    getInitialState: function() {
+    propTypes: {
+      buttonsHidden: React.PropTypes.bool,
+      // The selectedTab prop is used by the UI showcase.
+      selectedTab: React.PropTypes.string
+    },
+
+    getDefaultProps: function() {
       return {
+        buttonsHidden: false,
         selectedTab: "call"
       };
     },
 
+    getInitialState: function() {
+      return {selectedTab: this.props.selectedTab};
+    },
+
     handleSelectTab: function(event) {
       var tabName = event.target.dataset.tabName;
       this.setState({selectedTab: tabName});
     },
 
     render: function() {
       var cx = React.addons.classSet;
       var tabButtons = [];
       var tabs = [];
       React.Children.forEach(this.props.children, function(tab, i) {
+        // Filter out null tabs (eg. rooms when the feature is disabled)
+        if (!tab) {
+          return;
+        }
         var tabName = tab.props.name;
         var isSelected = (this.state.selectedTab == tabName);
         if (!tab.props.hidden) {
           tabButtons.push(
             <li className={cx({selected: isSelected})}
                 key={i}
                 data-tab-name={tabName}
                 onClick={this.handleSelectTab} />
@@ -50,17 +66,19 @@ loop.panel = (function(_, mozL10n) {
         tabs.push(
           <div key={i} className={cx({tab: true, selected: isSelected})}>
             {tab.props.children}
           </div>
         );
       }, this);
       return (
         <div className="tab-view-container">
-          <ul className="tab-view">{tabButtons}</ul>
+          {!this.props.buttonsHidden
+            ? <ul className="tab-view">{tabButtons}</ul>
+            : null}
           {tabs}
         </div>
       );
     }
   });
 
   var Tab = React.createClass({
     render: function() {
@@ -145,17 +163,17 @@ loop.panel = (function(_, mozL10n) {
       return {seenToS: navigator.mozLoop.getLoopCharPref('seenToS')};
     },
 
     render: function() {
       if (this.state.seenToS == "unseen") {
         var terms_of_use_url = navigator.mozLoop.getLoopCharPref('legal.ToS_url');
         var privacy_notice_url = navigator.mozLoop.getLoopCharPref('legal.privacy_url');
         var tosHTML = __("legal_text_and_links3", {
-          "clientShortname": __("client_shortname_fallback"),
+          "clientShortname": __("clientShortname2"),
           "terms_of_use": React.renderComponentToStaticMarkup(
             <a href={terms_of_use_url} target="_blank">
               {__("legal_text_tos")}
             </a>
           ),
           "privacy_notice": React.renderComponentToStaticMarkup(
             <a href={privacy_notice_url} target="_blank">
               {__("legal_text_privacy")}
@@ -223,16 +241,22 @@ loop.panel = (function(_, mozL10n) {
     },
 
     _isSignedIn: function() {
       return !!navigator.mozLoop.userProfile;
     },
 
     render: function() {
       var cx = React.addons.classSet;
+
+      // For now all of the menu entries require FxA so hide the whole gear if FxA is disabled.
+      if (!navigator.mozLoop.fxAEnabled) {
+        return null;
+      }
+
       return (
         <div className="settings-menu dropdown">
           <a className="button-settings" onClick={this.showDropdownMenu}
              title={__("settings_menu_button_tooltip")} />
           <ul className={cx({"dropdown-menu": true, hide: !this.state.showMenu})}
               onMouseLeave={this.hideDropdownMenu}>
             <SettingsDropdownEntry label={__("settings_menu_item_settings")}
                                    onClick={this.handleClickSettingsEntry}
@@ -241,16 +265,17 @@ loop.panel = (function(_, mozL10n) {
             <SettingsDropdownEntry label={__("settings_menu_item_account")}
                                    onClick={this.handleClickAccountEntry}
                                    icon="account"
                                    displayed={this._isSignedIn()} />
             <SettingsDropdownEntry label={this._isSignedIn() ?
                                           __("settings_menu_item_signout") :
                                           __("settings_menu_item_signin")}
                                    onClick={this.handleClickAuthEntry}
+                                   displayed={navigator.mozLoop.fxAEnabled}
                                    icon={this._isSignedIn() ? "signout" : "signin"} />
           </ul>
         </div>
       );
     }
   });
 
   /**
@@ -322,44 +347,62 @@ loop.panel = (function(_, mozL10n) {
       } else {
         try {
           var callUrl = new window.URL(callUrlData.callUrl);
           // XXX the current server vers does not implement the callToken field
           // but it exists in the API. This workaround should be removed in the future
           var token = callUrlData.callToken ||
                       callUrl.pathname.split('/').pop();
 
+          // Now that a new URL is available, indicate it has not been shared.
+          this.linkExfiltrated = false;
+
           this.setState({pending: false, copied: false,
                          callUrl: callUrl.href,
                          callUrlExpiry: callUrlData.expiresAt});
         } catch(e) {
           console.log(e);
           this.props.notifications.errorL10n("unable_retrieve_url");
           this.setState(this.getInitialState());
         }
       }
     },
 
     handleEmailButtonClick: function(event) {
       this.handleLinkExfiltration(event);
 
-      navigator.mozLoop.composeEmail(__("share_email_subject3"),
-        __("share_email_body3", { callUrl: this.state.callUrl }));
+      navigator.mozLoop.composeEmail(
+        __("share_email_subject4", { clientShortname: __("clientShortname2")}),
+        __("share_email_body4", { callUrl: this.state.callUrl,
+                                  clientShortname: __("clientShortname2"),
+                                  learnMoreUrl: navigator.mozLoop.getLoopCharPref("learnMoreUrl") }));
     },
 
     handleCopyButtonClick: function(event) {
       this.handleLinkExfiltration(event);
       // XXX the mozLoop object should be passed as a prop, to ease testing and
       //     using a fake implementation in UI components showcase.
       navigator.mozLoop.copyString(this.state.callUrl);
       this.setState({copied: true});
     },
 
+    linkExfiltrated: false,
+
     handleLinkExfiltration: function(event) {
-      // TODO Bug 1015988 -- Increase link exfiltration telemetry count
+      // Update the count of shared URLs only once per generated URL.
+      if (!this.linkExfiltrated) {
+        this.linkExfiltrated = true;
+        try {
+          navigator.mozLoop.telemetryAdd("LOOP_CLIENT_CALL_URL_SHARED", true);
+        } catch (err) {
+          console.error("Error recording telemetry", err);
+        }
+      }
+
+      // Note URL expiration every time it is shared.
       if (this.state.callUrlExpiry) {
         navigator.mozLoop.noteCallUrlExpiry(this.state.callUrlExpiry);
       }
     },
 
     render: function() {
       // XXX setting elem value from a state (in the callUrl input)
       // makes it immutable ie read only but that is fine in our case.
@@ -398,17 +441,17 @@ loop.panel = (function(_, mozL10n) {
    * FxA sign in/up link component.
    */
   var AuthLink = React.createClass({
     handleSignUpLinkClick: function() {
       navigator.mozLoop.logInToFxA();
     },
 
     render: function() {
-      if (navigator.mozLoop.userProfile) {
+      if (!navigator.mozLoop.fxAEnabled || navigator.mozLoop.userProfile) {
         return null;
       }
       return (
         <p className="signin-link">
           <a href="#" onClick={this.handleSignUpLinkClick}>
             {__("panel_footer_signin_or_signup_link")}
           </a>
         </p>
@@ -425,25 +468,145 @@ loop.panel = (function(_, mozL10n) {
         <p className="user-identity">
           {this.props.displayName}
         </p>
       );
     }
   });
 
   /**
+   * Room list entry.
+   */
+  var RoomEntry = React.createClass({
+    propTypes: {
+      openRoom: React.PropTypes.func.isRequired,
+      room:     React.PropTypes.instanceOf(loop.store.Room).isRequired
+    },
+
+    shouldComponentUpdate: function(nextProps, nextState) {
+      return nextProps.room.ctime > this.props.room.ctime;
+    },
+
+    handleClickRoom: function(event) {
+      event.preventDefault();
+      this.props.openRoom(this.props.room);
+    },
+
+    _isActive: function() {
+      // XXX bug 1074679 will implement this properly
+      return this.props.room.currSize > 0;
+    },
+
+    render: function() {
+      var room = this.props.room;
+      var roomClasses = React.addons.classSet({
+        "room-entry": true,
+        "room-active": this._isActive()
+      });
+
+      return (
+        <div className={roomClasses}>
+          <h2>
+            <span className="room-notification" />
+            {room.roomName}
+          </h2>
+          <p>
+            <a ref="room" href="#" onClick={this.handleClickRoom}>
+              {room.roomUrl}
+            </a>
+          </p>
+        </div>
+      );
+    }
+  });
+
+  /**
+   * Room list.
+   */
+  var RoomList = React.createClass({
+    mixins: [Backbone.Events],
+
+    propTypes: {
+      store: React.PropTypes.instanceOf(loop.store.RoomListStore).isRequired,
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      rooms: React.PropTypes.array
+    },
+
+    getInitialState: function() {
+      var storeState = this.props.store.getStoreState();
+      return {
+        error: this.props.error || storeState.error,
+        rooms: this.props.rooms || storeState.rooms,
+      };
+    },
+
+    componentWillMount: function() {
+      this.listenTo(this.props.store, "change", this._onRoomListChanged);
+
+      this.props.dispatcher.dispatch(new sharedActions.GetAllRooms());
+    },
+
+    componentWillUnmount: function() {
+      this.stopListening(this.props.store);
+    },
+
+    _onRoomListChanged: function() {
+      var storeState = this.props.store.getStoreState();
+      this.setState({
+        error: storeState.error,
+        rooms: storeState.rooms
+      });
+    },
+
+    _getListHeading: function() {
+      var numRooms = this.state.rooms.length;
+      if (numRooms === 0) {
+        return mozL10n.get("rooms_list_no_current_conversations");
+      }
+      return mozL10n.get("rooms_list_current_conversations", {num: numRooms});
+    },
+
+    openRoom: function(room) {
+      // XXX implement me; see bug 1074678
+    },
+
+    render: function() {
+      if (this.state.error) {
+        // XXX Better end user reporting of errors.
+        console.error(this.state.error);
+      }
+
+      return (
+        <div className="room-list">
+          <h1>{this._getListHeading()}</h1>
+          {
+            this.state.rooms.map(function(room, i) {
+              return <RoomEntry key={i} room={room} openRoom={this.openRoom} />;
+            }, this)
+          }
+        </div>
+      );
+    }
+  });
+
+  /**
    * Panel view.
    */
   var PanelView = React.createClass({
     propTypes: {
       notifications: React.PropTypes.object.isRequired,
       client: React.PropTypes.object.isRequired,
       // Mostly used for UI components showcase and unit tests
       callUrl: React.PropTypes.string,
       userProfile: React.PropTypes.object,
+      showTabButtons: React.PropTypes.bool,
+      selectedTab: React.PropTypes.string,
+      dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
+      roomListStore:
+        React.PropTypes.instanceOf(loop.store.RoomListStore).isRequired
     },
 
     getInitialState: function() {
       return {
         userProfile: this.props.userProfile || navigator.mozLoop.userProfile,
       };
     },
 
@@ -470,20 +633,43 @@ loop.panel = (function(_, mozL10n) {
           detailsButtonLabel: serviceError.error.friendlyDetailsButtonLabel,
         });
       } else {
         this.props.notifications.remove(this.props.notifications.get("service-error"));
       }
     },
 
     _onStatusChanged: function() {
-      this.setState({userProfile: navigator.mozLoop.userProfile});
+      var profile = navigator.mozLoop.userProfile;
+      var currUid = this.state.userProfile ? this.state.userProfile.uid : null;
+      var newUid = profile ? profile.uid : null;
+      if (currUid != newUid) {
+        // On profile change (login, logout), switch back to the default tab.
+        this.selectTab("call");
+      }
+      this.setState({userProfile: profile});
       this.updateServiceErrors();
     },
 
+    /**
+     * The rooms feature is hidden by default for now. Once it gets mainstream,
+     * this method can be safely removed.
+     */
+    _renderRoomsTab: function() {
+      if (!navigator.mozLoop.getLoopBoolPref("rooms.enabled")) {
+        return null;
+      }
+      return (
+        <Tab name="rooms">
+          <RoomList dispatcher={this.props.dispatcher}
+                    store={this.props.roomListStore} />
+        </Tab>
+      );
+    },
+
     startForm: function(name, contact) {
       this.refs[name].initForm(contact);
       this.selectTab(name);
     },
 
     selectTab: function(name) {
       this.refs.tabView.setState({ selectedTab: name });
     },
@@ -503,25 +689,27 @@ loop.panel = (function(_, mozL10n) {
     render: function() {
       var NotificationListView = sharedViews.NotificationListView;
       var displayName = this.state.userProfile && this.state.userProfile.email ||
                         __("display_name_guest");
       return (
         <div>
           <NotificationListView notifications={this.props.notifications}
                                 clearOnDocumentHidden={true} />
-          <TabView ref="tabView">
+          <TabView ref="tabView" selectedTab={this.props.selectedTab}
+            buttonsHidden={!this.state.userProfile && !this.props.showTabButtons}>
             <Tab name="call">
               <div className="content-area">
                 <CallUrlResult client={this.props.client}
                                notifications={this.props.notifications}
                                callUrl={this.props.callUrl} />
                 <ToSView />
               </div>
             </Tab>
+            {this._renderRoomsTab()}
             <Tab name="contacts">
               <ContactsList selectTab={this.selectTab}
                             startForm={this.startForm} />
             </Tab>
             <Tab name="contacts_add" hidden={true}>
               <ContactDetailsForm ref="contacts_add" mode="add"
                                   selectTab={this.selectTab} />
             </Tab>
@@ -551,35 +739,45 @@ loop.panel = (function(_, mozL10n) {
    * Panel initialisation.
    */
   function init() {
     // Do the initial L10n setup, we do this before anything
     // else to ensure the L10n environment is setup correctly.
     mozL10n.initialize(navigator.mozLoop);
 
     var client = new loop.Client();
-    var notifications = new sharedModels.NotificationCollection()
+    var notifications = new sharedModels.NotificationCollection();
+    var dispatcher = new loop.Dispatcher();
+    var roomListStore = new loop.store.RoomListStore({
+      mozLoop: navigator.mozLoop,
+      dispatcher: dispatcher
+    });
 
     React.renderComponent(<PanelView
       client={client}
-      notifications={notifications} />, document.querySelector("#main"));
+      notifications={notifications}
+      roomListStore={roomListStore}
+      dispatcher={dispatcher}
+    />, document.querySelector("#main"));
 
     document.body.classList.add(loop.shared.utils.getTargetPlatform());
     document.body.setAttribute("dir", mozL10n.getDirection());
 
     // Notify the window that we've finished initalization and initial layout
     var evtObject = document.createEvent('Event');
     evtObject.initEvent('loopPanelInitialized', true, false);
     window.dispatchEvent(evtObject);
   }
 
   return {
     init: init,
     UserIdentity: UserIdentity,
+    AuthLink: AuthLink,
     AvailabilityDropdown: AvailabilityDropdown,
     CallUrlResult: CallUrlResult,
     PanelView: PanelView,
+    RoomList: RoomList,
     SettingsDropdown: SettingsDropdown,
     ToSView: ToSView
   };
 })(_, document.mozL10n);
 
 document.addEventListener('DOMContentLoaded', loop.panel.init);
--- a/browser/components/loop/content/panel.html
+++ b/browser/components/loop/content/panel.html
@@ -1,16 +1,15 @@
 <!DOCTYPE html>
 <!-- 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/.  -->
 <html>
   <head>
     <meta charset="utf-8">
-    <title>Loop Panel</title>
     <link rel="stylesheet" type="text/css" href="loop/shared/css/reset.css">
     <link rel="stylesheet" type="text/css" href="loop/shared/css/common.css">
     <link rel="stylesheet" type="text/css" href="loop/shared/css/panel.css">
     <link rel="stylesheet" type="text/css" href="loop/shared/css/contacts.css">
   </head>
   <body class="panel">
 
     <div id="main"></div>
@@ -20,13 +19,17 @@
     <script type="text/javascript" src="loop/shared/libs/jquery-2.1.0.js"></script>
     <script type="text/javascript" src="loop/shared/libs/lodash-2.4.1.js"></script>
     <script type="text/javascript" src="loop/shared/libs/backbone-1.1.2.js"></script>
 
     <script type="text/javascript" src="loop/shared/js/utils.js"></script>
     <script type="text/javascript" src="loop/shared/js/models.js"></script>
     <script type="text/javascript" src="loop/shared/js/mixins.js"></script>
     <script type="text/javascript" src="loop/shared/js/views.js"></script>
+    <script type="text/javascript" src="loop/shared/js/validate.js"></script>
+    <script type="text/javascript" src="loop/shared/js/actions.js"></script>
+    <script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
+    <script type="text/javascript" src="loop/shared/js/roomListStore.js"></script>
     <script type="text/javascript" src="loop/js/client.js"></script>
     <script type="text/javascript;version=1.8" src="loop/js/contacts.js"></script>
     <script type="text/javascript" src="loop/js/panel.js"></script>
  </body>
 </html>
--- a/browser/components/loop/content/shared/css/common.css
+++ b/browser/components/loop/content/shared/css/common.css
@@ -8,17 +8,17 @@
  * "Fixes" the Box Model.
  * @see http://www.paulirish.com/2012/box-sizing-border-box-ftw/
  */
 *, *:before, *:after {
   box-sizing: border-box;
 }
 
 body {
-  font-family: "Lucida Grande", sans-serif;
+  font: message-box;
   font-size: 12px;
   background: #fbfbfb;
 }
 
 img {
   border: none;
 }
 
@@ -228,17 +228,16 @@ p {
   border-radius: 2px;
   border-bottom-right-radius: 0;
   border-top-right-radius: 0;
 }
 
 /* Alerts/Notifications */
 .notificationContainer {
   border-bottom: 2px solid #E9E9E9;
-  margin-bottom: 1em;
 }
 
 .messages > .notificationContainer > .alert {
   text-align: center;
 }
 
 .notificationContainer > .detailsBar,
 .alert {
@@ -360,33 +359,16 @@ p {
 }
 
 .mac p,
 .windows p,
 .linux p {
   line-height: 16px;
 }
 
-/* Using star selector to override
- * the specificity of other selectors
- * if performance is an issue we could
- * explicitely list all the elements */
-.windows * {
-  font-family: 'Segoe';
-}
-
-.mac * {
-  font-family: 'Lucida Grande';
-}
-
-.linux * {
-  /* XXX requires fallbacks */
-  font-family: 'Ubuntu', sans-serif;
-}
-
 /* Web panel */
 
 .info-panel {
   border-radius: 4px;
   background: #fff;
   padding: 20px 0;
   border: 1px solid #e7e7e7;
   box-shadow: 0 2px 0 rgba(0, 0, 0, .03);
--- a/browser/components/loop/content/shared/css/contacts.css
+++ b/browser/components/loop/content/shared/css/contacts.css
@@ -1,20 +1,24 @@
 /* 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/. */
 
+.content-area input.contact-filter {
+  margin-top: 14px;
+  border-radius: 10000px;
+}
+
 .contact-list {
   border-top: 1px solid #ccc;
   overflow-x: hidden;
   overflow-y: auto;
-  /* We need enough space to show the context menu of the first contact. */
-  min-height: 204px;
-  /* Show six contacts and scroll for the rest. */
-  max-height: 306px;
+  /* Space for six contacts, not affected by filtering.  This is enough space
+     to show the dropdown menu when there is only one contact. */
+  height: 306px;
 }
 
 .contact,
 .contact-separator {
   padding: 5px 10px;
   font-size: 13px;
 }
 
--- a/browser/components/loop/content/shared/css/panel.css
+++ b/browser/components/loop/content/shared/css/panel.css
@@ -1,12 +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/. */
 
+body {
+  background: none;
+}
+
 /* Panel styles */
 
 .panel {
   /* hide the extra margin space that the panel resizer now wants to show */
   overflow: hidden;
 }
 
 /* Notifications displayed over tabs */
@@ -16,22 +20,27 @@
 }
 
 .panel .messages .alert {
   margin: 0;
 }
 
 /* Tabs and tab selection buttons */
 
+.tab-view-container {
+  background-image: url("../img/beta-ribbon.svg#beta-ribbon");
+  background-size: 36px 36px;
+  background-repeat: no-repeat;
+}
+
 .tab-view {
   display: flex;
   flex-direction: row;
   padding: 10px;
   border-bottom: 1px solid #ccc;
-  background-color: #fbfbfb;
   color: #000;
   border-top-right-radius: 2px;
   border-top-left-radius: 2px;
   list-style: none;
 }
 
 .tab-view > li {
   flex: 1;
@@ -85,16 +94,22 @@
 /* Content area and input fields */
 
 .content-area {
   padding: 14px;
 }
 
 .content-area header {
   font-weight: 700;
+  -moz-padding-start: 20px;
+}
+
+.tab-view + .tab .content-area header {
+  /* The header shouldn't be indented if the tabs are present. */
+  -moz-padding-start: 0;
 }
 
 .content-area label {
   display: block;
   width: 100%;
   margin-top: 10px;
   font-size: 10px;
   color: #777;
@@ -115,16 +130,80 @@
   box-shadow: none;
 }
 
 .content-area input:not(.pristine):invalid {
   border-color: #d74345;
   box-shadow: 0 0 4px #c43c3e;
 }
 
+/* Rooms */
+.room-list {
+  background: #f5f5f5;
+}
+
+.room-list > h1 {
+  font-weight: bold;
+  color: #999;
+  padding: .5rem 1rem;
+  border-bottom: 1px solid #ddd;
+}
+
+.room-list > .room-entry {
+  padding: 1rem 1rem 0 .5rem;
+}
+
+.room-list > .room-entry > h2 {
+  font-size: .85rem;
+  color: #777;
+}
+
+.room-list > .room-entry.room-active > h2 {
+  font-weight: bold;
+  color: #000;
+}
+
+.room-list > .room-entry > h2 > .room-notification {
+  display: inline-block;
+  background: transparent;
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  margin-right: .3rem;
+}
+
+.room-list > .room-entry.room-active > h2 > .room-notification {
+  background-color: #00a0ec;
+}
+
+.room-list > .room-entry:hover {
+  background: #f1f1f1;
+}
+
+.room-list > .room-entry:not(:last-child) {
+  border-bottom: 1px solid #ddd;
+}
+
+.room-list > .room-entry > p {
+  margin: 0;
+  padding: .2em 0 1rem .8rem;
+}
+
+.room-list > .room-entry > p > a {
+  color: #777;
+  opacity: .5;
+  transition: opacity .1s ease-in-out 0s;
+  text-decoration: none;
+}
+
+.room-list > .room-entry > p > a:hover {
+  opacity: 1;
+  text-decoration: underline;
+}
+
 /* Buttons */
 
 .button-group {
   display: flex;
   flex-direction: row;
   width: 100%;
 }
 
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/beta-ribbon.svg
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+     viewBox="0 0 100 100"
+     xmlns:xlink="http://www.w3.org/1999/xlink"
+     enable-background="new 0 0 100 100">
+<style>
+</style>
+<defs>
+  <g id="beta-ribbon">
+    <path fill="#e6e6e6" d="M0,100 100,0 49.1,0 0,49.2z"/>
+    <path fill="#fff" d="M0,94.7 94.7,0 46.5,0 0,46.6z"/>
+    <g fill="#999">
+      <path d="m25.9,56.7l-4,4-13.7-13.7 3.7-3.7c2.4-2.4 5.7-4.1 8.3-1.4 1.7,1.7 1.4,3.9 .5,5.3l.1,.1c1.6-1.1 4-1.9 6.3,.3 3,3 1.3,6.5-1.2,9.1zm-12.2-12.2l-2.2,2.2 4.3,4.3 2.3-2.3c1.6-1.6 1.8-3.1 .2-4.7-1.4-1.6-2.9-1.2-4.6,.5zm6.1,5.4l-2.5,2.5 4.9,4.9 2.4-2.4c1.3-1.3 2.5-3.3 .6-5.3-2-2-3.9-1.2-5.4,.3z"/>
+      <path d="m30.7,42.7c2.5,2.2 4.6,2.1 6.2,.5 1-1 1.5-2 1.6-3.6l2,.4c-.2,1.7-.9,3.4-2.2,4.7-3.1,3.1-6.9,2.5-10.2-.7-3.2-3.2-3.8-7.1-1.1-9.9 2.7-2.7 6.2-2.3 9.4,.9 .4,.4 .7,.7 .9,1l-6.6,6.7zm-1.4-1.4l4.9-4.9c-2.2-2.1-4.1-2.5-5.7-.9-1.4,1.5-1.3,3.4 .8,5.8z"/>
+      <path d="m49.8,31.8c-.2,1.1-.8,2.2-1.6,3.1-1.9,1.9-4,1.6-5.9-.2l-6.3-6.3-1.6,1.6-1.4-1.4 1.7-1.7-2.4-2.4 1.6-2 2.6,2.6 2.6-2.6 1.2,1.6-2.4,2.4 6.3,6.3c1.1,1.1 1.9,1.2 2.8,.3 .5-.5 .7-1 .9-1.8l1.9,.5z"/>
+      <path d="m57,20.8c.8,.8 1.4,.9 2.1,.6l.8,1.7c-1.1,.8-2.1,1.1-3.4,.5 .3,1.7-.4,3.3-1.6,4.6-2.1,2.1-4.7,2.1-6.6,.2-2.2-2.2-1.6-5.1 1.5-8.3l1.4-1.3-.8-.8c-1.5-1.5-2.8-1.3-4.3,.2-.7,.7-1.5,1.8-2.2,3.3l-1.8-.9c.8-1.8 1.8-3.1 2.8-4.2 2.7-2.7 5.1-2.5 7.2-.4l4.9,4.8zm-2,1.6l-2.6-2.6-1.3,1.3c-2.2,2.2-2.2,3.8-.9,5.1 1.3,1.3 2.5,1.4 3.8,.1 1.1-1.1 1.3-2.4 1-3.9z"/>
+      <path d="M93.4,0 0,94.1 0,96 95.2,0z"/>
+      <path d="M45.3,0 0,46 0,47.9 47,0z"/>
+    </g>
+  </g>
+</defs>
+<use id="beta-ribbon" xlink:href="#beta-ribbon"/>
+</svg>
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -29,21 +29,19 @@ loop.shared.actions = (function() {
     return Action.bind(null, name, schema);
   };
 
   return {
     /**
      * Used to trigger gathering of initial call data.
      */
     GatherCallData: Action.define("gatherCallData", {
-      // XXX This may change when bug 1072323 is implemented.
-      // Optional: Specify the calleeId for an outgoing call
-      calleeId: [String, null],
       // Specify the callId for an incoming call.
-      callId: [String, null]
+      callId: [String, null],
+      outgoing: Boolean
     }),
 
     /**
      * Used to cancel call setup.
      */
     CancelCall: Action.define("cancelCall", {
     }),
 
@@ -65,26 +63,68 @@ loop.shared.actions = (function() {
 
     /**
      * Used for hanging up the call at the end of a successful call.
      */
     HangupCall: Action.define("hangupCall", {
     }),
 
     /**
+     * Used to indicate the peer hung up the call.
+     */
+    PeerHungupCall: Action.define("peerHungupCall", {
+    }),
+
+    /**
      * Used for notifying of connection progress state changes.
      * The connection refers to the overall connection flow as indicated
      * on the websocket.
      */
     ConnectionProgress: Action.define("connectionProgress", {
       // The connection state from the websocket.
       wsState: String
     }),
 
     /**
      * Used for notifying of connection failures.
      */
     ConnectionFailure: Action.define("connectionFailure", {
       // A string relating to the reason the connection failed.
       reason: String
+    }),
+
+    /**
+     * Used by the ongoing views to notify stores about the elements
+     * required for the sdk.
+     */
+    SetupStreamElements: Action.define("setupStreamElements", {
+      // The configuration for the publisher/subscribe options
+      publisherConfig: Object,
+      // The local stream element
+      getLocalElementFunc: Function,
+      // The remote stream element
+      getRemoteElementFunc: Function
+    }),
+
+    /**
+     * Used for notifying that the media is now up for the call.
+     */
+    MediaConnected: Action.define("mediaConnected", {
+    }),
+
+    /**
+     * Used to mute or unmute a stream
+     */
+    SetMute: Action.define("setMute", {
+      // The part of the stream to enable, e.g. "audio" or "video"
+      type: String,
+      // Whether or not to enable the stream.
+      enabled: Boolean
+    }),
+
+    /**
+     * Retrieves room list.
+     * XXX: should move to some roomActions module - refs bug 1079284
+     */
+    GetAllRooms: Action.define("getAllRooms", {
     })
   };
 })();
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -1,42 +1,43 @@
 /* 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/. */
 
 /* global loop:true */
 
 var loop = loop || {};
-loop.store = (function() {
+loop.store = loop.store || {};
 
+loop.store.ConversationStore = (function() {
   var sharedActions = loop.shared.actions;
-  var sharedUtils = loop.shared.utils;
+  var CALL_TYPES = loop.shared.utils.CALL_TYPES;
 
   /**
    * Websocket states taken from:
    * https://docs.services.mozilla.com/loop/apis.html#call-progress-state-change-progress
    */
-  var WS_STATES = {
+  var WS_STATES = loop.store.WS_STATES = {
     // The call is starting, and the remote party is not yet being alerted.
     INIT: "init",
     // The called party is being alerted.
     ALERTING: "alerting",
     // The call is no longer being set up and has been aborted for some reason.
     TERMINATED: "terminated",
     // The called party has indicated that he has answered the call,
     // but the media is not yet confirmed.
     CONNECTING: "connecting",
     // One of the two parties has indicated successful media set up,
     // but the other has not yet.
     HALF_CONNECTED: "half-connected",
     // Both endpoints have reported successfully establishing media.
     CONNECTED: "connected"
   };
 
-  var CALL_STATES = {
+  var CALL_STATES = loop.store.CALL_STATES = {
     // The initial state of the view.
     INIT: "cs-init",
     // The store is gathering the call data from the server.
     GATHER: "cs-gather",
     // The initial data has been gathered, the websocket is connecting, or has
     // connected, and waiting for the other side to connect to the server.
     CONNECTING: "cs-connecting",
     // The websocket has received information that we're now alerting
@@ -47,46 +48,49 @@ loop.store = (function() {
     // The call ended successfully.
     FINISHED: "cs-finished",
     // The user has finished with the window.
     CLOSE: "cs-close",
     // The call was terminated due to an issue during connection.
     TERMINATED: "cs-terminated"
   };
 
-
   var ConversationStore = Backbone.Model.extend({
     defaults: {
       // The current state of the call
       callState: CALL_STATES.INIT,
       // The reason if a call was terminated
       callStateReason: undefined,
       // The error information, if there was a failure
       error: undefined,
       // True if the call is outgoing, false if not, undefined if unknown
       outgoing: undefined,
-      // The id of the person being called for outgoing calls
-      calleeId: undefined,
+      // The contact being called for outgoing calls
+      contact: undefined,
       // The call type for the call.
       // XXX Don't hard-code, this comes from the data in bug 1072323
-      callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO,
+      callType: CALL_TYPES.AUDIO_VIDEO,
 
       // Call Connection information
       // The call id from the loop-server
       callId: undefined,
       // The connection progress url to connect the websocket
       progressURL: undefined,
       // The websocket token that allows connection to the progress url
       websocketToken: undefined,
       // SDK API key
       apiKey: undefined,
       // SDK session ID
       sessionId: undefined,
       // SDK session token
-      sessionToken: undefined
+      sessionToken: undefined,
+      // If the audio is muted
+      audioMuted: false,
+      // If the video is muted
+      videoMuted: false
     },
 
     /**
      * Constructor
      *
      * Options:
      * - {loop.Dispatcher} dispatcher The dispatcher for dispatching actions and
      *                                registering to consume actions.
@@ -99,38 +103,46 @@ loop.store = (function() {
       options = options || {};
 
       if (!options.dispatcher) {
         throw new Error("Missing option dispatcher");
       }
       if (!options.client) {
         throw new Error("Missing option client");
       }
+      if (!options.sdkDriver) {
+        throw new Error("Missing option sdkDriver");
+      }
 
       this.client = options.client;
       this.dispatcher = options.dispatcher;
+      this.sdkDriver = options.sdkDriver;
 
       this.dispatcher.register(this, [
         "connectionFailure",
         "connectionProgress",
         "gatherCallData",
         "connectCall",
         "hangupCall",
+        "peerHungupCall",
         "cancelCall",
-        "retryCall"
+        "retryCall",
+        "mediaConnected",
+        "setMute"
       ]);
     },
 
     /**
      * Handles the connection failure action, setting the state to
      * terminated.
      *
      * @param {sharedActions.ConnectionFailure} actionData The action data.
      */
     connectionFailure: function(actionData) {
+      this._endSession();
       this.set({
         callState: CALL_STATES.TERMINATED,
         callStateReason: actionData.reason
       });
     },
 
     /**
      * Handles the connection progress action, setting the next state
@@ -147,17 +159,25 @@ loop.store = (function() {
             this.set({callState: CALL_STATES.CONNECTING});
           }
           break;
         }
         case WS_STATES.ALERTING: {
           this.set({callState: CALL_STATES.ALERTING});
           break;
         }
-        case WS_STATES.CONNECTING:
+        case WS_STATES.CONNECTING: {
+          this.sdkDriver.connectSession({
+            apiKey: this.get("apiKey"),
+            sessionId: this.get("sessionId"),
+            sessionToken: this.get("sessionToken")
+          });
+          this.set({callState: CALL_STATES.ONGOING});
+          break;
+        }
         case WS_STATES.HALF_CONNECTED:
         case WS_STATES.CONNECTED: {
           this.set({callState: CALL_STATES.ONGOING});
           break;
         }
         default: {
           console.error("Unexpected websocket state passed to connectionProgress:",
             actionData.wsState);
@@ -167,23 +187,40 @@ loop.store = (function() {
 
     /**
      * Handles the gather call data action, setting the state
      * and starting to get the appropriate data for the type of call.
      *
      * @param {sharedActions.GatherCallData} actionData The action data.
      */
     gatherCallData: function(actionData) {
+      if (!actionData.outgoing) {
+        // XXX Other types aren't supported yet, but set the state for the
+        // view selection.
+        this.set({outgoing: false});
+        return;
+      }
+
+      var callData = navigator.mozLoop.getCallData(actionData.callId);
+      if (!callData) {
+        console.error("Failed to get the call data");
+        this.set({callState: CALL_STATES.TERMINATED});
+        return;
+      }
+
       this.set({
-        calleeId: actionData.calleeId,
-        outgoing: !!actionData.calleeId,
+        contact: callData.contact,
+        outgoing: actionData.outgoing,
         callId: actionData.callId,
+        callType: callData.callType,
         callState: CALL_STATES.GATHER
       });
 
+      this.set({videoMuted: this.get("callType") === CALL_TYPES.AUDIO_ONLY});
+
       if (this.get("outgoing")) {
         this._setupOutgoingCall();
       } // XXX Else, other types aren't supported yet.
     },
 
     /**
      * Handles the connect call action, this saves the appropriate
      * data and starts the connection for the websocket to notify the
@@ -195,51 +232,47 @@ loop.store = (function() {
       this.set(actionData.sessionData);
       this._connectWebSocket();
     },
 
     /**
      * Hangs up an ongoing call.
      */
     hangupCall: function() {
-      // XXX Stop the SDK once we add it.
-
-      // Ensure the websocket has been disconnected.
       if (this._websocket) {
         // Let the server know the user has hung up.
         this._websocket.mediaFail();
-        this._ensureWebSocketDisconnected();
       }
 
+      this._endSession();
+      this.set({callState: CALL_STATES.FINISHED});
+    },
+
+    /**
+     * The peer hungup the call.
+     */
+    peerHungupCall: function() {
+      this._endSession();
       this.set({callState: CALL_STATES.FINISHED});
     },
 
     /**
      * Cancels a call
      */
     cancelCall: function() {
       var callState = this.get("callState");
-      if (callState === CALL_STATES.TERMINATED) {
-        // All we need to do is close the window.
-        this.set({callState: CALL_STATES.CLOSE});
-        return;
+      if (this._websocket &&
+          (callState === CALL_STATES.CONNECTING ||
+           callState === CALL_STATES.ALERTING)) {
+         // Let the server know the user has hung up.
+        this._websocket.cancel();
       }
 
-      if (callState === CALL_STATES.CONNECTING ||
-          callState === CALL_STATES.ALERTING) {
-        if (this._websocket) {
-          // Let the server know the user has hung up.
-          this._websocket.cancel();
-          this._ensureWebSocketDisconnected();
-        }
-        this.set({callState: CALL_STATES.CLOSE});
-        return;
-      }
-
-      console.log("Unsupported cancel in state", callState);
+      this._endSession();
+      this.set({callState: CALL_STATES.CLOSE});
     },
 
     /**
      * Retries a call
      */
     retryCall: function() {
       var callState = this.get("callState");
       if (callState !== CALL_STATES.TERMINATED) {
@@ -249,22 +282,44 @@ loop.store = (function() {
 
       this.set({callState: CALL_STATES.GATHER});
       if (this.get("outgoing")) {
         this._setupOutgoingCall();
       }
     },
 
     /**
+     * Notifies that all media is now connected
+     */
+    mediaConnected: function() {
+      this._websocket.mediaUp();
+    },
+
+    /**
+     * Records the mute state for the stream.
+     *
+     * @param {sharedActions.setMute} actionData The mute state for the stream type.
+     */
+    setMute: function(actionData) {
+      var muteType = actionData.type + "Muted";
+      this.set(muteType, !actionData.enabled);
+    },
+
+    /**
      * Obtains the outgoing call data from the server and handles the
      * result.
      */
     _setupOutgoingCall: function() {
-      // XXX For now, we only have one calleeId, so just wrap that in an array.
-      this.client.setupOutgoingCall([this.get("calleeId")],
+      var contactAddresses = [];
+
+      this.get("contact").email.forEach(function(address) {
+        contactAddresses.push(address.value);
+      });
+
+      this.client.setupOutgoingCall(contactAddresses,
         this.get("callType"),
         function(err, result) {
           if (err) {
             console.error("Failed to get outgoing call data", err);
             this.dispatcher.dispatch(
               new sharedActions.ConnectionFailure({reason: "setup"}));
             return;
           }
@@ -303,24 +358,27 @@ loop.store = (function() {
           }));
         }.bind(this)
       );
 
       this.listenTo(this._websocket, "progress", this._handleWebSocketProgress);
     },
 
     /**
-     * Ensures the websocket gets disconnected.
+     * Ensures the session is ended and the websocket is disconnected.
      */
-    _ensureWebSocketDisconnected: function() {
-     this.stopListening(this._websocket);
+    _endSession: function(nextState) {
+      this.sdkDriver.disconnectSession();
+      if (this._websocket) {
+        this.stopListening(this._websocket);
 
-      // Now close the websocket.
-      this._websocket.close();
-      delete this._websocket;
+        // Now close the websocket.
+        this._websocket.close();
+        delete this._websocket;
+      }
     },
 
     /**
      * Used to handle any progressed received from the websocket. This will
      * dispatch new actions so that the data can be handled appropriately.
      */
     _handleWebSocketProgress: function(progressData) {
       var action;
@@ -339,14 +397,10 @@ loop.store = (function() {
           break;
         }
       }
 
       this.dispatcher.dispatch(action);
     }
   });
 
-  return {
-    CALL_STATES: CALL_STATES,
-    ConversationStore: ConversationStore,
-    WS_STATES: WS_STATES
-  };
+  return ConversationStore;
 })();
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/js/otSdkDriver.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/. */
+
+/* global loop:true */
+
+var loop = loop || {};
+loop.OTSdkDriver = (function() {
+
+  var sharedActions = loop.shared.actions;
+
+  /**
+   * This is a wrapper for the OT sdk. It is used to translate the SDK events into
+   * actions, and instruct the SDK what to do as a result of actions.
+   */
+  var OTSdkDriver = function(options) {
+      if (!options.dispatcher) {
+        throw new Error("Missing option dispatcher");
+      }
+      if (!options.sdk) {
+        throw new Error("Missing option sdk");
+      }
+
+      this.dispatcher = options.dispatcher;
+      this.sdk = options.sdk;
+
+      this.dispatcher.register(this, [
+        "setupStreamElements",
+        "setMute"
+      ]);
+  };
+
+  OTSdkDriver.prototype = {
+    /**
+     * Handles the setupStreamElements action. Saves the required data and
+     * kicks off the initialising of the publisher.
+     *
+     * @param {sharedActions.SetupStreamElements} actionData The data associated
+     *   with the action. See action.js.
+     */
+    setupStreamElements: function(actionData) {
+      this.getLocalElement = actionData.getLocalElementFunc;
+      this.getRemoteElement = actionData.getRemoteElementFunc;
+      this.publisherConfig = actionData.publisherConfig;
+
+      // At this state we init the publisher, even though we might be waiting for
+      // the initial connect of the session. This saves time when setting up
+      // the media.
+      this.publisher = this.sdk.initPublisher(this.getLocalElement(),
+        this.publisherConfig,
+        this._onPublishComplete.bind(this));
+    },
+
+    /**
+     * Handles the setMute action. Informs the published stream to mute
+     * or unmute audio as appropriate.
+     *
+     * @param {sharedActions.SetMute} actionData The data associated with the
+     *                                           action. See action.js.
+     */
+    setMute: function(actionData) {
+      if (actionData.type === "audio") {
+        this.publisher.publishAudio(actionData.enabled);
+      } else {
+        this.publisher.publishVideo(actionData.enabled);
+      }
+    },
+
+    /**
+     * Connects a session for the SDK, listening to the required events.
+     *
+     * sessionData items:
+     * - sessionId: The OT session ID
+     * - apiKey: The OT API key
+     * - sessionToken: The token for the OT session
+     *
+     * @param {Object} sessionData The session data for setting up the OT session.
+     */
+    connectSession: function(sessionData) {
+      this.session = this.sdk.initSession(sessionData.sessionId);
+
+      this.session.on("streamCreated", this._onRemoteStreamCreated.bind(this));
+      this.session.on("connectionDestroyed",
+        this._onConnectionDestroyed.bind(this));
+      this.session.on("sessionDisconnected",
+        this._onSessionDisconnected.bind(this));
+
+      // This starts the actual session connection.
+      this.session.connect(sessionData.apiKey, sessionData.sessionToken,
+        this._onConnectionComplete.bind(this));
+    },
+
+    /**
+     * Disconnects the sdk session.
+     */
+    disconnectSession: function() {
+      if (this.session) {
+        this.session.off("streamCreated", this._onRemoteStreamCreated.bind(this));
+        this.session.off("connectionDestroyed",
+          this._onConnectionDestroyed.bind(this));
+        this.session.off("sessionDisconnected",
+          this._onSessionDisconnected.bind(this));
+
+        this.session.disconnect();
+        delete this.session;
+      }
+      if (this.publisher) {
+        this.publisher.destroy();
+        delete this.publisher;
+      }
+
+      // Also, tidy these variables ready for next time.
+      delete this._sessionConnected;
+      delete this._publisherReady;
+      delete this._publishedLocalStream;
+      delete this._subscribedRemoteStream;
+    },
+
+    /**
+     * Called once the session has finished connecting.
+     *
+     * @param {Error} error An OT error object, null if there was no error.
+     */
+    _onConnectionComplete: function(error) {
+      if (error) {
+        console.error("Failed to complete connection", error);
+        this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
+          reason: "couldNotConnect"
+        }));
+        return;
+      }
+
+      this._sessionConnected = true;
+      this._maybePublishLocalStream();
+    },
+
+    /**
+     * Handles the connection event for a peer's connection being dropped.
+     *
+     * @param {SessionDisconnectEvent} event The event details
+     * https://tokbox.com/opentok/libraries/client/js/reference/SessionDisconnectEvent.html
+     */
+    _onConnectionDestroyed: function(event) {
+      var action;
+      if (event.reason === "clientDisconnected") {
+        action = new sharedActions.PeerHungupCall();
+      } else {
+        // Strictly speaking this isn't a failure on our part, but since our
+        // flow requires a full reconnection, then we just treat this as
+        // if a failure of our end had occurred.
+        action = new sharedActions.ConnectionFailure({
+          reason: "peerNetworkDisconnected"
+        });
+      }
+      this.dispatcher.dispatch(action);