Merge mozilla-central to inbound. a=merge CLOSED TREE
authorGurzau Raul <rgurzau@mozilla.com>
Wed, 10 Oct 2018 01:04:25 +0300
changeset 496110 38890d8fa2542082eafe6e16dfc26b87d42ade94
parent 496109 4d07362984d4e00ef10251fa54e652426d87bfcf (current diff)
parent 496049 6f8701d1be0ccf42a8e22bfce6f40056a4f58a1b (diff)
child 496111 241f876d557f679b81c2aa6d73f0ec598cc49545
push id9984
push userffxbld-merge
push dateMon, 15 Oct 2018 21:07:35 +0000
treeherdermozilla-beta@183d27ea8570 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge mozilla-central to inbound. a=merge CLOSED TREE
accessible/jsat/OutputGenerator.jsm
accessible/jsat/Presentation.jsm
layout/base/nsLayoutUtils.cpp
media/libjpeg/README.md
media/libjpeg/simd/jccolext-altivec.c
media/libjpeg/simd/jccolext-mmx.asm
media/libjpeg/simd/jccolext-sse2-64.asm
media/libjpeg/simd/jccolext-sse2.asm
media/libjpeg/simd/jccolor-altivec.c
media/libjpeg/simd/jccolor-mmx.asm
media/libjpeg/simd/jccolor-sse2-64.asm
media/libjpeg/simd/jccolor-sse2.asm
media/libjpeg/simd/jcgray-altivec.c
media/libjpeg/simd/jcgray-mmx.asm
media/libjpeg/simd/jcgray-sse2-64.asm
media/libjpeg/simd/jcgray-sse2.asm
media/libjpeg/simd/jcgryext-altivec.c
media/libjpeg/simd/jcgryext-mmx.asm
media/libjpeg/simd/jcgryext-sse2-64.asm
media/libjpeg/simd/jcgryext-sse2.asm
media/libjpeg/simd/jchuff-sse2-64.asm
media/libjpeg/simd/jchuff-sse2.asm
media/libjpeg/simd/jcolsamp.inc
media/libjpeg/simd/jcsample-altivec.c
media/libjpeg/simd/jcsample-mmx.asm
media/libjpeg/simd/jcsample-sse2-64.asm
media/libjpeg/simd/jcsample-sse2.asm
media/libjpeg/simd/jcsample.h
media/libjpeg/simd/jdcolext-altivec.c
media/libjpeg/simd/jdcolext-mmx.asm
media/libjpeg/simd/jdcolext-sse2-64.asm
media/libjpeg/simd/jdcolext-sse2.asm
media/libjpeg/simd/jdcolor-altivec.c
media/libjpeg/simd/jdcolor-mmx.asm
media/libjpeg/simd/jdcolor-sse2-64.asm
media/libjpeg/simd/jdcolor-sse2.asm
media/libjpeg/simd/jdct.inc
media/libjpeg/simd/jdmerge-altivec.c
media/libjpeg/simd/jdmerge-mmx.asm
media/libjpeg/simd/jdmerge-sse2-64.asm
media/libjpeg/simd/jdmerge-sse2.asm
media/libjpeg/simd/jdmrgext-altivec.c
media/libjpeg/simd/jdmrgext-mmx.asm
media/libjpeg/simd/jdmrgext-sse2-64.asm
media/libjpeg/simd/jdmrgext-sse2.asm
media/libjpeg/simd/jdsample-altivec.c
media/libjpeg/simd/jdsample-mmx.asm
media/libjpeg/simd/jdsample-sse2-64.asm
media/libjpeg/simd/jdsample-sse2.asm
media/libjpeg/simd/jfdctflt-3dn.asm
media/libjpeg/simd/jfdctflt-sse-64.asm
media/libjpeg/simd/jfdctflt-sse.asm
media/libjpeg/simd/jfdctfst-altivec.c
media/libjpeg/simd/jfdctfst-mmx.asm
media/libjpeg/simd/jfdctfst-sse2-64.asm
media/libjpeg/simd/jfdctfst-sse2.asm
media/libjpeg/simd/jfdctint-altivec.c
media/libjpeg/simd/jfdctint-mmx.asm
media/libjpeg/simd/jfdctint-sse2-64.asm
media/libjpeg/simd/jfdctint-sse2.asm
media/libjpeg/simd/jidctflt-3dn.asm
media/libjpeg/simd/jidctflt-sse.asm
media/libjpeg/simd/jidctflt-sse2-64.asm
media/libjpeg/simd/jidctflt-sse2.asm
media/libjpeg/simd/jidctfst-altivec.c
media/libjpeg/simd/jidctfst-mmx.asm
media/libjpeg/simd/jidctfst-sse2-64.asm
media/libjpeg/simd/jidctfst-sse2.asm
media/libjpeg/simd/jidctint-altivec.c
media/libjpeg/simd/jidctint-mmx.asm
media/libjpeg/simd/jidctint-sse2-64.asm
media/libjpeg/simd/jidctint-sse2.asm
media/libjpeg/simd/jidctred-mmx.asm
media/libjpeg/simd/jidctred-sse2-64.asm
media/libjpeg/simd/jidctred-sse2.asm
media/libjpeg/simd/jpeg_nbits_table.inc
media/libjpeg/simd/jquant-3dn.asm
media/libjpeg/simd/jquant-mmx.asm
media/libjpeg/simd/jquant-sse.asm
media/libjpeg/simd/jquantf-sse2-64.asm
media/libjpeg/simd/jquantf-sse2.asm
media/libjpeg/simd/jquanti-altivec.c
media/libjpeg/simd/jquanti-sse2-64.asm
media/libjpeg/simd/jquanti-sse2.asm
media/libjpeg/simd/jsimd_altivec.h
media/libjpeg/simd/jsimd_arm.c
media/libjpeg/simd/jsimd_arm64.c
media/libjpeg/simd/jsimd_arm64_neon.S
media/libjpeg/simd/jsimd_arm_neon.S
media/libjpeg/simd/jsimd_i386.c
media/libjpeg/simd/jsimd_mips.c
media/libjpeg/simd/jsimd_mips_dspr2.S
media/libjpeg/simd/jsimd_mips_dspr2_asm.h
media/libjpeg/simd/jsimd_powerpc.c
media/libjpeg/simd/jsimd_x86_64.c
media/libjpeg/simd/jsimdcfg.inc
media/libjpeg/simd/jsimdcpu.asm
media/libjpeg/simd/jsimdext.inc
toolkit/components/antitracking/test/browser/browser_imageCache11-1.js
toolkit/components/antitracking/test/browser/browser_imageCache11-2.js
toolkit/components/antitracking/test/browser/browser_imageCache11.js
toolkit/components/antitracking/test/browser/browser_imageCache14-1.js
toolkit/components/antitracking/test/browser/browser_imageCache14-2.js
toolkit/components/antitracking/test/browser/browser_imageCache14.js
toolkit/components/antitracking/test/browser/browser_imageCache2-1.js
toolkit/components/antitracking/test/browser/browser_imageCache2-2.js
toolkit/components/antitracking/test/browser/browser_imageCache2.js
toolkit/components/antitracking/test/browser/browser_imageCache5-1.js
toolkit/components/antitracking/test/browser/browser_imageCache5-2.js
toolkit/components/antitracking/test/browser/browser_imageCache5.js
toolkit/components/antitracking/test/browser/browser_imageCache6-1.js
toolkit/components/antitracking/test/browser/browser_imageCache6-2.js
toolkit/components/antitracking/test/browser/browser_imageCache6.js
toolkit/components/antitracking/test/browser/browser_imageCache9.js
--- a/accessible/android/AccessibleWrap.cpp
+++ b/accessible/android/AccessibleWrap.cpp
@@ -1,23 +1,509 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "AccessibleWrap.h"
 
+#include "Accessible-inl.h"
+#include "DocAccessibleWrap.h"
+#include "IDSet.h"
+#include "JavaBuiltins.h"
+#include "SessionAccessibility.h"
+#include "nsAccessibilityService.h"
+#include "nsIPersistentProperties2.h"
+#include "nsIStringBundle.h"
+#include "nsAccUtils.h"
+
+#define ROLE_STRINGS_URL "chrome://global/locale/AccessFu.properties"
+
 using namespace mozilla::a11y;
 
+// IDs should be a positive 32bit integer.
+IDSet sIDSet(31UL);
+
 //-----------------------------------------------------
 // construction
 //-----------------------------------------------------
 AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc)
   : Accessible(aContent, aDoc)
 {
+  if (aDoc) {
+    mID = AcquireID();
+    DocAccessibleWrap* doc = static_cast<DocAccessibleWrap*>(aDoc);
+    doc->AddID(mID, this);
+  }
 }
 
 //-----------------------------------------------------
 // destruction
 //-----------------------------------------------------
-AccessibleWrap::~AccessibleWrap()
+AccessibleWrap::~AccessibleWrap() {}
+
+nsresult
+AccessibleWrap::HandleAccEvent(AccEvent* aEvent)
+{
+  nsresult rv = Accessible::HandleAccEvent(aEvent);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (IPCAccessibilityActive()) {
+    return NS_OK;
+  }
+
+  auto accessible = static_cast<AccessibleWrap*>(aEvent->GetAccessible());
+  NS_ENSURE_TRUE(accessible, NS_ERROR_FAILURE);
+
+  // The accessible can become defunct if we have an xpcom event listener
+  // which decides it would be fun to change the DOM and flush layout.
+  if (accessible->IsDefunct() || !accessible->IsBoundToParent()) {
+    return NS_OK;
+  }
+
+  if (DocAccessible* doc = accessible->Document()) {
+    if (!nsCoreUtils::IsContentDocument(doc->DocumentNode())) {
+      return NS_OK;
+    }
+  }
+
+  SessionAccessibility* sessionAcc =
+    SessionAccessibility::GetInstanceFor(accessible);
+  if (!sessionAcc) {
+    return NS_OK;
+  }
+
+  switch (aEvent->GetEventType()) {
+    case nsIAccessibleEvent::EVENT_FOCUS:
+      sessionAcc->SendFocusEvent(accessible);
+      break;
+    case nsIAccessibleEvent::EVENT_VIRTUALCURSOR_CHANGED: {
+      AccVCChangeEvent* vcEvent = downcast_accEvent(aEvent);
+      auto newPosition = static_cast<AccessibleWrap*>(vcEvent->NewAccessible());
+      auto oldPosition = static_cast<AccessibleWrap*>(vcEvent->OldAccessible());
+
+      if (sessionAcc && newPosition) {
+        if (oldPosition != newPosition) {
+          if (vcEvent->Reason() == nsIAccessiblePivot::REASON_POINT) {
+            sessionAcc->SendHoverEnterEvent(newPosition);
+          } else {
+            sessionAcc->SendAccessibilityFocusedEvent(newPosition);
+          }
+        }
+
+        if (vcEvent->BoundaryType() != nsIAccessiblePivot::NO_BOUNDARY) {
+          sessionAcc->SendTextTraversedEvent(
+            newPosition, vcEvent->NewStartOffset(), vcEvent->NewEndOffset());
+        }
+      }
+      break;
+    }
+    case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
+      AccCaretMoveEvent* event = downcast_accEvent(aEvent);
+      sessionAcc->SendTextSelectionChangedEvent(accessible,
+                                                event->GetCaretOffset());
+      break;
+    }
+    case nsIAccessibleEvent::EVENT_TEXT_INSERTED:
+    case nsIAccessibleEvent::EVENT_TEXT_REMOVED: {
+      AccTextChangeEvent* event = downcast_accEvent(aEvent);
+      sessionAcc->SendTextChangedEvent(accessible,
+                                       event->ModifiedText(),
+                                       event->GetStartOffset(),
+                                       event->GetLength(),
+                                       event->IsTextInserted(),
+                                       event->IsFromUserInput());
+      break;
+    }
+    case nsIAccessibleEvent::EVENT_STATE_CHANGE: {
+      AccStateChangeEvent* event = downcast_accEvent(aEvent);
+      auto state = event->GetState();
+      if (state & states::CHECKED) {
+        sessionAcc->SendClickedEvent(accessible);
+      }
+
+      if (state & states::SELECTED) {
+        sessionAcc->SendSelectedEvent(accessible);
+      }
+
+      if (state & states::BUSY) {
+        sessionAcc->SendWindowStateChangedEvent(accessible);
+      }
+      break;
+    }
+    case nsIAccessibleEvent::EVENT_SCROLLING: {
+      AccScrollingEvent* event = downcast_accEvent(aEvent);
+      sessionAcc->SendScrollingEvent(accessible,
+                                     event->ScrollX(),
+                                     event->ScrollY(),
+                                     event->MaxScrollX(),
+                                     event->MaxScrollY());
+      break;
+    }
+    case nsIAccessibleEvent::EVENT_SHOW:
+    case nsIAccessibleEvent::EVENT_HIDE: {
+      AccMutationEvent* event = downcast_accEvent(aEvent);
+      auto parent = static_cast<AccessibleWrap*>(event->Parent());
+      sessionAcc->SendWindowContentChangedEvent(parent);
+      break;
+    }
+    default:
+      break;
+  }
+
+  return NS_OK;
+}
+
+void
+AccessibleWrap::Shutdown()
+{
+  if (mDoc) {
+    if (mID > 0) {
+      if (auto doc = static_cast<DocAccessibleWrap*>(mDoc.get())) {
+        doc->RemoveID(mID);
+      }
+      ReleaseID(mID);
+      mID = 0;
+    }
+  }
+
+  Accessible::Shutdown();
+}
+
+int32_t
+AccessibleWrap::AcquireID()
+{
+  return sIDSet.GetID();
+}
+
+void
+AccessibleWrap::ReleaseID(int32_t aID)
+{
+  sIDSet.ReleaseID(aID);
+}
+
+void
+AccessibleWrap::SetTextContents(const nsAString& aText) {
+  if (IsHyperText()) {
+    AsHyperText()->ReplaceText(aText);
+  }
+}
+
+void
+AccessibleWrap::GetTextContents(nsAString& aText) {
+  // For now it is a simple wrapper for getting entire range of TextSubstring.
+  // In the future this may be smarter and retrieve a flattened string.
+  if (IsHyperText()) {
+    AsHyperText()->TextSubstring(0, -1, aText);
+  }
+}
+
+bool
+AccessibleWrap::GetSelectionBounds(int32_t* aStartOffset, int32_t* aEndOffset) {
+  if (IsHyperText()) {
+    return AsHyperText()->SelectionBoundsAt(0, aStartOffset, aEndOffset);
+  }
+
+  return false;
+}
+
+mozilla::java::GeckoBundle::LocalRef
+AccessibleWrap::CreateBundle(int32_t aParentID,
+                             role aRole,
+                             uint64_t aState,
+                             const nsString& aName,
+                             const nsString& aTextValue,
+                             const nsString& aDOMNodeID,
+                             const nsIntRect& aBounds,
+                             double aCurVal,
+                             double aMinVal,
+                             double aMaxVal,
+                             double aStep,
+                             nsIPersistentProperties* aAttributes,
+                             const nsTArray<int32_t>& aChildren) const
 {
+  GECKOBUNDLE_START(nodeInfo);
+  GECKOBUNDLE_PUT(nodeInfo, "id", java::sdk::Integer::ValueOf(VirtualViewID()));
+  GECKOBUNDLE_PUT(nodeInfo, "parentId", java::sdk::Integer::ValueOf(aParentID));
+  uint64_t flags = GetFlags(aRole, aState);
+  GECKOBUNDLE_PUT(nodeInfo, "flags", java::sdk::Integer::ValueOf(flags));
+
+  nsAutoString geckoRole;
+  nsAutoString roleDescription;
+  nsAutoString className;
+  GetAndroidRoleAndClass(aRole, geckoRole, roleDescription, className);
+  if (VirtualViewID() == kNoID) {
+    className.AssignLiteral("android.webkit.WebView");
+    roleDescription.AssignLiteral("");
+  }
+  GECKOBUNDLE_PUT(
+    nodeInfo, "roleDescription", jni::StringParam(roleDescription));
+  GECKOBUNDLE_PUT(nodeInfo, "geckoRole", jni::StringParam(geckoRole));
+  GECKOBUNDLE_PUT(nodeInfo, "className", jni::StringParam(className));
+
+  if (!aTextValue.IsEmpty() &&
+      (flags & java::SessionAccessibility::FLAG_EDITABLE)) {
+    GECKOBUNDLE_PUT(nodeInfo, "hint", jni::StringParam(aName));
+    GECKOBUNDLE_PUT(nodeInfo, "text", jni::StringParam(aTextValue));
+  } else {
+    GECKOBUNDLE_PUT(nodeInfo, "text", jni::StringParam(aName));
+  }
+
+  if (!aDOMNodeID.IsEmpty()) {
+    GECKOBUNDLE_PUT(
+      nodeInfo, "viewIdResourceName", jni::StringParam(aDOMNodeID));
+  }
+
+  const int32_t data[4] = {
+    aBounds.x, aBounds.y, aBounds.x + aBounds.width, aBounds.y + aBounds.height
+  };
+  GECKOBUNDLE_PUT(nodeInfo, "bounds", jni::IntArray::New(data, 4));
+
+  if (HasNumericValue()) {
+    GECKOBUNDLE_START(rangeInfo);
+    if (aMaxVal == 1 && aMinVal == 0) {
+      GECKOBUNDLE_PUT(
+        rangeInfo, "type", java::sdk::Integer::ValueOf(2)); // percent
+    } else if (std::round(aStep) != aStep) {
+      GECKOBUNDLE_PUT(
+        rangeInfo, "type", java::sdk::Integer::ValueOf(1)); // float
+    } else {
+      GECKOBUNDLE_PUT(
+        rangeInfo, "type", java::sdk::Integer::ValueOf(0)); // integer
+    }
+
+    if (!IsNaN(aCurVal)) {
+      GECKOBUNDLE_PUT(rangeInfo, "current", java::sdk::Double::New(aCurVal));
+    }
+    if (!IsNaN(aMinVal)) {
+      GECKOBUNDLE_PUT(rangeInfo, "min", java::sdk::Double::New(aMinVal));
+    }
+    if (!IsNaN(aMaxVal)) {
+      GECKOBUNDLE_PUT(rangeInfo, "max", java::sdk::Double::New(aMaxVal));
+    }
+
+    GECKOBUNDLE_FINISH(rangeInfo);
+    GECKOBUNDLE_PUT(nodeInfo, "rangeInfo", rangeInfo);
+  }
+
+  nsString inputType;
+  nsAccUtils::GetAccAttr(aAttributes, nsGkAtoms::textInputType, inputType);
+  if (!inputType.IsEmpty()) {
+    GECKOBUNDLE_PUT(nodeInfo, "inputType", jni::StringParam(inputType));
+  }
+
+  nsString posinset;
+  nsresult rv = aAttributes->GetStringProperty(NS_LITERAL_CSTRING("posinset"), posinset);
+  if (NS_SUCCEEDED(rv)) {
+    int32_t rowIndex;
+    if (sscanf(NS_ConvertUTF16toUTF8(posinset).get(), "%d", &rowIndex) > 0) {
+      GECKOBUNDLE_START(collectionItemInfo);
+      GECKOBUNDLE_PUT(
+        collectionItemInfo, "rowIndex", java::sdk::Integer::ValueOf(rowIndex));
+      GECKOBUNDLE_PUT(
+        collectionItemInfo, "columnIndex", java::sdk::Integer::ValueOf(0));
+      GECKOBUNDLE_PUT(
+        collectionItemInfo, "rowSpan", java::sdk::Integer::ValueOf(1));
+      GECKOBUNDLE_PUT(
+        collectionItemInfo, "columnSpan", java::sdk::Integer::ValueOf(1));
+      GECKOBUNDLE_FINISH(collectionItemInfo);
+
+      GECKOBUNDLE_PUT(nodeInfo, "collectionItemInfo", collectionItemInfo);
+    }
+  }
+
+  nsString colSize;
+  rv = aAttributes->GetStringProperty(NS_LITERAL_CSTRING("child-item-count"),
+                                      colSize);
+  if (NS_SUCCEEDED(rv)) {
+    int32_t rowCount;
+    if (sscanf(NS_ConvertUTF16toUTF8(colSize).get(), "%d", &rowCount) > 0) {
+      GECKOBUNDLE_START(collectionInfo);
+      GECKOBUNDLE_PUT(
+        collectionInfo, "rowCount", java::sdk::Integer::ValueOf(rowCount));
+      GECKOBUNDLE_PUT(
+        collectionInfo, "columnCount", java::sdk::Integer::ValueOf(1));
+
+      nsString unused;
+      rv = aAttributes->GetStringProperty(NS_LITERAL_CSTRING("hierarchical"),
+                                          unused);
+      if (NS_SUCCEEDED(rv)) {
+        GECKOBUNDLE_PUT(
+          collectionInfo, "isHierarchical", java::sdk::Boolean::TRUE());
+      }
+
+      if (IsSelect()) {
+        int32_t selectionMode = (aState & states::MULTISELECTABLE) ? 2 : 1;
+        GECKOBUNDLE_PUT(collectionInfo,
+                        "selectionMode",
+                        java::sdk::Integer::ValueOf(selectionMode));
+      }
+      GECKOBUNDLE_FINISH(collectionInfo);
+
+      GECKOBUNDLE_PUT(nodeInfo, "collectionInfo", collectionInfo);
+    }
+  }
+
+  GECKOBUNDLE_PUT(nodeInfo,
+                  "children",
+                  jni::IntArray::New(aChildren.Elements(), aChildren.Length()));
+  GECKOBUNDLE_FINISH(nodeInfo);
+
+  return nodeInfo;
 }
+
+uint64_t
+AccessibleWrap::GetFlags(role aRole, uint64_t aState)
+{
+  uint64_t flags = 0;
+  if (aState & states::CHECKABLE) {
+    flags |= java::SessionAccessibility::FLAG_CHECKABLE;
+  }
+
+  if (aState & states::CHECKED) {
+    flags |= java::SessionAccessibility::FLAG_CHECKED;
+  }
+
+  if (aState & states::INVALID) {
+    flags |= java::SessionAccessibility::FLAG_CONTENT_INVALID;
+  }
+
+  if (aState & states::EDITABLE) {
+    flags |= java::SessionAccessibility::FLAG_EDITABLE;
+  }
+
+  if (aState & states::SENSITIVE) {
+    flags |= java::SessionAccessibility::FLAG_CLICKABLE;
+  }
+
+  if (aState & states::ENABLED) {
+    flags |= java::SessionAccessibility::FLAG_ENABLED;
+  }
+
+  if (aState & states::FOCUSABLE) {
+    flags |= java::SessionAccessibility::FLAG_FOCUSABLE;
+  }
+
+  if (aState & states::FOCUSED) {
+    flags |= java::SessionAccessibility::FLAG_FOCUSED;
+  }
+
+  if (aState & states::MULTI_LINE) {
+    flags |= java::SessionAccessibility::FLAG_MULTI_LINE;
+  }
+
+  if (aState & states::SELECTABLE) {
+    flags |= java::SessionAccessibility::FLAG_SELECTABLE;
+  }
+
+  if (aState & states::SELECTED) {
+    flags |= java::SessionAccessibility::FLAG_SELECTED;
+  }
+
+  if ((aState & (states::INVISIBLE | states::OFFSCREEN)) == 0) {
+    flags |= java::SessionAccessibility::FLAG_VISIBLE_TO_USER;
+  }
+
+  if (aRole == roles::PASSWORD_TEXT) {
+    flags |= java::SessionAccessibility::FLAG_PASSWORD;
+  }
+
+  return flags;
+}
+
+void
+AccessibleWrap::GetAndroidRoleAndClass(role aRole,
+                                       nsAString& aGeckoRole,
+                                       nsAString& aRoleDescription,
+                                       nsAString& aClassStr)
+{
+  nsresult rv = NS_OK;
+
+  nsCOMPtr<nsIStringBundleService> sbs =
+    do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv);
+  if (NS_FAILED(rv)) {
+    NS_WARNING("Failed to get string bundle service");
+    return;
+  }
+
+  nsCOMPtr<nsIStringBundle> bundle;
+  rv = sbs->CreateBundle(ROLE_STRINGS_URL, getter_AddRefs(bundle));
+  if (NS_FAILED(rv)) {
+    NS_WARNING("Failed to get string bundle");
+    return;
+  }
+
+#define ROLE(geckoRole,                                                        \
+             stringRole,                                                       \
+             atkRole,                                                          \
+             macRole,                                                          \
+             msaaRole,                                                         \
+             ia2Role,                                                          \
+             androidClass,                                                     \
+             nameRule)                                                         \
+  case roles::geckoRole:                                                       \
+    rv = bundle->GetStringFromName(stringRole, aRoleDescription);              \
+    if (NS_FAILED(rv))                                                         \
+      aRoleDescription.AssignLiteral("");                                      \
+    aGeckoRole.AssignLiteral(stringRole);                                      \
+    aClassStr.AssignLiteral(androidClass);                                     \
+    break;
+
+  switch (aRole) {
+#include "RoleMap.h"
+    default:
+      aRoleDescription.AssignLiteral("");
+      aGeckoRole.AssignLiteral("nothing");
+      aClassStr.AssignLiteral("android.view.View");
+      return;
+  }
+
+#undef ROLE
+}
+
+void
+AccessibleWrap::DOMNodeID(nsString& aDOMNodeID)
+{
+  if (mContent) {
+    nsAtom* id = mContent->GetID();
+    if (id) {
+      id->ToString(aDOMNodeID);
+    }
+  }
+}
+
+mozilla::java::GeckoBundle::LocalRef
+AccessibleWrap::ToBundle()
+{
+  AccessibleWrap* parent = static_cast<AccessibleWrap*>(Parent());
+
+  nsAutoString name;
+  Name(name);
+
+  nsAutoString value;
+  Value(value);
+
+  nsAutoString viewIdResourceName;
+  DOMNodeID(viewIdResourceName);
+
+  nsCOMPtr<nsIPersistentProperties> attributes = Attributes();
+
+  auto childCount = ChildCount();
+  nsTArray<int32_t> children(childCount);
+  for (uint32_t i = 0; i < childCount; i++) {
+    auto child = static_cast<AccessibleWrap*>(GetChildAt(i));
+    children.AppendElement(child->VirtualViewID());
+  }
+
+  return CreateBundle(parent ? parent->VirtualViewID() : 0,
+                      Role(),
+                      State(),
+                      name,
+                      value,
+                      viewIdResourceName,
+                      Bounds(),
+                      CurValue(),
+                      MinValue(),
+                      MaxValue(),
+                      Step(),
+                      attributes,
+                      children);
+}
--- a/accessible/android/AccessibleWrap.h
+++ b/accessible/android/AccessibleWrap.h
@@ -1,25 +1,80 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_a11y_AccessibleWrap_h_
 #define mozilla_a11y_AccessibleWrap_h_
 
+#include "Accessible.h"
+#include "GeneratedJNIWrappers.h"
+#include "mozilla/a11y/ProxyAccessible.h"
 #include "nsCOMPtr.h"
-#include "Accessible.h"
 
 namespace mozilla {
 namespace a11y {
 
 class AccessibleWrap : public Accessible
 {
-public: // construction, destruction
+public:
   AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc);
   virtual ~AccessibleWrap();
+
+  virtual nsresult HandleAccEvent(AccEvent* aEvent) override;
+  virtual void Shutdown() override;
+
+  int32_t VirtualViewID() const { return mID; }
+
+  virtual void SetTextContents(const nsAString& aText);
+
+  virtual void GetTextContents(nsAString& aText);
+
+  virtual bool GetSelectionBounds(int32_t* aStartOffset, int32_t* aEndOffset);
+
+  virtual mozilla::java::GeckoBundle::LocalRef ToBundle();
+
+  static const int32_t kNoID = -1;
+
+protected:
+  mozilla::java::GeckoBundle::LocalRef CreateBundle(
+    int32_t aParentID,
+    role aRole,
+    uint64_t aState,
+    const nsString& aName,
+    const nsString& aTextValue,
+    const nsString& aDOMNodeID,
+    const nsIntRect& aBounds,
+    double aCurVal,
+    double aMinVal,
+    double aMaxVal,
+    double aStep,
+    nsIPersistentProperties* aAttributes,
+    const nsTArray<int32_t>& aChildren) const;
+
+  // IDs should be a positive 32bit integer.
+  static int32_t AcquireID();
+  static void ReleaseID(int32_t aID);
+
+  int32_t mID;
+
+private:
+  void DOMNodeID(nsString& aDOMNodeID);
+
+  static void GetAndroidRoleAndClass(role aRole,
+                                     nsAString& aGeckoRole,
+                                     nsAString& aRoleDescription,
+                                     nsAString& aClassStr);
+
+  static uint64_t GetFlags(role aRole, uint64_t aState);
 };
 
+static inline AccessibleWrap*
+WrapperFor(const ProxyAccessible* aProxy)
+{
+  return reinterpret_cast<AccessibleWrap*>(aProxy->GetWrapper());
+}
+
 } // namespace a11y
 } // namespace mozilla
 
 #endif
new file mode 100644
--- /dev/null
+++ b/accessible/android/DocAccessibleWrap.cpp
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DocAccessibleWrap.h"
+#include "nsIDocShell.h"
+
+using namespace mozilla::a11y;
+
+////////////////////////////////////////////////////////////////////////////////
+// DocAccessibleWrap
+////////////////////////////////////////////////////////////////////////////////
+
+DocAccessibleWrap::DocAccessibleWrap(nsIDocument* aDocument,
+                                     nsIPresShell* aPresShell)
+  : DocAccessible(aDocument, aPresShell)
+{
+  nsCOMPtr<nsIDocShellTreeItem> treeItem(aDocument->GetDocShell());
+
+  nsCOMPtr<nsIDocShellTreeItem> parentTreeItem;
+  treeItem->GetParent(getter_AddRefs(parentTreeItem));
+
+  if (treeItem->ItemType() == nsIDocShellTreeItem::typeContent &&
+      (!parentTreeItem ||
+       parentTreeItem->ItemType() == nsIDocShellTreeItem::typeChrome)) {
+    // The top-level content document gets this special ID.
+    mID = kNoID;
+  } else {
+    mID = AcquireID();
+  }
+}
+
+DocAccessibleWrap::~DocAccessibleWrap() {}
+
+AccessibleWrap*
+DocAccessibleWrap::GetAccessibleByID(int32_t aID) const
+{
+  if (AccessibleWrap* acc = mIDToAccessibleMap.Get(aID)) {
+    return acc;
+  }
+
+  // If the ID is not in the hash table, check the IDs of the child docs.
+  for (uint32_t i = 0; i < ChildDocumentCount(); i++) {
+    auto childDoc = reinterpret_cast<AccessibleWrap*>(GetChildDocumentAt(i));
+    if (childDoc->VirtualViewID() == aID) {
+      return childDoc;
+    }
+  }
+
+  return nullptr;
+}
--- a/accessible/android/DocAccessibleWrap.h
+++ b/accessible/android/DocAccessibleWrap.h
@@ -6,14 +6,35 @@
 #ifndef mozilla_a11y_DocAccessibleWrap_h__
 #define mozilla_a11y_DocAccessibleWrap_h__
 
 #include "DocAccessible.h"
 
 namespace mozilla {
 namespace a11y {
 
-typedef DocAccessible DocAccessibleWrap;
+class DocAccessibleWrap : public DocAccessible
+{
+public:
+  DocAccessibleWrap(nsIDocument* aDocument, nsIPresShell* aPresShell);
+  virtual ~DocAccessibleWrap();
+
+  /**
+   * Manage the mapping from id to Accessible.
+   */
+  void AddID(uint32_t aID, AccessibleWrap* aAcc)
+  {
+    mIDToAccessibleMap.Put(aID, aAcc);
+  }
+  void RemoveID(uint32_t aID) { mIDToAccessibleMap.Remove(aID); }
+  AccessibleWrap* GetAccessibleByID(int32_t aID) const;
+
+protected:
+  /*
+   * This provides a mapping from 32 bit id to accessible objects.
+   */
+  nsDataHashtable<nsUint32HashKey, AccessibleWrap*> mIDToAccessibleMap;
+};
 
 } // namespace a11y
 } // namespace mozilla
 
 #endif
--- a/accessible/android/Platform.cpp
+++ b/accessible/android/Platform.cpp
@@ -1,89 +1,200 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "Platform.h"
+#include "ProxyAccessibleWrap.h"
+#include "SessionAccessibility.h"
+#include "mozilla/a11y/ProxyAccessible.h"
+#include "nsIAccessibleEvent.h"
+#include "nsIAccessiblePivot.h"
 
 using namespace mozilla;
 using namespace mozilla::a11y;
 
 void
 a11y::PlatformInit()
 {
 }
 
 void
 a11y::PlatformShutdown()
 {
 }
 
 void
-a11y::ProxyCreated(ProxyAccessible*, uint32_t)
+a11y::ProxyCreated(ProxyAccessible* aProxy, uint32_t aInterfaces)
 {
+  AccessibleWrap* wrapper = nullptr;
+  if (aProxy->IsDoc()) {
+    wrapper = new DocProxyAccessibleWrap(aProxy->AsDoc());
+  } else {
+    wrapper = new ProxyAccessibleWrap(aProxy);
+  }
+
+  wrapper->AddRef();
+  aProxy->SetWrapper(reinterpret_cast<uintptr_t>(wrapper));
+}
+
+void
+a11y::ProxyDestroyed(ProxyAccessible* aProxy)
+{
+  AccessibleWrap* wrapper =
+    reinterpret_cast<AccessibleWrap*>(aProxy->GetWrapper());
+
+  // If aProxy is a document that was created, but
+  // RecvPDocAccessibleConstructor failed then aProxy->GetWrapper() will be
+  // null.
+  if (!wrapper) {
+    return;
+  }
+
+  wrapper->Shutdown();
+  aProxy->SetWrapper(0);
+  wrapper->Release();
 }
 
 void
-a11y::ProxyDestroyed(ProxyAccessible*)
+a11y::ProxyEvent(ProxyAccessible* aTarget, uint32_t aEventType)
 {
+  SessionAccessibility* sessionAcc =
+    SessionAccessibility::GetInstanceFor(aTarget);
+  if (!sessionAcc) {
+    return;
+  }
+
+  switch (aEventType) {
+    case nsIAccessibleEvent::EVENT_FOCUS:
+      sessionAcc->SendFocusEvent(WrapperFor(aTarget));
+      break;
+  }
 }
 
 void
-a11y::ProxyEvent(ProxyAccessible*, uint32_t)
+a11y::ProxyStateChangeEvent(ProxyAccessible* aTarget,
+                            uint64_t aState,
+                            bool aEnabled)
 {
-}
+  SessionAccessibility* sessionAcc =
+    SessionAccessibility::GetInstanceFor(aTarget);
+
+  if (!sessionAcc) {
+    return;
+  }
 
-void
-a11y::ProxyStateChangeEvent(ProxyAccessible*, uint64_t, bool)
-{
+  if (aState & states::CHECKED) {
+    sessionAcc->SendClickedEvent(WrapperFor(aTarget));
+  }
+
+  if (aState & states::SELECTED) {
+    sessionAcc->SendSelectedEvent(WrapperFor(aTarget));
+  }
+
+  if (aState & states::BUSY) {
+    sessionAcc->SendWindowStateChangedEvent(WrapperFor(aTarget));
+  }
 }
 
 void
 a11y::ProxyCaretMoveEvent(ProxyAccessible* aTarget, int32_t aOffset)
 {
+  SessionAccessibility* sessionAcc =
+    SessionAccessibility::GetInstanceFor(aTarget);
+
+  if (sessionAcc) {
+    sessionAcc->SendTextSelectionChangedEvent(WrapperFor(aTarget), aOffset);
+  }
 }
 
 void
-a11y::ProxyTextChangeEvent(ProxyAccessible*,
-                           const nsString&,
-                           int32_t,
-                           uint32_t,
-                           bool,
-                           bool)
+a11y::ProxyTextChangeEvent(ProxyAccessible* aTarget,
+                           const nsString& aStr,
+                           int32_t aStart,
+                           uint32_t aLen,
+                           bool aIsInsert,
+                           bool aFromUser)
 {
+  SessionAccessibility* sessionAcc =
+    SessionAccessibility::GetInstanceFor(aTarget);
+
+  if (sessionAcc) {
+    sessionAcc->SendTextChangedEvent(
+      WrapperFor(aTarget), aStr, aStart, aLen, aIsInsert, aFromUser);
+  }
 }
 
 void
-a11y::ProxyShowHideEvent(ProxyAccessible*, ProxyAccessible*, bool, bool)
+a11y::ProxyShowHideEvent(ProxyAccessible* aTarget,
+                         ProxyAccessible* aParent,
+                         bool aInsert,
+                         bool aFromUser)
 {
+  SessionAccessibility* sessionAcc =
+    SessionAccessibility::GetInstanceFor(aTarget);
+  if (sessionAcc) {
+    sessionAcc->SendWindowContentChangedEvent(WrapperFor(aParent));
+  }
 }
 
 void
 a11y::ProxySelectionEvent(ProxyAccessible*, ProxyAccessible*, uint32_t)
 {
 }
 
 void
-a11y::ProxyVirtualCursorChangeEvent(ProxyAccessible*,
-                                    ProxyAccessible*,
-                                    int32_t,
-                                    int32_t,
-                                    ProxyAccessible*,
-                                    int32_t,
-                                    int32_t,
-                                    int16_t,
-                                    int16_t,
-                                    bool)
+a11y::ProxyVirtualCursorChangeEvent(ProxyAccessible* aTarget,
+                                    ProxyAccessible* aOldPosition,
+                                    int32_t aOldStartOffset,
+                                    int32_t aOldEndOffset,
+                                    ProxyAccessible* aNewPosition,
+                                    int32_t aNewStartOffset,
+                                    int32_t aNewEndOffset,
+                                    int16_t aReason,
+                                    int16_t aBoundaryType,
+                                    bool aFromUser)
 {
+  if (!aNewPosition) {
+    return;
+  }
+
+  SessionAccessibility* sessionAcc =
+    SessionAccessibility::GetInstanceFor(aTarget);
+
+  if (!sessionAcc) {
+    return;
+  }
+
+  if (aOldPosition != aNewPosition) {
+    if (aReason == nsIAccessiblePivot::REASON_POINT) {
+      sessionAcc->SendHoverEnterEvent(WrapperFor(aNewPosition));
+    } else {
+      sessionAcc->SendAccessibilityFocusedEvent(WrapperFor(aNewPosition));
+    }
+  }
+
+  if (aBoundaryType != nsIAccessiblePivot::NO_BOUNDARY) {
+    sessionAcc->SendTextTraversedEvent(
+      WrapperFor(aNewPosition), aNewStartOffset, aNewEndOffset);
+  }
 }
 
 void
-a11y::ProxyScrollingEvent(ProxyAccessible*,
-                          uint32_t,
-                          uint32_t,
-                          uint32_t,
-                          uint32_t,
-                          uint32_t)
+a11y::ProxyScrollingEvent(ProxyAccessible* aTarget,
+                          uint32_t aEventType,
+                          uint32_t aScrollX,
+                          uint32_t aScrollY,
+                          uint32_t aMaxScrollX,
+                          uint32_t aMaxScrollY)
 {
+  if (aEventType == nsIAccessibleEvent::EVENT_SCROLLING) {
+    SessionAccessibility* sessionAcc =
+      SessionAccessibility::GetInstanceFor(aTarget);
+
+    if (sessionAcc) {
+      sessionAcc->SendScrollingEvent(
+        WrapperFor(aTarget), aScrollX, aScrollY, aMaxScrollX, aMaxScrollY);
+    }
+  }
 }
new file mode 100644
--- /dev/null
+++ b/accessible/android/ProxyAccessibleWrap.cpp
@@ -0,0 +1,147 @@
+/* -*- Mode: c++; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ProxyAccessibleWrap.h"
+#include "nsPersistentProperties.h"
+
+using namespace mozilla::a11y;
+
+ProxyAccessibleWrap::ProxyAccessibleWrap(ProxyAccessible* aProxy)
+  : AccessibleWrap(nullptr, nullptr)
+{
+  mType = eProxyType;
+  mBits.proxy = aProxy;
+
+  if (aProxy->mHasValue) {
+    mStateFlags |= eHasNumericValue;
+  }
+
+  if (aProxy->mIsSelection) {
+    mGenericTypes |= eSelect;
+  }
+
+  if (aProxy->mIsHyperText) {
+    mGenericTypes |= eHyperText;
+  }
+
+  auto doc = reinterpret_cast<DocProxyAccessibleWrap*>(
+    Proxy()->Document()->GetWrapper());
+  if (doc) {
+    mID = AcquireID();
+    doc->AddID(mID, this);
+  }
+}
+
+void
+ProxyAccessibleWrap::Shutdown()
+{
+  auto doc = reinterpret_cast<DocProxyAccessibleWrap*>(
+    Proxy()->Document()->GetWrapper());
+  if (mID && doc) {
+    doc->RemoveID(mID);
+    ReleaseID(mID);
+    mID = 0;
+  }
+
+  mBits.proxy = nullptr;
+  mStateFlags |= eIsDefunct;
+}
+
+// Accessible
+
+already_AddRefed<nsIPersistentProperties>
+ProxyAccessibleWrap::Attributes()
+{
+  RefPtr<nsPersistentProperties> attributes = new nsPersistentProperties();
+  nsAutoString unused;
+  AutoTArray<Attribute, 10> attrs;
+  Proxy()->Attributes(&attrs);
+  for (size_t i = 0; i < attrs.Length(); i++) {
+    attributes->SetStringProperty(
+      attrs.ElementAt(i).Name(), attrs.ElementAt(i).Value(), unused);
+  }
+
+  return attributes.forget();
+}
+
+uint32_t
+ProxyAccessibleWrap::ChildCount() const
+{
+  return Proxy()->ChildrenCount();
+}
+
+void
+ProxyAccessibleWrap::ScrollTo(uint32_t aHow) const
+{
+  Proxy()->ScrollTo(aHow);
+}
+
+// Other
+
+void
+ProxyAccessibleWrap::SetTextContents(const nsAString& aText)
+{
+  Proxy()->ReplaceText(PromiseFlatString(aText));
+}
+
+void
+ProxyAccessibleWrap::GetTextContents(nsAString& aText)
+{
+  nsAutoString text;
+  Proxy()->TextSubstring(0, -1, text);
+  aText.Assign(text);
+}
+
+bool
+ProxyAccessibleWrap::GetSelectionBounds(int32_t* aStartOffset,
+                                        int32_t* aEndOffset)
+{
+  nsAutoString unused;
+  return Proxy()->SelectionBoundsAt(0, unused, aStartOffset, aEndOffset);
+}
+
+mozilla::java::GeckoBundle::LocalRef
+ProxyAccessibleWrap::ToBundle()
+{
+  ProxyAccessible* proxy = Proxy();
+  if (!proxy) {
+    return nullptr;
+  }
+
+  int32_t parentID = proxy->Parent() ?
+    WrapperFor(proxy->Parent())->VirtualViewID() : 0;
+
+  nsAutoString name;
+  proxy->Name(name);
+
+  nsAutoString value;
+  proxy->Value(value);
+
+  nsAutoString viewIdResourceName;
+  proxy->DOMNodeID(viewIdResourceName);
+
+  nsCOMPtr<nsIPersistentProperties> attributes = Attributes();
+
+  auto childCount = proxy->ChildrenCount();
+  nsTArray<int32_t> children(childCount);
+  for (uint32_t i = 0; i < childCount; i++) {
+    auto child = WrapperFor(proxy->ChildAt(i));
+    children.AppendElement(child->VirtualViewID());
+  }
+
+  return CreateBundle(parentID,
+                      proxy->Role(),
+                      proxy->State(),
+                      name,
+                      value,
+                      viewIdResourceName,
+                      proxy->Bounds(),
+                      proxy->CurValue(),
+                      proxy->MinValue(),
+                      proxy->MaxValue(),
+                      proxy->Step(),
+                      attributes,
+                      children);
+}
new file mode 100644
--- /dev/null
+++ b/accessible/android/ProxyAccessibleWrap.h
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. *
+ */
+
+#ifndef MOZILLA_A11Y_ProxyAccessibleWrap_h
+#define MOZILLA_A11Y_ProxyAccessibleWrap_h
+
+#include "AccessibleWrap.h"
+#include "DocAccessibleParent.h"
+
+namespace mozilla {
+namespace a11y {
+
+/**
+ * A wrapper for Accessible proxies. The public methods here should be overriden
+ * from AccessibleWrap or its super classes.
+ * This gives us an abstraction layer so SessionAccessibility doesn't have
+ * to distinguish between a local or remote accessibles.
+ * NOTE: This shouldn't be regarded as a full Accessible implementation.
+ */
+class ProxyAccessibleWrap : public AccessibleWrap
+{
+public:
+  explicit ProxyAccessibleWrap(ProxyAccessible* aProxy);
+
+  virtual void Shutdown() override;
+
+  // Accessible
+
+  virtual already_AddRefed<nsIPersistentProperties> Attributes() override;
+
+  virtual uint32_t ChildCount() const override;
+
+  virtual void ScrollTo(uint32_t aHow) const override;
+
+  // AccessibleWrap
+
+  virtual void SetTextContents(const nsAString& aText) override;
+
+  virtual void GetTextContents(nsAString& aText) override;
+
+  virtual bool GetSelectionBounds(int32_t* aStartOffset, int32_t* aEndOffset) override;
+
+  virtual mozilla::java::GeckoBundle::LocalRef ToBundle() override;
+};
+
+class DocProxyAccessibleWrap : public ProxyAccessibleWrap
+{
+public:
+  explicit DocProxyAccessibleWrap(DocAccessibleParent* aProxy)
+    : ProxyAccessibleWrap(aProxy)
+  {
+    mGenericTypes |= eDocument;
+
+    if (auto parent = ParentDocument()) {
+      mID = AcquireID();
+      parent->AddID(mID, this);
+    } else {
+      // top level
+      mID = kNoID;
+    }
+  }
+
+  virtual void Shutdown() override
+  {
+    if (mID) {
+      auto parent = ParentDocument();
+      if (parent) {
+        MOZ_ASSERT(mID != kNoID, "A non root accessible always has a parent");
+        parent->RemoveID(mID);
+        ReleaseID(mID);
+      }
+    }
+    mID = 0;
+    mBits.proxy = nullptr;
+    mStateFlags |= eIsDefunct;
+  }
+
+  DocProxyAccessibleWrap* ParentDocument()
+  {
+    DocAccessibleParent* proxy = static_cast<DocAccessibleParent*>(Proxy());
+    MOZ_ASSERT(proxy);
+    if (DocAccessibleParent* parent = proxy->ParentDoc()) {
+      return reinterpret_cast<DocProxyAccessibleWrap*>(parent->GetWrapper());
+    }
+
+    return nullptr;
+  }
+
+  DocProxyAccessibleWrap* GetChildDocumentAt(uint32_t aIndex)
+  {
+    auto doc = Proxy()->AsDoc();
+    if (doc && doc->ChildDocCount() > aIndex) {
+      return reinterpret_cast<DocProxyAccessibleWrap*>(
+        doc->ChildDocAt(aIndex)->GetWrapper());
+    }
+
+    return nullptr;
+  }
+
+  void AddID(uint32_t aID, AccessibleWrap* aAcc)
+  {
+    mIDToAccessibleMap.Put(aID, aAcc);
+  }
+  void RemoveID(uint32_t aID) { mIDToAccessibleMap.Remove(aID); }
+  AccessibleWrap* GetAccessibleByID(uint32_t aID) const
+  {
+    return mIDToAccessibleMap.Get(aID);
+  }
+
+private:
+  /*
+   * This provides a mapping from 32 bit id to accessible objects.
+   */
+  nsDataHashtable<nsUint32HashKey, AccessibleWrap*> mIDToAccessibleMap;
+};
+}
+}
+
+#endif
new file mode 100644
--- /dev/null
+++ b/accessible/android/RootAccessibleWrap.cpp
@@ -0,0 +1,92 @@
+/* -*- Mode: c++; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RootAccessibleWrap.h"
+
+#include "AccessibleOrProxy.h"
+#include "DocAccessibleParent.h"
+#include "ProxyAccessibleWrap.h"
+#include "SessionAccessibility.h"
+
+using namespace mozilla::a11y;
+
+RootAccessibleWrap::RootAccessibleWrap(nsIDocument* aDoc,
+                                       nsIPresShell* aPresShell)
+  : RootAccessible(aDoc, aPresShell)
+{
+}
+
+RootAccessibleWrap::~RootAccessibleWrap() {}
+
+AccessibleWrap*
+RootAccessibleWrap::GetContentAccessible()
+{
+  if (ProxyAccessible* proxy = GetPrimaryRemoteTopLevelContentDoc()) {
+    return WrapperFor(proxy);
+  }
+
+  if (ChildDocumentCount()) {
+    return GetChildDocumentAt(0);
+  }
+
+  return nullptr;
+}
+
+AccessibleWrap*
+RootAccessibleWrap::FindAccessibleById(int32_t aID)
+{
+  AccessibleWrap* contentAcc = GetContentAccessible();
+
+  if (!contentAcc) {
+    return nullptr;
+  }
+
+  if (aID == AccessibleWrap::kNoID) {
+    return contentAcc;
+  }
+
+  if (contentAcc->IsProxy()) {
+    return FindAccessibleById(static_cast<DocProxyAccessibleWrap*>(contentAcc),
+                              aID);
+  }
+
+  return FindAccessibleById(
+    static_cast<DocAccessibleWrap*>(contentAcc->AsDoc()), aID);
+}
+
+AccessibleWrap*
+RootAccessibleWrap::FindAccessibleById(DocProxyAccessibleWrap* aDoc,
+                                       int32_t aID)
+{
+  AccessibleWrap* acc = aDoc->GetAccessibleByID(aID);
+  uint32_t index = 0;
+  while (!acc) {
+    auto child =
+      static_cast<DocProxyAccessibleWrap*>(aDoc->GetChildDocumentAt(index++));
+    if (!child) {
+      break;
+    }
+    acc = FindAccessibleById(child, aID);
+  }
+
+  return acc;
+}
+
+AccessibleWrap*
+RootAccessibleWrap::FindAccessibleById(DocAccessibleWrap* aDoc, int32_t aID)
+{
+  AccessibleWrap* acc = aDoc->GetAccessibleByID(aID);
+  uint32_t index = 0;
+  while (!acc) {
+    auto child =
+      static_cast<DocAccessibleWrap*>(aDoc->GetChildDocumentAt(index++));
+    if (!child) {
+      break;
+    }
+    acc = FindAccessibleById(child, aID);
+  }
+
+  return acc;
+}
--- a/accessible/android/RootAccessibleWrap.h
+++ b/accessible/android/RootAccessibleWrap.h
@@ -6,14 +6,32 @@
 #ifndef mozilla_a11y_RootAccessibleWrap_h__
 #define mozilla_a11y_RootAccessibleWrap_h__
 
 #include "RootAccessible.h"
 
 namespace mozilla {
 namespace a11y {
 
-typedef RootAccessible RootAccessibleWrap;
+class DocProxyAccessibleWrap;
+
+class RootAccessibleWrap : public RootAccessible
+{
+public:
+  RootAccessibleWrap(nsIDocument* aDocument, nsIPresShell* aPresShell);
+  virtual ~RootAccessibleWrap();
+
+  AccessibleWrap* GetContentAccessible();
+
+  AccessibleWrap* FindAccessibleById(int32_t aID);
+
+  // Recursively searches for the accessible ID within the document tree.
+  AccessibleWrap* FindAccessibleById(DocAccessibleWrap* aDocument, int32_t aID);
+
+  // Recursively searches for the accessible ID within the proxy document tree.
+  AccessibleWrap* FindAccessibleById(DocProxyAccessibleWrap* aDocument,
+                                     int32_t aID);
+};
 
 } // namespace a11y
 } // namespace mozilla
 
 #endif
--- a/accessible/android/SessionAccessibility.cpp
+++ b/accessible/android/SessionAccessibility.cpp
@@ -1,16 +1,22 @@
 /* -*- Mode: c++; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "SessionAccessibility.h"
 #include "AndroidUiThread.h"
 #include "nsThreadUtils.h"
+#include "AccessibilityEvent.h"
+#include "HyperTextAccessible.h"
+#include "JavaBuiltins.h"
+#include "RootAccessibleWrap.h"
+#include "nsAccessibilityService.h"
+#include "nsViewManager.h"
 
 #ifdef DEBUG
 #include <android/log.h>
 #define AALOG(args...)                                                         \
   __android_log_print(ANDROID_LOG_INFO, "GeckoAccessibilityNative", ##args)
 #else
 #define AALOG(args...)                                                         \
   do {                                                                         \
@@ -18,16 +24,30 @@
 #endif
 
 template<>
 const char nsWindow::NativePtr<mozilla::a11y::SessionAccessibility>::sName[] =
   "SessionAccessibility";
 
 using namespace mozilla::a11y;
 
+class Settings final
+  : public mozilla::java::SessionAccessibility::Settings::Natives<Settings>
+{
+public:
+  static void ToggleNativeAccessibility(bool aEnable)
+  {
+    if (aEnable) {
+      GetOrCreateAccService();
+    } else {
+      MaybeShutdownAccService(nsAccessibilityService::ePlatformAPI);
+    }
+  }
+};
+
 void
 SessionAccessibility::SetAttached(bool aAttached,
                                   already_AddRefed<Runnable> aRunnable)
 {
   if (RefPtr<nsThread> uiThread = GetAndroidUiThread()) {
     uiThread->Dispatch(NS_NewRunnableFunction(
       "SessionAccessibility::Attach",
       [aAttached,
@@ -36,8 +56,264 @@ SessionAccessibility::SetAttached(bool a
        runnable = RefPtr<Runnable>(aRunnable)] {
         sa->SetAttached(aAttached);
         if (runnable) {
           runnable->Run();
         }
       }));
   }
 }
+
+void
+SessionAccessibility::Init()
+{
+  java::SessionAccessibility::NativeProvider::Natives<
+    SessionAccessibility>::Init();
+  Settings::Init();
+}
+
+mozilla::jni::Object::LocalRef
+SessionAccessibility::GetNodeInfo(int32_t aID)
+{
+  java::GeckoBundle::GlobalRef ret = nullptr;
+  RefPtr<SessionAccessibility> self(this);
+  nsAppShell::SyncRunEvent([this, self, aID, &ret] {
+    if (RootAccessibleWrap* rootAcc = GetRoot()) {
+      AccessibleWrap* acc = rootAcc->FindAccessibleById(aID);
+      if (acc) {
+        ret = acc->ToBundle();
+      } else {
+        AALOG("oops, nothing for %d", aID);
+      }
+    }
+  });
+
+  return mozilla::jni::Object::Ref::From(ret);
+}
+
+RootAccessibleWrap*
+SessionAccessibility::GetRoot()
+{
+  if (!mWindow) {
+    return nullptr;
+  }
+
+  return static_cast<RootAccessibleWrap*>(mWindow->GetRootAccessible());
+}
+
+void
+SessionAccessibility::SetText(int32_t aID, jni::String::Param aText)
+{
+  if (RootAccessibleWrap* rootAcc = GetRoot()) {
+    AccessibleWrap* acc = rootAcc->FindAccessibleById(aID);
+    if (!acc) {
+      return;
+    }
+
+    acc->SetTextContents(aText->ToString());
+  }
+}
+
+SessionAccessibility*
+SessionAccessibility::GetInstanceFor(ProxyAccessible* aAccessible)
+{
+  Accessible* outerDoc = aAccessible->OuterDocOfRemoteBrowser();
+  if (!outerDoc) {
+    return nullptr;
+  }
+
+  return GetInstanceFor(outerDoc);
+}
+
+SessionAccessibility*
+SessionAccessibility::GetInstanceFor(Accessible* aAccessible)
+{
+  RootAccessible* rootAcc = aAccessible->RootAccessible();
+  nsIPresShell* shell = rootAcc->PresShell();
+  nsViewManager* vm = shell->GetViewManager();
+  if (!vm) {
+    return nullptr;
+  }
+
+  nsCOMPtr<nsIWidget> rootWidget;
+  vm->GetRootWidget(getter_AddRefs(rootWidget));
+  // `rootWidget` can be one of several types. Here we make sure it is an
+  // android nsWindow that implemented NS_NATIVE_WIDGET to return itself.
+  if (rootWidget &&
+      rootWidget->WindowType() == nsWindowType::eWindowType_toplevel &&
+      rootWidget->GetNativeData(NS_NATIVE_WIDGET) == rootWidget) {
+    return static_cast<nsWindow*>(rootWidget.get())->GetSessionAccessibility();
+  }
+
+  return nullptr;
+}
+
+void
+SessionAccessibility::SendAccessibilityFocusedEvent(AccessibleWrap* aAccessible)
+{
+  mSessionAccessibility->SendEvent(
+    java::sdk::AccessibilityEvent::TYPE_VIEW_ACCESSIBILITY_FOCUSED,
+    aAccessible->VirtualViewID(), nullptr, aAccessible->ToBundle());
+  aAccessible->ScrollTo(nsIAccessibleScrollType::SCROLL_TYPE_ANYWHERE);
+}
+
+void
+SessionAccessibility::SendHoverEnterEvent(AccessibleWrap* aAccessible)
+{
+  mSessionAccessibility->SendEvent(
+    java::sdk::AccessibilityEvent::TYPE_VIEW_HOVER_ENTER,
+    aAccessible->VirtualViewID(), nullptr, aAccessible->ToBundle());
+}
+
+void
+SessionAccessibility::SendFocusEvent(AccessibleWrap* aAccessible)
+{
+  // Suppress focus events from about:blank pages.
+  // This is important for tests.
+  if (aAccessible->IsDoc() && aAccessible->ChildCount() == 0) {
+    return;
+  }
+
+  mSessionAccessibility->SendEvent(
+    java::sdk::AccessibilityEvent::TYPE_VIEW_FOCUSED,
+    aAccessible->VirtualViewID(), nullptr, aAccessible->ToBundle());
+}
+
+void
+SessionAccessibility::SendScrollingEvent(AccessibleWrap* aAccessible,
+                                         int32_t aScrollX,
+                                         int32_t aScrollY,
+                                         int32_t aMaxScrollX,
+                                         int32_t aMaxScrollY)
+{
+  int32_t virtualViewId = aAccessible->VirtualViewID();
+
+  if (virtualViewId != AccessibleWrap::kNoID) {
+    // XXX: Support scrolling in subframes
+    return;
+  }
+
+  GECKOBUNDLE_START(eventInfo);
+  GECKOBUNDLE_PUT(eventInfo, "scrollX", java::sdk::Integer::ValueOf(aScrollX));
+  GECKOBUNDLE_PUT(eventInfo, "scrollY", java::sdk::Integer::ValueOf(aScrollY));
+  GECKOBUNDLE_PUT(eventInfo, "maxScrollX", java::sdk::Integer::ValueOf(aMaxScrollX));
+  GECKOBUNDLE_PUT(eventInfo, "maxScrollY", java::sdk::Integer::ValueOf(aMaxScrollY));
+  GECKOBUNDLE_FINISH(eventInfo);
+
+  mSessionAccessibility->SendEvent(
+    java::sdk::AccessibilityEvent::TYPE_VIEW_SCROLLED, virtualViewId,
+    eventInfo, aAccessible->ToBundle());
+
+  SendWindowContentChangedEvent(aAccessible);
+}
+
+void
+SessionAccessibility::SendWindowContentChangedEvent(AccessibleWrap* aAccessible)
+{
+  mSessionAccessibility->SendEvent(
+    java::sdk::AccessibilityEvent::TYPE_WINDOW_CONTENT_CHANGED,
+    aAccessible->VirtualViewID(), nullptr, aAccessible->ToBundle());
+}
+
+void
+SessionAccessibility::SendWindowStateChangedEvent(AccessibleWrap* aAccessible)
+{
+  // Suppress window state changed events from about:blank pages.
+  // This is important for tests.
+  if (aAccessible->IsDoc() && aAccessible->ChildCount() == 0) {
+    return;
+  }
+
+  mSessionAccessibility->SendEvent(
+    java::sdk::AccessibilityEvent::TYPE_WINDOW_STATE_CHANGED,
+    aAccessible->VirtualViewID(), nullptr, aAccessible->ToBundle());
+}
+
+void
+SessionAccessibility::SendTextSelectionChangedEvent(AccessibleWrap* aAccessible,
+                                                    int32_t aCaretOffset)
+{
+  int32_t fromIndex = aCaretOffset;
+  int32_t startSel = -1;
+  int32_t endSel = -1;
+  if (aAccessible->GetSelectionBounds(&startSel, &endSel)) {
+    fromIndex = startSel == aCaretOffset ? endSel : startSel;
+  }
+
+  GECKOBUNDLE_START(eventInfo);
+  GECKOBUNDLE_PUT(eventInfo, "fromIndex", java::sdk::Integer::ValueOf(fromIndex));
+  GECKOBUNDLE_PUT(eventInfo, "toIndex", java::sdk::Integer::ValueOf(aCaretOffset));
+  GECKOBUNDLE_FINISH(eventInfo);
+
+  mSessionAccessibility->SendEvent(
+    java::sdk::AccessibilityEvent::TYPE_VIEW_TEXT_SELECTION_CHANGED,
+    aAccessible->VirtualViewID(), eventInfo, aAccessible->ToBundle());
+}
+
+void
+SessionAccessibility::SendTextChangedEvent(AccessibleWrap* aAccessible,
+                                           const nsString& aStr,
+                                           int32_t aStart,
+                                           uint32_t aLen,
+                                           bool aIsInsert,
+                                           bool aFromUser)
+{
+  if (!aFromUser) {
+    // Only dispatch text change events from users, for now.
+    return;
+  }
+
+  nsAutoString text;
+  aAccessible->GetTextContents(text);
+  nsAutoString beforeText(text);
+  if (aIsInsert) {
+    beforeText.Cut(aStart, aLen);
+  } else {
+    beforeText.Insert(aStr, aStart);
+  }
+
+  GECKOBUNDLE_START(eventInfo);
+  GECKOBUNDLE_PUT(eventInfo, "text", jni::StringParam(text));
+  GECKOBUNDLE_PUT(eventInfo, "beforeText", jni::StringParam(beforeText));
+  GECKOBUNDLE_PUT(eventInfo, "addedCount", java::sdk::Integer::ValueOf(aIsInsert ? aLen : 0));
+  GECKOBUNDLE_PUT(eventInfo, "removedCount", java::sdk::Integer::ValueOf(aIsInsert ? 0 : aLen));
+  GECKOBUNDLE_FINISH(eventInfo);
+
+  mSessionAccessibility->SendEvent(
+    java::sdk::AccessibilityEvent::TYPE_VIEW_TEXT_CHANGED,
+    aAccessible->VirtualViewID(), eventInfo, aAccessible->ToBundle());
+}
+
+void
+SessionAccessibility::SendTextTraversedEvent(AccessibleWrap* aAccessible,
+                                             int32_t aStartOffset,
+                                             int32_t aEndOffset)
+{
+  nsAutoString text;
+  aAccessible->GetTextContents(text);
+
+  GECKOBUNDLE_START(eventInfo);
+  GECKOBUNDLE_PUT(eventInfo, "text", jni::StringParam(text));
+  GECKOBUNDLE_PUT(eventInfo, "fromIndex", java::sdk::Integer::ValueOf(aStartOffset));
+  GECKOBUNDLE_PUT(eventInfo, "toIndex", java::sdk::Integer::ValueOf(aEndOffset));
+  GECKOBUNDLE_FINISH(eventInfo);
+
+  mSessionAccessibility->SendEvent(
+    java::sdk::AccessibilityEvent::
+      TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
+    aAccessible->VirtualViewID(), eventInfo, aAccessible->ToBundle());
+}
+
+void
+SessionAccessibility::SendClickedEvent(AccessibleWrap* aAccessible)
+{
+  mSessionAccessibility->SendEvent(
+    java::sdk::AccessibilityEvent::TYPE_VIEW_CLICKED,
+    aAccessible->VirtualViewID(), nullptr, aAccessible->ToBundle());
+}
+
+void
+SessionAccessibility::SendSelectedEvent(AccessibleWrap* aAccessible)
+{
+  mSessionAccessibility->SendEvent(
+    java::sdk::AccessibilityEvent::TYPE_VIEW_SELECTED,
+    aAccessible->VirtualViewID(), nullptr, aAccessible->ToBundle());
+}
--- a/accessible/android/SessionAccessibility.h
+++ b/accessible/android/SessionAccessibility.h
@@ -2,21 +2,51 @@
 /* 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/. */
 
 #ifndef mozilla_a11y_SessionAccessibility_h_
 #define mozilla_a11y_SessionAccessibility_h_
 
 #include "GeneratedJNINatives.h"
+#include "GeneratedJNIWrappers.h"
+#include "nsAppShell.h"
+#include "nsThreadUtils.h"
 #include "nsWindow.h"
 
+#define GECKOBUNDLE_START(name)                                                \
+  nsTArray<jni::String::LocalRef> _##name##_keys;                              \
+  nsTArray<jni::Object::LocalRef> _##name##_values;
+
+#define GECKOBUNDLE_PUT(name, key, value)                                      \
+  _##name##_keys.AppendElement(jni::StringParam(NS_LITERAL_STRING(key)));      \
+  _##name##_values.AppendElement(value);
+
+#define GECKOBUNDLE_FINISH(name)                                               \
+  MOZ_ASSERT(_##name##_keys.Length() == _##name##_values.Length());            \
+  auto _##name##_jkeys =                                                       \
+    jni::ObjectArray::New<jni::String>(_##name##_keys.Length());               \
+  auto _##name##_jvalues =                                                     \
+    jni::ObjectArray::New<jni::Object>(_##name##_values.Length());             \
+  for (size_t i = 0;                                                           \
+       i < _##name##_keys.Length() && i < _##name##_values.Length();           \
+       i++) {                                                                  \
+    _##name##_jkeys->SetElement(i, _##name##_keys.ElementAt(i));               \
+    _##name##_jvalues->SetElement(i, _##name##_values.ElementAt(i));           \
+  }                                                                            \
+  auto name =                                                                  \
+    mozilla::java::GeckoBundle::New(_##name##_jkeys, _##name##_jvalues);
+
 namespace mozilla {
 namespace a11y {
 
+class AccessibleWrap;
+class ProxyAccessible;
+class RootAccessibleWrap;
+
 class SessionAccessibility final
   : public java::SessionAccessibility::NativeProvider::Natives<SessionAccessibility>
 {
 public:
   typedef java::SessionAccessibility::NativeProvider::Natives<SessionAccessibility> Base;
 
   SessionAccessibility(
     nsWindow::NativePtr<SessionAccessibility>* aPtr,
@@ -33,26 +63,59 @@ public:
     SetAttached(false, std::move(aDisposer));
   }
 
   const java::SessionAccessibility::NativeProvider::Ref& GetJavaAccessibility()
   {
     return mSessionAccessibility;
   }
 
+  static void Init();
+  static SessionAccessibility* GetInstanceFor(ProxyAccessible* aAccessible);
+  static SessionAccessibility* GetInstanceFor(Accessible* aAccessible);
+
   // Native implementations
   using Base::AttachNative;
   using Base::DisposeNative;
+  jni::Object::LocalRef GetNodeInfo(int32_t aID);
+  void SetText(int32_t aID, jni::String::Param aText);
+  void StartNativeAccessibility();
 
-  NS_INLINE_DECL_REFCOUNTING(SessionAccessibility)
+  // Event methods
+  void SendFocusEvent(AccessibleWrap* aAccessible);
+  void SendScrollingEvent(AccessibleWrap* aAccessible,
+                          int32_t aScrollX,
+                          int32_t aScrollY,
+                          int32_t aMaxScrollX,
+                          int32_t aMaxScrollY);
+  void SendAccessibilityFocusedEvent(AccessibleWrap* aAccessible);
+  void SendHoverEnterEvent(AccessibleWrap* aAccessible);
+  void SendTextSelectionChangedEvent(AccessibleWrap* aAccessible,
+                                     int32_t aCaretOffset);
+  void SendTextTraversedEvent(AccessibleWrap* aAccessible,
+                              int32_t aStartOffset,
+                              int32_t aEndOffset);
+  void SendTextChangedEvent(AccessibleWrap* aAccessible,
+                            const nsString& aStr,
+                            int32_t aStart,
+                            uint32_t aLen,
+                            bool aIsInsert,
+                            bool aFromUser);
+  void SendSelectedEvent(AccessibleWrap* aAccessible);
+  void SendClickedEvent(AccessibleWrap* aAccessible);
+  void SendWindowContentChangedEvent(AccessibleWrap* aAccessible);
+  void SendWindowStateChangedEvent(AccessibleWrap* aAccessible);
+
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SessionAccessibility)
 
 private:
   ~SessionAccessibility() {}
 
   void SetAttached(bool aAttached, already_AddRefed<Runnable> aRunnable);
+  RootAccessibleWrap* GetRoot();
 
   nsWindow::WindowPtr<SessionAccessibility> mWindow; // Parent only
   java::SessionAccessibility::NativeProvider::GlobalRef mSessionAccessibility;
 };
 
 } // namespace a11y
 } // namespace mozilla
 
--- a/accessible/android/moz.build
+++ b/accessible/android/moz.build
@@ -6,17 +6,20 @@
 
 EXPORTS.mozilla.a11y += ['AccessibleWrap.h',
     'HyperTextAccessibleWrap.h',
     'SessionAccessibility.h',
 ]
 
 SOURCES += [
     'AccessibleWrap.cpp',
+    'DocAccessibleWrap.cpp',
     'Platform.cpp',
+    'ProxyAccessibleWrap.cpp',
+    'RootAccessibleWrap.cpp',
     'SessionAccessibility.cpp',
 ]
 
 LOCAL_INCLUDES += [
     '/accessible/base',
     '/accessible/generic',
     '/accessible/html',
     '/accessible/ipc',
--- a/accessible/generic/Accessible.h
+++ b/accessible/generic/Accessible.h
@@ -550,17 +550,17 @@ public:
   /**
    * Focus the accessible.
    */
   virtual void TakeFocus() const;
 
   /**
    * Scroll the accessible into view.
    */
-  void ScrollTo(uint32_t aHow) const;
+  virtual void ScrollTo(uint32_t aHow) const;
 
   /**
    * Scroll the accessible to the given point.
    */
   void ScrollToPoint(uint32_t aCoordinateType, int32_t aX, int32_t aY);
 
   /**
    * Get a pointer to accessibility interface for this node, which is specific
--- a/accessible/ipc/ProxyAccessibleBase.h
+++ b/accessible/ipc/ProxyAccessibleBase.h
@@ -163,23 +163,24 @@ protected:
     , mWrapper(0)
     , mID(aID)
     , mRole(aRole)
     , mOuterDoc(false)
     , mIsDoc(false)
     , mHasValue(aInterfaces & Interfaces::VALUE)
     , mIsHyperLink(aInterfaces & Interfaces::HYPERLINK)
     , mIsHyperText(aInterfaces & Interfaces::HYPERTEXT)
+    , mIsSelection(aInterfaces & Interfaces::SELECTION)
   {
   }
 
   explicit ProxyAccessibleBase(DocAccessibleParent* aThisAsDoc) :
     mParent(kNoParent), mDoc(aThisAsDoc), mWrapper(0), mID(0),
     mRole(roles::DOCUMENT), mOuterDoc(false), mIsDoc(true), mHasValue(false),
-    mIsHyperLink(false), mIsHyperText(false)
+    mIsHyperLink(false), mIsHyperText(false), mIsSelection(false)
   {}
 
 protected:
   void SetParent(Derived* aParent);
 
 private:
   uintptr_t mParent;
   static const uintptr_t kNoParent = UINTPTR_MAX;
@@ -199,16 +200,17 @@ protected:
 private:
   bool mOuterDoc : 1;
 
 public:
   const bool mIsDoc: 1;
   const bool mHasValue: 1;
   const bool mIsHyperLink: 1;
   const bool mIsHyperText: 1;
+  const bool mIsSelection: 1;
 };
 
 extern template class ProxyAccessibleBase<ProxyAccessible>;
 
 }
 }
 
 #endif
--- a/accessible/jsat/AccessFu.jsm
+++ b/accessible/jsat/AccessFu.jsm
@@ -14,29 +14,29 @@ ChromeUtils.defineModuleGetter(this, "Re
 if (Utils.MozBuildApp === "mobile/android") {
   ChromeUtils.import("resource://gre/modules/Messaging.jsm");
 }
 
 const GECKOVIEW_MESSAGE = {
   ACTIVATE: "GeckoView:AccessibilityActivate",
   BY_GRANULARITY: "GeckoView:AccessibilityByGranularity",
   CLIPBOARD: "GeckoView:AccessibilityClipboard",
+  CURSOR_TO_FOCUSED: "GeckoView:AccessibilityCursorToFocused",
   EXPLORE_BY_TOUCH: "GeckoView:AccessibilityExploreByTouch",
   LONG_PRESS: "GeckoView:AccessibilityLongPress",
   NEXT: "GeckoView:AccessibilityNext",
   PREVIOUS: "GeckoView:AccessibilityPrevious",
   SCROLL_BACKWARD: "GeckoView:AccessibilityScrollBackward",
   SCROLL_FORWARD: "GeckoView:AccessibilityScrollForward",
   SELECT: "GeckoView:AccessibilitySelect",
   SET_SELECTION: "GeckoView:AccessibilitySetSelection",
   VIEW_FOCUSED: "GeckoView:AccessibilityViewFocused",
 };
 
 const ACCESSFU_MESSAGE = {
-  PRESENT: "AccessFu:Present",
   DOSCROLL: "AccessFu:DoScroll",
 };
 
 const FRAME_SCRIPT = "chrome://global/content/accessibility/content-script.js";
 
 var AccessFu = {
   /**
    * A lazy getter for event handler that binds the scope to AccessFu object.
@@ -52,21 +52,16 @@ var AccessFu = {
    */
   enable: function enable() {
     if (this._enabled) {
       return;
     }
     this._enabled = true;
 
     ChromeUtils.import("resource://gre/modules/accessibility/Utils.jsm");
-    ChromeUtils.import("resource://gre/modules/accessibility/Presentation.jsm");
-
-    // Check for output notification
-    this._notifyOutputPref =
-      new PrefCache("accessibility.accessfu.notify_output");
 
     Services.obs.addObserver(this, "remote-browser-shown");
     Services.obs.addObserver(this, "inprocess-browser-shown");
     Services.ww.registerNotification(this);
 
     for (let win of Services.wm.getEnumerator(null)) {
       this._attachWindow(win);
     }
@@ -87,35 +82,30 @@ var AccessFu = {
     Services.obs.removeObserver(this, "remote-browser-shown");
     Services.obs.removeObserver(this, "inprocess-browser-shown");
     Services.ww.unregisterNotification(this);
 
     for (let win of Services.wm.getEnumerator(null)) {
       this._detachWindow(win);
     }
 
-    delete this._notifyOutputPref;
-
     if (this.doneCallback) {
       this.doneCallback();
       delete this.doneCallback;
     }
 
     Logger.info("AccessFu:Disabled");
   },
 
   receiveMessage: function receiveMessage(aMessage) {
     Logger.debug(() => {
       return ["Recieved", aMessage.name, JSON.stringify(aMessage.json)];
     });
 
     switch (aMessage.name) {
-      case ACCESSFU_MESSAGE.PRESENT:
-        this._output(aMessage.json, aMessage.target);
-        break;
       case ACCESSFU_MESSAGE.DOSCROLL:
         this.Input.doScroll(aMessage.json, aMessage.target);
         break;
     }
   },
 
   _attachWindow: function _attachWindow(win) {
     let wtype = win.document.documentElement.getAttribute("windowtype");
@@ -150,48 +140,16 @@ var AccessFu = {
     win.removeEventListener("TabSelect", this);
     if (win.WindowEventDispatcher) {
       // desktop mochitests don't have this.
       win.WindowEventDispatcher.unregisterListener(this,
         Object.values(GECKOVIEW_MESSAGE));
     }
   },
 
-  _output: function _output(aPresentationData, aBrowser) {
-    if (!aPresentationData) {
-      // Either no android events to send or a string used for testing only.
-      return;
-    }
-
-    if (!Utils.isAliveAndVisible(Utils.AccService.getAccessibleFor(aBrowser))) {
-      return;
-    }
-
-    let win = aBrowser.ownerGlobal;
-
-    for (let evt of aPresentationData) {
-      if (typeof evt == "string") {
-        continue;
-      }
-
-      if (win.WindowEventDispatcher) {
-        // desktop mochitests don't have this.
-        win.WindowEventDispatcher.sendRequest({
-          ...evt,
-          type: "GeckoView:AccessibilityEvent"
-        });
-      }
-    }
-
-    if (this._notifyOutputPref.value) {
-      Services.obs.notifyObservers(null, "accessibility-output",
-                                   JSON.stringify(aPresentationData));
-    }
-  },
-
   onEvent(event, data, callback) {
     switch (event) {
       case GECKOVIEW_MESSAGE.SETTINGS:
         if (data.enabled) {
           this._enable();
         } else {
           this._disable();
         }
@@ -214,21 +172,18 @@ var AccessFu = {
         // XXX: Advertize long press on supported objects and implement action
         break;
       case GECKOVIEW_MESSAGE.SCROLL_FORWARD:
         this.Input.androidScroll("forward");
         break;
       case GECKOVIEW_MESSAGE.SCROLL_BACKWARD:
         this.Input.androidScroll("backward");
         break;
-      case GECKOVIEW_MESSAGE.VIEW_FOCUSED:
-        this._focused = data.gainFocus;
-        if (this._focused) {
-          this.autoMove({ forcePresent: true, noOpIfOnScreen: true });
-        }
+      case GECKOVIEW_MESSAGE.CURSOR_TO_FOCUSED:
+        this.autoMove({ moveToFocused: true });
         break;
       case GECKOVIEW_MESSAGE.BY_GRANULARITY:
         this.Input.moveByGranularity(data);
         break;
       case GECKOVIEW_MESSAGE.EXPLORE_BY_TOUCH:
         this.Input.moveToPoint("Simple", ...data.coordinates);
         break;
       case GECKOVIEW_MESSAGE.SET_SELECTION:
@@ -276,20 +231,16 @@ var AccessFu = {
     }
   },
 
   autoMove: function autoMove(aOptions) {
     const mm = Utils.getMessageManager();
     mm.sendAsyncMessage("AccessFu:AutoMove", aOptions);
   },
 
-  announce: function announce(aAnnouncement) {
-    this._output(Presentation.announce(aAnnouncement), Utils.getCurrentBrowser());
-  },
-
   // So we don't enable/disable twice
   _enabled: false,
 
   // Layerview is focused
   _focused: false,
 
   /**
    * Adjusts the given bounds that are defined in device display pixels
--- a/accessible/jsat/ContentControl.jsm
+++ b/accessible/jsat/ContentControl.jsm
@@ -9,18 +9,16 @@ ChromeUtils.defineModuleGetter(this, "Lo
 ChromeUtils.defineModuleGetter(this, "Roles",
   "resource://gre/modules/accessibility/Constants.jsm");
 ChromeUtils.defineModuleGetter(this, "States",
   "resource://gre/modules/accessibility/Constants.jsm");
 ChromeUtils.defineModuleGetter(this, "TraversalRules",
   "resource://gre/modules/accessibility/Traversal.jsm");
 ChromeUtils.defineModuleGetter(this, "TraversalHelper",
   "resource://gre/modules/accessibility/Traversal.jsm");
-ChromeUtils.defineModuleGetter(this, "Presentation",
-  "resource://gre/modules/accessibility/Presentation.jsm");
 
 var EXPORTED_SYMBOLS = ["ContentControl"];
 
 const MOVEMENT_GRANULARITY_CHARACTER = 1;
 const MOVEMENT_GRANULARITY_WORD = 2;
 const MOVEMENT_GRANULARITY_LINE = 4;
 
 const CLIPBOARD_COPY = 0x4000;
@@ -150,19 +148,16 @@ this.ContentControl.prototype = {
         // new position.
         this.sendToChild(vc, aMessage, { action: childAction }, true);
       }
     } else if (!this._childMessageSenders.has(aMessage.target) &&
                origin !== "top") {
       // We failed to move, and the message is not from a parent, so forward
       // to it.
       this.sendToParent(aMessage);
-    } else {
-      this._contentScope.get().sendAsyncMessage("AccessFu:Present",
-        Presentation.noMove(action));
     }
   },
 
   handleMoveToPoint: function cc_handleMoveToPoint(aMessage) {
     let [x, y] = [aMessage.json.x, aMessage.json.y];
     let rule = TraversalRules[aMessage.json.rule];
 
     this.vc.moveToPoint(rule, x, y, true);
@@ -223,39 +218,28 @@ this.ContentControl.prototype = {
 
         for (let eventType of ["mousedown", "mouseup"]) {
           let evt = this.document.createEvent("MouseEvents");
           evt.initMouseEvent(eventType, true, true, this.window,
             x, y, 0, 0, 0, false, false, false, false, 0, null);
           node.dispatchEvent(evt);
         }
       }
-
-      // Action invoked will be presented on checked/selected state change.
-      if (!Utils.getState(aAccessible).contains(States.CHECKABLE) &&
-          !Utils.getState(aAccessible).contains(States.SELECTABLE)) {
-        this._contentScope.get().sendAsyncMessage("AccessFu:Present",
-          Presentation.actionInvoked());
-      }
     };
 
     let focusedAcc = Utils.AccService.getAccessibleFor(
       this.document.activeElement);
     if (focusedAcc && this.vc.position === focusedAcc
         && focusedAcc.role === Roles.ENTRY) {
       let accText = focusedAcc.QueryInterface(Ci.nsIAccessibleText);
-      let oldOffset = accText.caretOffset;
       let newOffset = aMessage.json.offset;
-      let text = accText.getText(0, accText.characterCount);
-
       if (newOffset >= 0 && newOffset <= accText.characterCount) {
         accText.caretOffset = newOffset;
       }
 
-      this.presentCaretChange(text, oldOffset, accText.caretOffset);
       return;
     }
 
     // recursively find a descendant that is activatable.
     let getActivatableDescendant = (aAccessible) => {
       if (aAccessible.actionCount > 0) {
         return aAccessible;
       }
@@ -394,25 +378,16 @@ this.ContentControl.prototype = {
           if (startSel != endSel) {
             editText.cutText(startSel, endSel);
           }
           break;
       }
     }
   },
 
-  presentCaretChange: function cc_presentCaretChange(
-    aText, aOldOffset, aNewOffset) {
-    if (aOldOffset !== aNewOffset) {
-      let msg = Presentation.textSelectionChanged(aText, aNewOffset, aNewOffset,
-        aOldOffset, aOldOffset, true);
-      this._contentScope.get().sendAsyncMessage("AccessFu:Present", msg);
-    }
-  },
-
   getChildCursor: function cc_getChildCursor(aAccessible) {
     let acc = aAccessible || this.vc.position;
     if (Utils.isAliveAndVisible(acc) && acc.role === Roles.INTERNAL_FRAME) {
       let domNode = acc.DOMNode;
       let mm = this._childMessageSenders.get(domNode, null);
       if (!mm) {
         mm = Utils.getMessageManager(domNode);
         mm.addWeakMessageListener("AccessFu:MoveCursor", this);
@@ -451,48 +426,37 @@ this.ContentControl.prototype = {
   sendToParent: function cc_sendToParent(aMessage) {
     // XXX: This is a silly way to make a deep copy
     let newJSON = JSON.parse(JSON.stringify(aMessage.json));
     newJSON.origin = "child";
     aMessage.target.sendAsyncMessage(aMessage.name, newJSON);
   },
 
   /**
-   * Move cursor and/or present its location.
+   * Move cursor.
    * aOptions could have any of these fields:
    * - delay: in ms, before actual move is performed. Another autoMove call
    *    would cancel it. Useful if we want to wait for a possible trailing
    *    focus move. Default 0.
    * - noOpIfOnScreen: if accessible is alive and visible, don't do anything.
-   * - forcePresent: present cursor location, whether we move or don't.
    * - moveToFocused: if there is a focused accessible move to that. This takes
    *    precedence over given anchor.
    * - moveMethod: pivot move method to use, default is 'moveNext',
    */
   autoMove: function cc_autoMove(aAnchor, aOptions = {}) {
     this.cancelAutoMove();
 
     let moveFunc = () => {
       let vc = this.vc;
       let acc = aAnchor;
       let rule = aOptions.onScreenOnly ?
         TraversalRules.SimpleOnScreen : TraversalRules.Simple;
-      let forcePresentFunc = () => {
-        if (aOptions.forcePresent) {
-          this._contentScope.get().sendAsyncMessage(
-            "AccessFu:Present", Presentation.pivotChanged(
-              vc.position, null, vc.startOffset, vc.endOffset,
-              Ci.nsIAccessiblePivot.REASON_NONE,
-              Ci.nsIAccessiblePivot.NO_BOUNDARY));
-        }
-      };
 
       if (aOptions.noOpIfOnScreen &&
         Utils.isAliveAndVisible(vc.position, true)) {
-        forcePresentFunc();
         return;
       }
 
       if (aOptions.moveToFocused) {
         acc = Utils.AccService.getAccessibleFor(
           this.document.activeElement) || acc;
       }
 
@@ -504,29 +468,24 @@ this.ContentControl.prototype = {
         moved = vc[moveFirstOrLast ? "moveNext" : moveMethod](rule, acc, true,
                                                               true);
       }
       if (moveFirstOrLast && !moved) {
         // We move to first/last after no anchor move happened or succeeded.
         moved = vc[moveMethod](rule, true);
       }
 
-      let sentToChild = this.sendToChild(vc, {
+      this.sendToChild(vc, {
         name: "AccessFu:AutoMove",
         json: {
           moveMethod: aOptions.moveMethod,
           moveToFocused: aOptions.moveToFocused,
           noOpIfOnScreen: true,
-          forcePresent: true
         }
       }, null, true);
-
-      if (!moved && !sentToChild) {
-        forcePresentFunc();
-      }
     };
 
     if (aOptions.delay) {
       this._autoMove = this.window.setTimeout(moveFunc, aOptions.delay);
     } else {
       moveFunc();
     }
   },
--- a/accessible/jsat/EventManager.jsm
+++ b/accessible/jsat/EventManager.jsm
@@ -5,125 +5,73 @@
 "use strict";
 
 ChromeUtils.defineModuleGetter(this, "Services",
   "resource://gre/modules/Services.jsm");
 ChromeUtils.defineModuleGetter(this, "Utils",
   "resource://gre/modules/accessibility/Utils.jsm");
 ChromeUtils.defineModuleGetter(this, "Logger",
   "resource://gre/modules/accessibility/Utils.jsm");
-ChromeUtils.defineModuleGetter(this, "Presentation",
-  "resource://gre/modules/accessibility/Presentation.jsm");
 ChromeUtils.defineModuleGetter(this, "Roles",
   "resource://gre/modules/accessibility/Constants.jsm");
 ChromeUtils.defineModuleGetter(this, "Events",
   "resource://gre/modules/accessibility/Constants.jsm");
 ChromeUtils.defineModuleGetter(this, "States",
   "resource://gre/modules/accessibility/Constants.jsm");
-ChromeUtils.defineModuleGetter(this, "clearTimeout",
-  "resource://gre/modules/Timer.jsm");
-ChromeUtils.defineModuleGetter(this, "setTimeout",
-  "resource://gre/modules/Timer.jsm");
 
 var EXPORTED_SYMBOLS = ["EventManager"];
 
 function EventManager(aContentScope) {
   this.contentScope = aContentScope;
   this.addEventListener = this.contentScope.addEventListener.bind(
     this.contentScope);
   this.removeEventListener = this.contentScope.removeEventListener.bind(
     this.contentScope);
   this.sendMsgFunc = this.contentScope.sendAsyncMessage.bind(
     this.contentScope);
-  this.webProgress = this.contentScope.docShell.
-    QueryInterface(Ci.nsIInterfaceRequestor).
-    getInterface(Ci.nsIWebProgress);
 }
 
 this.EventManager.prototype = {
   start: function start() {
     try {
       if (!this._started) {
         Logger.debug("EventManager.start");
 
         this._started = true;
 
         AccessibilityEventObserver.addListener(this);
 
-        this.webProgress.addProgressListener(this,
-          (Ci.nsIWebProgress.NOTIFY_STATE_ALL |
-           Ci.nsIWebProgress.NOTIFY_LOCATION));
-        this.addEventListener("wheel", this, true);
-        this.addEventListener("scroll", this, true);
-        this.addEventListener("resize", this, true);
         this._preDialogPosition = new WeakMap();
       }
-      this.present(Presentation.tabStateChanged(null, "newtab"));
-
     } catch (x) {
       Logger.logException(x, "Failed to start EventManager");
     }
   },
 
   // XXX: Stop is not called when the tab is closed (|TabClose| event is too
   // late). It is only called when the AccessFu is disabled explicitly.
   stop: function stop() {
     if (!this._started) {
       return;
     }
     Logger.debug("EventManager.stop");
     AccessibilityEventObserver.removeListener(this);
     try {
       this._preDialogPosition = new WeakMap();
-      this.webProgress.removeProgressListener(this);
-      this.removeEventListener("wheel", this, true);
-      this.removeEventListener("scroll", this, true);
-      this.removeEventListener("resize", this, true);
     } catch (x) {
       // contentScope is dead.
     } finally {
       this._started = false;
     }
   },
 
   get contentControl() {
     return this.contentScope._jsat_contentControl;
   },
 
-  handleEvent: function handleEvent(aEvent) {
-    Logger.debug(() => {
-      return ["DOMEvent", aEvent.type];
-    });
-
-    // The target could be an element, document or window
-    const win = aEvent.target.ownerGlobal;
-    try {
-      switch (aEvent.type) {
-        case "wheel":
-        {
-          let delta = aEvent.deltaX || aEvent.deltaY;
-          this.contentControl.autoMove(
-           null,
-           { moveMethod: delta > 0 ? "moveNext" : "movePrevious",
-             onScreenOnly: true, noOpIfOnScreen: true, delay: 500 });
-          break;
-        }
-        case "scroll":
-          this.present(Presentation.viewportScrolled(win));
-        case "resize":
-        {
-          this.present(Presentation.viewportChanged(win));
-          break;
-        }
-      }
-    } catch (x) {
-      Logger.logException(x, "Error handling DOM event");
-    }
-  },
-
   handleAccEvent: function handleAccEvent(aEvent) {
     Logger.debug(() => {
       return ["A11yEvent", Logger.eventToString(aEvent),
               Logger.accessibleToString(aEvent.accessible)];
     });
 
     // Don't bother with non-content events in firefox.
     if (Utils.MozBuildApp == "browser" &&
@@ -151,358 +99,47 @@ this.EventManager.prototype = {
         if (position && position.role == Roles.INTERNAL_FRAME) {
           break;
         }
 
         // Blur to document if new position is not explicitly focused.
         if (!position || !Utils.getState(position).contains(States.FOCUSED)) {
           aEvent.accessibleDocument.takeFocus();
         }
-
-        this.present(
-          Presentation.pivotChanged(position, event.oldAccessible,
-                                    event.newStartOffset, event.newEndOffset,
-                                    event.reason, event.boundaryType));
-
-        break;
-      }
-      case Events.STATE_CHANGE:
-      {
-        const event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent);
-        const state = Utils.getState(event);
-        if (state.contains(States.CHECKED)) {
-          this.present(Presentation.checked(aEvent.accessible));
-        } else if (state.contains(States.SELECTED)) {
-          this.present(Presentation.selected(aEvent.accessible));
-        }
         break;
       }
       case Events.NAME_CHANGE:
       {
-        let acc = aEvent.accessible;
-        if (acc === this.contentControl.vc.position) {
-          this.present(Presentation.nameChanged(acc));
-        } else {
-          let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
-            ["text", "all"]);
-          if (liveRegion) {
-            this.present(Presentation.nameChanged(acc, isPolite));
-          }
-        }
+        // XXX: Port to Android
         break;
       }
       case Events.SCROLLING_START:
       {
         this.contentControl.autoMove(aEvent.accessible);
         break;
       }
-      case Events.TEXT_CARET_MOVED:
-      {
-        let acc = aEvent.accessible.QueryInterface(Ci.nsIAccessibleText);
-        let caretOffset = aEvent.
-          QueryInterface(Ci.nsIAccessibleCaretMoveEvent).caretOffset;
-
-        // We could get a caret move in an accessible that is not focused,
-        // it doesn't mean we are not on any editable accessible. just not
-        // on this one..
-        let state = Utils.getState(acc);
-        if (state.contains(States.FOCUSED) && state.contains(States.EDITABLE)) {
-          let fromIndex = caretOffset;
-          if (acc.selectionCount) {
-            const [startSel, endSel] = Utils.getTextSelection(acc);
-            fromIndex = startSel == caretOffset ? endSel : startSel;
-          }
-          this.present(Presentation.textSelectionChanged(
-            acc.getText(0, -1), fromIndex, caretOffset, 0, 0,
-            aEvent.isFromUserInput));
-        }
-        break;
-      }
       case Events.SHOW:
       {
-        this._handleShow(aEvent);
+        // XXX: Port to Android
         break;
       }
       case Events.HIDE:
       {
-        let evt = aEvent.QueryInterface(Ci.nsIAccessibleHideEvent);
-        this._handleHide(evt);
-        break;
-      }
-      case Events.TEXT_INSERTED:
-      case Events.TEXT_REMOVED:
-      {
-        let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
-          ["text", "all"]);
-        if (aEvent.isFromUserInput || liveRegion) {
-          // Handle all text mutations coming from the user or if they happen
-          // on a live region.
-          this._handleText(aEvent, liveRegion, isPolite);
-        }
+        // XXX: Port to Android
         break;
       }
-      case Events.FOCUS:
-      {
-        // Put vc where the focus is at
-        let acc = aEvent.accessible;
-        if (![Roles.CHROME_WINDOW,
-             Roles.DOCUMENT,
-             Roles.APPLICATION].includes(acc.role)) {
-          this.contentControl.autoMove(acc);
-        }
-
-        this.present(Presentation.focused(acc));
-
-       if (Utils.inTest) {
-        this.sendMsgFunc("AccessFu:Focused");
-       }
-       break;
-      }
-      case Events.DOCUMENT_LOAD_COMPLETE:
-      {
-        let position = this.contentControl.vc.position;
-        // Check if position is in the subtree of the DOCUMENT_LOAD_COMPLETE
-        // event's dialog accessible or accessible document
-        let subtreeRoot = aEvent.accessible.role === Roles.DIALOG ?
-          aEvent.accessible : aEvent.accessibleDocument;
-        if (aEvent.accessible === aEvent.accessibleDocument ||
-            (position && Utils.isInSubtree(position, subtreeRoot))) {
-          // Do not automove into the document if the virtual cursor is already
-          // positioned inside it.
-          break;
-        }
-        this._preDialogPosition.set(aEvent.accessible.DOMNode, position);
-        this.contentControl.autoMove(aEvent.accessible, { delay: 500 });
-        break;
-      }
-      case Events.TEXT_VALUE_CHANGE:
-        // We handle this events in TEXT_INSERTED/TEXT_REMOVED.
-        break;
       case Events.VALUE_CHANGE:
       {
-        let position = this.contentControl.vc.position;
-        let target = aEvent.accessible;
-        if (position === target ||
-            Utils.getEmbeddedControl(position) === target) {
-          this.present(Presentation.valueChanged(target));
-        } else {
-          let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
-            ["text", "all"]);
-          if (liveRegion) {
-            this.present(Presentation.valueChanged(target, isPolite));
-          }
-        }
-      }
-    }
-  },
-
-  _handleShow: function _handleShow(aEvent) {
-    let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
-      ["additions", "all"]);
-    // Only handle show if it is a relevant live region.
-    if (!liveRegion) {
-      return;
-    }
-    // Show for text is handled by the EVENT_TEXT_INSERTED handler.
-    if (aEvent.accessible.role === Roles.TEXT_LEAF) {
-      return;
-    }
-    this._dequeueLiveEvent(Events.HIDE, liveRegion);
-    this.present(Presentation.liveRegion(liveRegion, isPolite, false));
-  },
-
-  _handleHide: function _handleHide(aEvent) {
-    let {liveRegion, isPolite} = this._handleLiveRegion(
-      aEvent, ["removals", "all"]);
-    let acc = aEvent.accessible;
-    if (liveRegion) {
-      // Hide for text is handled by the EVENT_TEXT_REMOVED handler.
-      if (acc.role === Roles.TEXT_LEAF) {
-        return;
-      }
-      this._queueLiveEvent(Events.HIDE, liveRegion, isPolite);
-    } else {
-      let vc = Utils.getVirtualCursor(this.contentScope.content.document);
-      if (vc.position &&
-        (Utils.getState(vc.position).contains(States.DEFUNCT) ||
-          Utils.isInSubtree(vc.position, acc))) {
-        let position = this._preDialogPosition.get(aEvent.accessible.DOMNode) ||
-          aEvent.targetPrevSibling || aEvent.targetParent;
-        if (!position) {
-          try {
-            position = acc.previousSibling;
-          } catch (x) {
-            // Accessible is unattached from the accessible tree.
-            position = acc.parent;
-          }
-        }
-        this.contentControl.autoMove(position,
-          { moveToFocused: true, delay: 500 });
+        // XXX: Port to Android
+        break;
       }
     }
   },
 
-  _handleText: function _handleText(aEvent, aLiveRegion, aIsPolite) {
-    let event = aEvent.QueryInterface(Ci.nsIAccessibleTextChangeEvent);
-    let isInserted = event.isInserted;
-    let txtIface = aEvent.accessible.QueryInterface(Ci.nsIAccessibleText);
-
-    let text = "";
-    try {
-      text = txtIface.getText(0, Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT);
-    } catch (x) {
-      // XXX we might have gotten an exception with of a
-      // zero-length text. If we did, ignore it (bug #749810).
-      if (txtIface.characterCount) {
-        throw x;
-      }
-    }
-    // If there are embedded objects in the text, ignore them.
-    // Assuming changes to the descendants would already be handled by the
-    // show/hide event.
-    let modifiedText = event.modifiedText.replace(/\uFFFC/g, "");
-    if (modifiedText != event.modifiedText && !modifiedText.trim()) {
-      return;
-    }
-
-    if (aLiveRegion) {
-      if (aEvent.eventType === Events.TEXT_REMOVED) {
-        this._queueLiveEvent(Events.TEXT_REMOVED, aLiveRegion, aIsPolite,
-          modifiedText);
-      } else {
-        this._dequeueLiveEvent(Events.TEXT_REMOVED, aLiveRegion);
-        this.present(Presentation.liveRegion(aLiveRegion, aIsPolite, false,
-          modifiedText));
-      }
-    } else {
-      this.present(Presentation.textChanged(aEvent.accessible, isInserted,
-        event.start, event.length, text, modifiedText));
-    }
-  },
-
-  _handleLiveRegion: function _handleLiveRegion(aEvent, aRelevant) {
-    if (aEvent.isFromUserInput) {
-      return {};
-    }
-    let parseLiveAttrs = function parseLiveAttrs(aAccessible) {
-      let attrs = Utils.getAttributes(aAccessible);
-      if (attrs["container-live"]) {
-        return {
-          live: attrs["container-live"],
-          relevant: attrs["container-relevant"] || "additions text",
-          busy: attrs["container-busy"],
-          atomic: attrs["container-atomic"],
-          memberOf: attrs["member-of"]
-        };
-      }
-      return null;
-    };
-    // XXX live attributes are not set for hidden accessibles yet. Need to
-    // climb up the tree to check for them.
-    let getLiveAttributes = function getLiveAttributes(aEvent) {
-      let liveAttrs = parseLiveAttrs(aEvent.accessible);
-      if (liveAttrs) {
-        return liveAttrs;
-      }
-      let parent = aEvent.targetParent;
-      while (parent) {
-        liveAttrs = parseLiveAttrs(parent);
-        if (liveAttrs) {
-          return liveAttrs;
-        }
-        parent = parent.parent;
-      }
-      return {};
-    };
-    let {live, relevant, /* busy, atomic, memberOf */ } = getLiveAttributes(aEvent);
-    // If container-live is not present or is set to |off| ignore the event.
-    if (!live || live === "off") {
-      return {};
-    }
-    // XXX: support busy and atomic.
-
-    // Determine if the type of the mutation is relevant. Default is additions
-    // and text.
-    let isRelevant = Utils.matchAttributeValue(relevant, aRelevant);
-    if (!isRelevant) {
-      return {};
-    }
-    return {
-      liveRegion: aEvent.accessible,
-      isPolite: live === "polite"
-    };
-  },
-
-  _dequeueLiveEvent: function _dequeueLiveEvent(aEventType, aLiveRegion) {
-    let domNode = aLiveRegion.DOMNode;
-    if (this._liveEventQueue && this._liveEventQueue.has(domNode)) {
-      let queue = this._liveEventQueue.get(domNode);
-      let nextEvent = queue[0];
-      if (nextEvent.eventType === aEventType) {
-        clearTimeout(nextEvent.timeoutID);
-        queue.shift();
-        if (queue.length === 0) {
-          this._liveEventQueue.delete(domNode);
-        }
-      }
-    }
-  },
-
-  _queueLiveEvent: function _queueLiveEvent(aEventType, aLiveRegion, aIsPolite, aModifiedText) {
-    if (!this._liveEventQueue) {
-      this._liveEventQueue = new WeakMap();
-    }
-    let eventHandler = {
-      eventType: aEventType,
-      timeoutID: setTimeout(this.present.bind(this),
-        20, // Wait for a possible EVENT_SHOW or EVENT_TEXT_INSERTED event.
-        Presentation.liveRegion(aLiveRegion, aIsPolite, true, aModifiedText))
-    };
-
-    let domNode = aLiveRegion.DOMNode;
-    if (this._liveEventQueue.has(domNode)) {
-      this._liveEventQueue.get(domNode).push(eventHandler);
-    } else {
-      this._liveEventQueue.set(domNode, [eventHandler]);
-    }
-  },
-
-  present: function present(aPresentationData) {
-    if (aPresentationData && aPresentationData.length > 0) {
-      this.sendMsgFunc("AccessFu:Present", aPresentationData);
-    }
-  },
-
-  onStateChange: function onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
-    let tabstate = "";
-
-    let loadingState = Ci.nsIWebProgressListener.STATE_TRANSFERRING |
-      Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
-    let loadedState = Ci.nsIWebProgressListener.STATE_STOP |
-      Ci.nsIWebProgressListener.STATE_IS_NETWORK;
-
-    if ((aStateFlags & loadingState) == loadingState) {
-      tabstate = "loading";
-    } else if ((aStateFlags & loadedState) == loadedState &&
-               !aWebProgress.isLoadingDocument) {
-      tabstate = "loaded";
-    }
-
-    if (tabstate) {
-      let docAcc = Utils.AccService.getAccessibleFor(aWebProgress.DOMWindow.document);
-      this.present(Presentation.tabStateChanged(docAcc, tabstate));
-    }
-  },
-
-  onLocationChange: function onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
-    let docAcc = Utils.AccService.getAccessibleFor(aWebProgress.DOMWindow.document);
-    this.present(Presentation.tabStateChanged(docAcc, "newdoc"));
-  },
-
-  QueryInterface: ChromeUtils.generateQI([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference, Ci.nsIObserver])
+  QueryInterface: ChromeUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIObserver])
 };
 
 const AccessibilityEventObserver = {
 
   /**
    * A WeakMap containing [content, EventManager] pairs.
    */
   eventManagers: new WeakMap(),
deleted file mode 100644
--- a/accessible/jsat/OutputGenerator.jsm
+++ /dev/null
@@ -1,824 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-/* exported UtteranceGenerator */
-
-"use strict";
-
-const INCLUDE_DESC = 0x01;
-const INCLUDE_NAME = 0x02;
-const INCLUDE_VALUE = 0x04;
-const NAME_FROM_SUBTREE_RULE = 0x10;
-const IGNORE_EXPLICIT_NAME = 0x20;
-
-const OUTPUT_DESC_FIRST = 0;
-const OUTPUT_DESC_LAST = 1;
-
-ChromeUtils.defineModuleGetter(this, "Utils", // jshint ignore:line
-  "resource://gre/modules/accessibility/Utils.jsm");
-ChromeUtils.defineModuleGetter(this, "PrefCache", // jshint ignore:line
-  "resource://gre/modules/accessibility/Utils.jsm");
-ChromeUtils.defineModuleGetter(this, "Logger", // jshint ignore:line
-  "resource://gre/modules/accessibility/Utils.jsm");
-ChromeUtils.defineModuleGetter(this, "Roles", // jshint ignore:line
-  "resource://gre/modules/accessibility/Constants.jsm");
-ChromeUtils.defineModuleGetter(this, "States", // jshint ignore:line
-  "resource://gre/modules/accessibility/Constants.jsm");
-
-var EXPORTED_SYMBOLS = ["UtteranceGenerator"]; // jshint ignore:line
-
-var OutputGenerator = {
-
-  defaultOutputOrder: OUTPUT_DESC_LAST,
-
-  /**
-   * Generates output for a PivotContext.
-   * @param {PivotContext} aContext object that generates and caches
-   *    context information for a given accessible and its relationship with
-   *    another accessible.
-   * @return {Object} An array of speech data. Depending on the utterance order,
-   *    the data describes the context for an accessible object either
-   *    starting from the accessible's ancestry or accessible's subtree.
-   */
-  genForContext: function genForContext(aContext) {
-    let output = [];
-    let self = this;
-    let addOutput = function addOutput(aAccessible) {
-      output.push.apply(output, self.genForObject(aAccessible, aContext));
-    };
-    let ignoreSubtree = function ignoreSubtree(aAccessible) {
-      let roleString = Utils.AccService.getStringRole(aAccessible.role);
-      let nameRule = self.roleRuleMap[roleString] || 0;
-      // Ignore subtree if the name is explicit and the role's name rule is the
-      // NAME_FROM_SUBTREE_RULE.
-      return (((nameRule & INCLUDE_VALUE) && aAccessible.value) ||
-              ((nameRule & NAME_FROM_SUBTREE_RULE) &&
-               (Utils.getAttributes(aAccessible)["explicit-name"] === "true" &&
-               !(nameRule & IGNORE_EXPLICIT_NAME))));
-    };
-
-    let contextStart = this._getContextStart(aContext);
-
-    if (this.outputOrder === OUTPUT_DESC_FIRST) {
-      contextStart.forEach(addOutput);
-      addOutput(aContext.accessible);
-      for (let node of aContext.subtreeGenerator(true, ignoreSubtree)) {
-        addOutput(node);
-      }
-    } else {
-      for (let node of aContext.subtreeGenerator(false, ignoreSubtree)) {
-        addOutput(node);
-      }
-      addOutput(aContext.accessible);
-
-      // If there are any documents in new ancestry, find a first one and place
-      // it in the beginning of the utterance.
-      let doc, docIndex = contextStart.findIndex(
-        ancestor => ancestor.role === Roles.DOCUMENT);
-
-      if (docIndex > -1) {
-        doc = contextStart.splice(docIndex, 1)[0];
-      }
-
-      contextStart.reverse().forEach(addOutput);
-      if (doc) {
-        output.unshift.apply(output, self.genForObject(doc, aContext));
-      }
-    }
-
-    return output;
-  },
-
-
-  /**
-   * Generates output for an object.
-   * @param {nsIAccessible} aAccessible accessible object to generate output
-   *    for.
-   * @param {PivotContext} aContext object that generates and caches
-   *    context information for a given accessible and its relationship with
-   *    another accessible.
-   * @return {Array} A 2 element array of speech data. The first element
-   *    describes the object and its state. The second element is the object's
-   *    name. Whether the object's description or it's role is included is
-   *    determined by {@link roleRuleMap}.
-   */
-  genForObject: function genForObject(aAccessible, aContext) {
-    let roleString = Utils.AccService.getStringRole(aAccessible.role);
-    let func = this.objectOutputFunctions[
-      OutputGenerator._getOutputName(roleString)] ||
-      this.objectOutputFunctions.defaultFunc;
-
-    let flags = this.roleRuleMap[roleString] || 0;
-
-    if (aAccessible.childCount === 0) {
-      flags |= INCLUDE_NAME;
-    }
-
-    return func.apply(this, [aAccessible, roleString,
-                             Utils.getState(aAccessible), flags, aContext]);
-  },
-
-  /**
-   * Generates output for an action performed.
-   * @param {nsIAccessible} aAccessible accessible object that the action was
-   *    invoked in.
-   * @param {string} aActionName the name of the action, one of the keys in
-   *    {@link gActionMap}.
-   * @return {Array} A one element array with action data.
-   */
-  genForAction: function genForAction(aObject, aActionName) {}, // jshint ignore:line
-
-  /**
-   * Generates output for an announcement.
-   * @param {string} aAnnouncement unlocalized announcement.
-   * @return {Array} An announcement speech data to be localized.
-   */
-  genForAnnouncement: function genForAnnouncement(aAnnouncement) {}, // jshint ignore:line
-
-  /**
-   * Generates output for a tab state change.
-   * @param {nsIAccessible} aAccessible accessible object of the tab's attached
-   *    document.
-   * @param {string} aTabState the tab state name, see
-   *    {@link Presenter.tabStateChanged}.
-   * @return {Array} The tab state utterace.
-   */
-  genForTabStateChange: function genForTabStateChange(aObject, aTabState) {}, // jshint ignore:line
-
-  /**
-   * Generates output for announcing entering and leaving editing mode.
-   * @param {aIsEditing} boolean true if we are in editing mode
-   * @return {Array} The mode utterance
-   */
-  genForEditingMode: function genForEditingMode(aIsEditing) {}, // jshint ignore:line
-
-  _getContextStart: function getContextStart(aContext) {}, // jshint ignore:line
-
-  /**
-   * Adds an accessible name and description to the output if available.
-   * @param {Array} aOutput Output array.
-   * @param {nsIAccessible} aAccessible current accessible object.
-   * @param {Number} aFlags output flags.
-   */
-  _addName: function _addName(aOutput, aAccessible, aFlags) {
-    let name;
-    if ((Utils.getAttributes(aAccessible)["explicit-name"] === "true" &&
-         !(aFlags & IGNORE_EXPLICIT_NAME)) || (aFlags & INCLUDE_NAME)) {
-      name = aAccessible.name;
-    }
-
-    let description = aAccessible.description;
-    if (description) {
-      // Compare against the calculated name unconditionally, regardless of name rule,
-      // so we can make sure we don't speak duplicated descriptions
-      let tmpName = name || aAccessible.name;
-      if (tmpName && (description !== tmpName)) {
-        if (name) {
-          name = this.outputOrder === OUTPUT_DESC_FIRST ?
-            description + " - " + name :
-            name + " - " + description;
-        } else {
-          name = description;
-        }
-      }
-    }
-
-    if (!name || !name.trim()) {
-      return;
-    }
-    aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? "push" : "unshift"](name);
-  },
-
-  /**
-   * Adds a landmark role to the output if available.
-   * @param {Array} aOutput Output array.
-   * @param {nsIAccessible} aAccessible current accessible object.
-   */
-  _addLandmark: function _addLandmark(aOutput, aAccessible) {
-    let landmarkName = Utils.getLandmarkName(aAccessible);
-    if (!landmarkName) {
-      return;
-    }
-    aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? "unshift" : "push"]({
-      string: landmarkName
-    });
-  },
-
-  /**
-   * Adds math roles to the output, for a MathML accessible.
-   * @param {Array} aOutput Output array.
-   * @param {nsIAccessible} aAccessible current accessible object.
-   * @param {String} aRoleStr aAccessible's role string.
-   */
-  _addMathRoles: function _addMathRoles(aOutput, aAccessible, aRoleStr) {
-    // First, determine the actual role to use (e.g. mathmlfraction).
-    let roleStr = aRoleStr;
-    switch (aAccessible.role) {
-      case Roles.MATHML_CELL:
-      case Roles.MATHML_ENCLOSED:
-      case Roles.MATHML_LABELED_ROW:
-      case Roles.MATHML_ROOT:
-      case Roles.MATHML_SQUARE_ROOT:
-      case Roles.MATHML_TABLE:
-      case Roles.MATHML_TABLE_ROW:
-        // Use the default role string.
-        break;
-      case Roles.MATHML_MULTISCRIPTS:
-      case Roles.MATHML_OVER:
-      case Roles.MATHML_SUB:
-      case Roles.MATHML_SUB_SUP:
-      case Roles.MATHML_SUP:
-      case Roles.MATHML_UNDER:
-      case Roles.MATHML_UNDER_OVER:
-        // For scripted accessibles, use the string 'mathmlscripted'.
-        roleStr = "mathmlscripted";
-        break;
-      case Roles.MATHML_FRACTION:
-        // From a semantic point of view, the only important point is to
-        // distinguish between fractions that have a bar and those that do not.
-        // Per the MathML 3 spec, the latter happens iff the linethickness
-        // attribute is of the form [zero-float][optional-unit]. In that case,
-        // we use the string 'mathmlfractionwithoutbar'.
-        let linethickness = Utils.getAttributes(aAccessible).linethickness;
-        if (linethickness) {
-            let numberMatch = linethickness.match(/^(?:\d|\.)+/);
-            if (numberMatch && !parseFloat(numberMatch[0])) {
-                roleStr += "withoutbar";
-            }
-        }
-        break;
-      default:
-        // Otherwise, do not output the actual role.
-        roleStr = null;
-        break;
-    }
-
-    // Get the math role based on the position in the parent accessible
-    // (e.g. numerator for the first child of a mathmlfraction).
-    let mathRole = Utils.getMathRole(aAccessible);
-    if (mathRole) {
-      aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? "push" : "unshift"]({
-        string: this._getOutputName(mathRole)});
-    }
-    if (roleStr) {
-      aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? "push" : "unshift"]({
-        string: this._getOutputName(roleStr)});
-    }
-  },
-
-  /**
-   * Adds MathML menclose notations to the output.
-   * @param {Array} aOutput Output array.
-   * @param {nsIAccessible} aAccessible current accessible object.
-   */
-  _addMencloseNotations: function _addMencloseNotations(aOutput, aAccessible) {
-    let notations = Utils.getAttributes(aAccessible).notation || "longdiv";
-    aOutput[this.outputOrder === OUTPUT_DESC_FIRST ? "push" : "unshift"].apply(
-      aOutput, notations.split(" ").map(notation => {
-        return { string: this._getOutputName("notation-" + notation) };
-      }));
-  },
-
-  /**
-   * Adds an entry type attribute to the description if available.
-   * @param {Array} aOutput Output array.
-   * @param {nsIAccessible} aAccessible current accessible object.
-   * @param {String} aRoleStr aAccessible's role string.
-   */
-  _addType: function _addType(aOutput, aAccessible, aRoleStr) {
-    if (aRoleStr !== "entry") {
-      return;
-    }
-
-    let typeName = Utils.getAttributes(aAccessible)["text-input-type"];
-    // Ignore the the input type="text" case.
-    if (!typeName || typeName === "text") {
-      return;
-    }
-    aOutput.push({string: "textInputType_" + typeName});
-  },
-
-  _addState: function _addState(aOutput, aState, aRoleStr) {}, // jshint ignore:line
-
-  _addRole: function _addRole(aOutput, aAccessible, aRoleStr) {}, // jshint ignore:line
-
-  get outputOrder() {
-    if (!this._utteranceOrder) {
-      this._utteranceOrder = new PrefCache("accessibility.accessfu.utterance");
-    }
-    return typeof this._utteranceOrder.value === "number" ?
-      this._utteranceOrder.value : this.defaultOutputOrder;
-  },
-
-  _getOutputName: function _getOutputName(aName) {
-    return aName.replace(/\s/g, "");
-  },
-
-  roleRuleMap: {
-    "menubar": INCLUDE_DESC,
-    "scrollbar": INCLUDE_DESC,
-    "grip": INCLUDE_DESC,
-    "alert": INCLUDE_DESC | INCLUDE_NAME,
-    "menupopup": INCLUDE_DESC,
-    "menuitem": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "tooltip": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "columnheader": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "rowheader": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "column": NAME_FROM_SUBTREE_RULE,
-    "row": NAME_FROM_SUBTREE_RULE,
-    "cell": INCLUDE_DESC | INCLUDE_NAME,
-    "application": INCLUDE_NAME,
-    "document": INCLUDE_NAME | NAME_FROM_SUBTREE_RULE, // don't use the subtree of entire document
-    "grouping": INCLUDE_DESC | INCLUDE_NAME,
-    "toolbar": INCLUDE_DESC,
-    "table": INCLUDE_DESC | INCLUDE_NAME,
-    "link": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "helpballoon": NAME_FROM_SUBTREE_RULE,
-    "list": INCLUDE_DESC | INCLUDE_NAME,
-    "listitem": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "outline": INCLUDE_DESC,
-    "outlineitem": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "pagetab": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "graphic": INCLUDE_DESC,
-    "switch": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "pushbutton": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "checkbutton": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "radiobutton": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "buttondropdown": NAME_FROM_SUBTREE_RULE,
-    "combobox": INCLUDE_DESC | INCLUDE_VALUE,
-    "droplist": INCLUDE_DESC,
-    "progressbar": INCLUDE_DESC | INCLUDE_VALUE,
-    "slider": INCLUDE_DESC | INCLUDE_VALUE,
-    "spinbutton": INCLUDE_DESC | INCLUDE_VALUE,
-    "diagram": INCLUDE_DESC,
-    "animation": INCLUDE_DESC,
-    "equation": INCLUDE_DESC,
-    "buttonmenu": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "buttondropdowngrid": NAME_FROM_SUBTREE_RULE,
-    "pagetablist": INCLUDE_DESC,
-    "canvas": INCLUDE_DESC,
-    "check menu item": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "label": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "password text": INCLUDE_DESC,
-    "popup menu": INCLUDE_DESC,
-    "radio menu item": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "table column header": NAME_FROM_SUBTREE_RULE,
-    "table row header": NAME_FROM_SUBTREE_RULE,
-    "tear off menu item": NAME_FROM_SUBTREE_RULE,
-    "toggle button": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "parent menuitem": NAME_FROM_SUBTREE_RULE,
-    "header": INCLUDE_DESC,
-    "footer": INCLUDE_DESC,
-    "entry": INCLUDE_DESC | INCLUDE_NAME | INCLUDE_VALUE,
-    "caption": INCLUDE_DESC,
-    "document frame": INCLUDE_DESC,
-    "heading": INCLUDE_DESC,
-    "calendar": INCLUDE_DESC | INCLUDE_NAME,
-    "combobox option": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "listbox option": INCLUDE_DESC | NAME_FROM_SUBTREE_RULE,
-    "listbox rich option": NAME_FROM_SUBTREE_RULE,
-    "gridcell": NAME_FROM_SUBTREE_RULE,
-    "check rich option": NAME_FROM_SUBTREE_RULE,
-    "term": NAME_FROM_SUBTREE_RULE,
-    "definition": NAME_FROM_SUBTREE_RULE,
-    "key": NAME_FROM_SUBTREE_RULE,
-    "image map": INCLUDE_DESC,
-    "option": INCLUDE_DESC,
-    "listbox": INCLUDE_DESC,
-    "definitionlist": INCLUDE_DESC | INCLUDE_NAME,
-    "dialog": INCLUDE_DESC | INCLUDE_NAME,
-    "chrome window": IGNORE_EXPLICIT_NAME,
-    "app root": IGNORE_EXPLICIT_NAME,
-    "statusbar": NAME_FROM_SUBTREE_RULE,
-    "mathml table": INCLUDE_DESC | INCLUDE_NAME,
-    "mathml labeled row": NAME_FROM_SUBTREE_RULE,
-    "mathml table row": NAME_FROM_SUBTREE_RULE,
-    "mathml cell": INCLUDE_DESC | INCLUDE_NAME,
-    "mathml fraction": INCLUDE_DESC,
-    "mathml square root": INCLUDE_DESC,
-    "mathml root": INCLUDE_DESC,
-    "mathml enclosed": INCLUDE_DESC,
-    "mathml sub": INCLUDE_DESC,
-    "mathml sup": INCLUDE_DESC,
-    "mathml sub sup": INCLUDE_DESC,
-    "mathml under": INCLUDE_DESC,
-    "mathml over": INCLUDE_DESC,
-    "mathml under over": INCLUDE_DESC,
-    "mathml multiscripts": INCLUDE_DESC,
-    "mathml identifier": INCLUDE_DESC,
-    "mathml number": INCLUDE_DESC,
-    "mathml operator": INCLUDE_DESC,
-    "mathml text": INCLUDE_DESC,
-    "mathml string literal": INCLUDE_DESC,
-    "mathml row": INCLUDE_DESC,
-    "mathml style": INCLUDE_DESC,
-    "mathml error": INCLUDE_DESC },
-
-  mathmlRolesSet: new Set([
-    Roles.MATHML_MATH,
-    Roles.MATHML_IDENTIFIER,
-    Roles.MATHML_NUMBER,
-    Roles.MATHML_OPERATOR,
-    Roles.MATHML_TEXT,
-    Roles.MATHML_STRING_LITERAL,
-    Roles.MATHML_GLYPH,
-    Roles.MATHML_ROW,
-    Roles.MATHML_FRACTION,
-    Roles.MATHML_SQUARE_ROOT,
-    Roles.MATHML_ROOT,
-    Roles.MATHML_FENCED,
-    Roles.MATHML_ENCLOSED,
-    Roles.MATHML_STYLE,
-    Roles.MATHML_SUB,
-    Roles.MATHML_SUP,
-    Roles.MATHML_SUB_SUP,
-    Roles.MATHML_UNDER,
-    Roles.MATHML_OVER,
-    Roles.MATHML_UNDER_OVER,
-    Roles.MATHML_MULTISCRIPTS,
-    Roles.MATHML_TABLE,
-    Roles.LABELED_ROW,
-    Roles.MATHML_TABLE_ROW,
-    Roles.MATHML_CELL,
-    Roles.MATHML_ACTION,
-    Roles.MATHML_ERROR,
-    Roles.MATHML_STACK,
-    Roles.MATHML_LONG_DIVISION,
-    Roles.MATHML_STACK_GROUP,
-    Roles.MATHML_STACK_ROW,
-    Roles.MATHML_STACK_CARRIES,
-    Roles.MATHML_STACK_CARRY,
-    Roles.MATHML_STACK_LINE
-  ]),
-
-  objectOutputFunctions: {
-    _generateBaseOutput:
-      function _generateBaseOutput(aAccessible, aRoleStr, aState, aFlags) {
-        let output = [];
-
-        if (aFlags & INCLUDE_DESC) {
-          this._addState(output, aState, aRoleStr);
-          this._addType(output, aAccessible, aRoleStr);
-          this._addRole(output, aAccessible, aRoleStr);
-        }
-
-        if (aFlags & INCLUDE_VALUE && aAccessible.value.trim()) {
-          output[this.outputOrder === OUTPUT_DESC_FIRST ? "push" : "unshift"](
-            aAccessible.value);
-        }
-
-        this._addName(output, aAccessible, aFlags);
-        this._addLandmark(output, aAccessible);
-
-        return output;
-      },
-
-    label: function label(aAccessible, aRoleStr, aState, aFlags, aContext) {
-      if (aContext.isNestedControl ||
-          aContext.accessible == Utils.getEmbeddedControl(aAccessible)) {
-        // If we are on a nested control, or a nesting label,
-        // we don't need the context.
-        return [];
-      }
-
-      return this.objectOutputFunctions.defaultFunc.apply(this, arguments);
-    },
-
-    entry: function entry(aAccessible, aRoleStr, aState, aFlags) {
-      let rolestr = aState.contains(States.MULTI_LINE) ? "textarea" : "entry";
-      return this.objectOutputFunctions.defaultFunc.apply(
-        this, [aAccessible, rolestr, aState, aFlags]);
-    },
-
-    pagetab: function pagetab(aAccessible, aRoleStr, aState, aFlags) {
-      let itemno = {};
-      let itemof = {};
-      aAccessible.groupPosition({}, itemof, itemno);
-      let output = [];
-      this._addState(output, aState);
-      this._addRole(output, aAccessible, aRoleStr);
-      output.push({
-        string: "objItemOfN",
-        args: [itemno.value, itemof.value]
-      });
-
-      this._addName(output, aAccessible, aFlags);
-      this._addLandmark(output, aAccessible);
-
-      return output;
-    },
-
-    table: function table(aAccessible, aRoleStr, aState, aFlags) {
-      let output = [];
-      let table;
-      try {
-        table = aAccessible.QueryInterface(Ci.nsIAccessibleTable);
-      } catch (x) {
-        Logger.logException(x);
-        return output;
-      } finally {
-        // Check if it's a layout table, and bail out if true.
-        // We don't want to speak any table information for layout tables.
-        if (table.isProbablyForLayout()) {
-          return output;
-        }
-        this._addRole(output, aAccessible, aRoleStr);
-        output.push.call(output, {
-          string: this._getOutputName("tblColumnInfo"),
-          count: table.columnCount
-        }, {
-          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;
-    },
-
-    // Use the table output functions for MathML tabular elements.
-    mathmltable: function mathmltable() {
-      return this.objectOutputFunctions.table.apply(this, arguments);
-    },
-
-    mathmlcell: function mathmlcell() {
-      return this.objectOutputFunctions.cell.apply(this, arguments);
-    },
-
-    mathmlenclosed: function mathmlenclosed(aAccessible, aRoleStr, aState,
-                                            aFlags, aContext) {
-      let output = this.objectOutputFunctions.defaultFunc.
-        apply(this, [aAccessible, aRoleStr, aState, aFlags, aContext]);
-      this._addMencloseNotations(output, aAccessible);
-      return output;
-    }
-  }
-};
-
-/**
- * Generates speech utterances from objects, actions and state changes.
- * An utterance is an array of speech data.
- *
- * It should not be assumed that flattening an utterance array would create a
- * gramatically correct sentence. For example, {@link genForObject} might
- * return: ['graphic', 'Welcome to my home page'].
- * Each string element in an utterance should be gramatically correct in itself.
- * Another example from {@link genForObject}: ['list item 2 of 5', 'Alabama'].
- *
- * An utterance is ordered from the least to the most important. Speaking the
- * last string usually makes sense, but speaking the first often won't.
- * For example {@link genForAction} might return ['button', 'clicked'] for a
- * clicked event. Speaking only 'clicked' makes sense. Speaking 'button' does
- * not.
- */
-var UtteranceGenerator = {  // jshint ignore:line
-  __proto__: OutputGenerator, // jshint ignore:line
-
-  gActionMap: {
-    jump: "jumpAction",
-    press: "pressAction",
-    check: "checkAction",
-    uncheck: "uncheckAction",
-    on: "onAction",
-    off: "offAction",
-    select: "selectAction",
-    unselect: "unselectAction",
-    open: "openAction",
-    close: "closeAction",
-    switch: "switchAction",
-    click: "clickAction",
-    collapse: "collapseAction",
-    expand: "expandAction",
-    activate: "activateAction",
-    cycle: "cycleAction"
-  },
-
-  // TODO: May become more verbose in the future.
-  genForAction: function genForAction(aObject, aActionName) {
-    return [{string: this.gActionMap[aActionName]}];
-  },
-
-  genForLiveRegion:
-    function genForLiveRegion(aContext, aIsHide, aModifiedText) {
-      let utterance = [];
-      if (aIsHide) {
-        utterance.push({string: "hidden"});
-      }
-      return utterance.concat(aModifiedText || this.genForContext(aContext));
-    },
-
-  genForAnnouncement: function genForAnnouncement(aAnnouncement) {
-    return [{
-      string: aAnnouncement
-    }];
-  },
-
-  genForTabStateChange: function genForTabStateChange(aObject, aTabState) {
-    switch (aTabState) {
-      case "newtab":
-        return [{string: "tabNew"}];
-      case "loading":
-        return [{string: "tabLoading"}];
-      case "loaded":
-        return [aObject.name, {string: "tabLoaded"}];
-      case "loadstopped":
-        return [{string: "tabLoadStopped"}];
-      case "reload":
-        return [{string: "tabReload"}];
-      default:
-        return [];
-    }
-  },
-
-  genForEditingMode: function genForEditingMode(aIsEditing) {
-    return [{string: aIsEditing ? "editingMode" : "navigationMode"}];
-  },
-
-  objectOutputFunctions: {
-
-    __proto__: OutputGenerator.objectOutputFunctions, // jshint ignore:line
-
-    defaultFunc: function defaultFunc() {
-      return this.objectOutputFunctions._generateBaseOutput.apply(
-        this, arguments);
-    },
-
-    heading: function heading(aAccessible, aRoleStr, aState, aFlags) {
-      let level = {};
-      aAccessible.groupPosition(level, {}, {});
-      let utterance = [{string: "headingLevel", args: [level.value]}];
-
-      this._addName(utterance, aAccessible, aFlags);
-      this._addLandmark(utterance, aAccessible);
-
-      return utterance;
-    },
-
-    listitem: function listitem(aAccessible, aRoleStr, aState, aFlags) {
-      let itemno = {};
-      let itemof = {};
-      aAccessible.groupPosition({}, itemof, itemno);
-      let utterance = [];
-      if (itemno.value == 1) {
-        // Start of list
-        utterance.push({string: "listStart"});
-      } else if (itemno.value == itemof.value) {
-        // last item
-        utterance.push({string: "listEnd"});
-      }
-
-      this._addName(utterance, aAccessible, aFlags);
-      this._addLandmark(utterance, aAccessible);
-
-      return utterance;
-    },
-
-    list: function list(aAccessible, aRoleStr, aState, aFlags) {
-      return this._getListUtterance(aAccessible, aRoleStr, aFlags,
-        aAccessible.childCount);
-    },
-
-    definitionlist:
-      function definitionlist(aAccessible, aRoleStr, aState, aFlags) {
-        return this._getListUtterance(aAccessible, aRoleStr, aFlags,
-          aAccessible.childCount / 2);
-      },
-
-    application: function application(aAccessible, aRoleStr, aState, aFlags) {
-      // Don't utter location of applications, it gets tiring.
-      if (aAccessible.name != aAccessible.DOMNode.location) {
-        return this.objectOutputFunctions.defaultFunc.apply(this,
-          [aAccessible, aRoleStr, aState, aFlags]);
-      }
-
-      return [];
-    },
-
-    cell: function cell(aAccessible, aRoleStr, aState, aFlags, aContext) {
-      let utterance = [];
-      let cell = aContext.getCellInfo(aAccessible);
-      if (cell) {
-        let addCellChanged =
-          function addCellChanged(aUtterance, aChanged, aString, aIndex) {
-            if (aChanged) {
-              aUtterance.push({string: aString, args: [aIndex + 1]});
-            }
-          };
-        let addExtent = function addExtent(aUtterance, aExtent, aString) {
-          if (aExtent > 1) {
-            aUtterance.push({string: aString, args: [aExtent]});
-          }
-        };
-        let addHeaders = function addHeaders(aUtterance, aHeaders) {
-          if (aHeaders.length > 0) {
-            aUtterance.push.apply(aUtterance, aHeaders);
-          }
-        };
-
-        addCellChanged(utterance, cell.columnChanged, "columnInfo",
-          cell.columnIndex);
-        addCellChanged(utterance, cell.rowChanged, "rowInfo", cell.rowIndex);
-
-        addExtent(utterance, cell.columnExtent, "spansColumns");
-        addExtent(utterance, cell.rowExtent, "spansRows");
-
-        addHeaders(utterance, cell.columnHeaders);
-        addHeaders(utterance, cell.rowHeaders);
-      }
-
-      this._addName(utterance, aAccessible, aFlags);
-      this._addLandmark(utterance, aAccessible);
-
-      return utterance;
-    },
-
-    columnheader: function columnheader() {
-      return this.objectOutputFunctions.cell.apply(this, arguments);
-    },
-
-    rowheader: function rowheader() {
-      return this.objectOutputFunctions.cell.apply(this, arguments);
-    },
-
-    statictext: function statictext(aAccessible) {
-      if (Utils.isListItemDecorator(aAccessible, true)) {
-        return [];
-      }
-
-      return this.objectOutputFunctions.defaultFunc.apply(this, arguments);
-    }
-  },
-
-  _getContextStart: function _getContextStart(aContext) {
-    return aContext.newAncestry;
-  },
-
-  _addRole: function _addRole(aOutput, aAccessible, aRoleStr) {
-    if (this.mathmlRolesSet.has(aAccessible.role)) {
-      this._addMathRoles(aOutput, aAccessible, aRoleStr);
-    } else {
-      aOutput.push({string: this._getOutputName(aRoleStr)});
-    }
-  },
-
-  /**
-   * Add localized state information to output data.
-   * Note: We do not expose checked and selected states, we let TalkBack do it for us
-   * there. This is because we expose the checked information on the node info itself.
-   */
-  _addState: function _addState(aOutput, aState, aRoleStr) {
-    if (aState.contains(States.UNAVAILABLE)) {
-      aOutput.push({string: "stateUnavailable"});
-    }
-
-    if (aState.contains(States.READONLY)) {
-      aOutput.push({string: "stateReadonly"});
-    }
-
-    if (aState.contains(States.PRESSED)) {
-      aOutput.push({string: "statePressed"});
-    }
-
-    if (aState.contains(States.EXPANDABLE)) {
-      let statetr = aState.contains(States.EXPANDED) ?
-        "stateExpanded" : "stateCollapsed";
-      aOutput.push({string: statetr});
-    }
-
-    if (aState.contains(States.REQUIRED)) {
-      aOutput.push({string: "stateRequired"});
-    }
-
-    if (aState.contains(States.TRAVERSED)) {
-      aOutput.push({string: "stateTraversed"});
-    }
-
-    if (aState.contains(States.HASPOPUP)) {
-      aOutput.push({string: "stateHasPopup"});
-    }
-  },
-
-  _getListUtterance:
-    function _getListUtterance(aAccessible, aRoleStr, aFlags, aItemCount) {
-      let utterance = [];
-      this._addRole(utterance, aAccessible, aRoleStr);
-      utterance.push({
-        string: this._getOutputName("listItemsCount"),
-        count: aItemCount
-      });
-
-      this._addName(utterance, aAccessible, aFlags);
-      this._addLandmark(utterance, aAccessible);
-
-      return utterance;
-    }
-};
deleted file mode 100644
--- a/accessible/jsat/Presentation.jsm
+++ /dev/null
@@ -1,332 +0,0 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-/* exported Presentation */
-
-"use strict";
-
-ChromeUtils.import("resource://gre/modules/accessibility/Utils.jsm");
-ChromeUtils.defineModuleGetter(this, "PivotContext", // jshint ignore:line
-  "resource://gre/modules/accessibility/Utils.jsm");
-ChromeUtils.defineModuleGetter(this, "UtteranceGenerator", // jshint ignore:line
-  "resource://gre/modules/accessibility/OutputGenerator.jsm");
-ChromeUtils.defineModuleGetter(this, "States", // jshint ignore:line
-  "resource://gre/modules/accessibility/Constants.jsm");
-ChromeUtils.defineModuleGetter(this, "Roles", // jshint ignore:line
-  "resource://gre/modules/accessibility/Constants.jsm");
-ChromeUtils.defineModuleGetter(this, "AndroidEvents", // jshint ignore:line
-  "resource://gre/modules/accessibility/Constants.jsm");
-
-var EXPORTED_SYMBOLS = ["Presentation"]; // jshint ignore:line
-
-const EDIT_TEXT_ROLES = new Set([
-  Roles.SPINBUTTON, Roles.PASSWORD_TEXT,
-  Roles.AUTOCOMPLETE, Roles.ENTRY, Roles.EDITCOMBOBOX]);
-
-class AndroidPresentor {
-  constructor() {
-    this.type = "Android";
-    this.displayedAccessibles = new WeakMap();
-  }
-
-  /**
-   * The virtual cursor's position changed.
-   * @param {PivotContext} aContext the context object for the new pivot
-   *   position.
-   * @param {int} aReason the reason for the pivot change.
-   *   See nsIAccessiblePivot.
-   * @param {bool} aBoundaryType the boundary type for the text movement
-   * or NO_BOUNDARY if it was not a text movement. See nsIAccessiblePivot.
-   */
-  pivotChanged(aPosition, aOldPosition, aStartOffset, aEndOffset, aReason, aBoundaryType) {
-    let context = new PivotContext(
-      aPosition, aOldPosition, aStartOffset, aEndOffset);
-    if (!context.accessible) {
-      return null;
-    }
-
-    let androidEvents = [];
-
-    const isExploreByTouch = aReason == Ci.nsIAccessiblePivot.REASON_POINT;
-
-    if (isExploreByTouch) {
-      // This isn't really used by TalkBack so this is a half-hearted attempt
-      // for now.
-      androidEvents.push({eventType: AndroidEvents.VIEW_HOVER_EXIT, text: []});
-    }
-
-    if (aPosition != aOldPosition) {
-      let info = this._infoFromContext(context);
-      let eventType = isExploreByTouch ?
-        AndroidEvents.VIEW_HOVER_ENTER :
-        AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED;
-      androidEvents.push({...info, eventType});
-
-      try {
-        context.accessibleForBounds.scrollTo(
-          Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE);
-      } catch (e) {}
-    }
-
-    if (aBoundaryType != Ci.nsIAccessiblePivot.NO_BOUNDARY) {
-      const adjustedText = context.textAndAdjustedOffsets;
-
-      androidEvents.push({
-        eventType: AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-        text: [adjustedText.text],
-        fromIndex: adjustedText.startOffset,
-        toIndex: adjustedText.endOffset
-      });
-
-      aPosition.QueryInterface(Ci.nsIAccessibleText).scrollSubstringTo(
-        aStartOffset, aEndOffset,
-        Ci.nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE);
-    }
-
-    if (context.accessible) {
-      this.displayedAccessibles.set(context.accessible.document.window, context);
-    }
-
-    return androidEvents;
-  }
-
-  focused(aObject) {
-    let info = this._infoFromContext(
-      new PivotContext(aObject, null, -1, -1, true, false));
-    return [{ eventType: AndroidEvents.VIEW_FOCUSED, ...info }];
-  }
-
-  /**
-   * An object's check action has been invoked.
-   * Note: Checkable objects use TalkBack's text derived from the event state, so we don't
-   * populate the text here.
-   * @param {nsIAccessible} aAccessible the object that has been invoked.
-   */
-  checked(aAccessible) {
-    return [{
-      eventType: AndroidEvents.VIEW_CLICKED,
-      checked: Utils.getState(aAccessible).contains(States.CHECKED)
-    }];
-  }
-
-  /**
-   * An object's select action has been invoked.
-   * @param {nsIAccessible} aAccessible the object that has been invoked.
-   */
-  selected(aAccessible) {
-    return [{
-      eventType: AndroidEvents.VIEW_SELECTED,
-      selected: Utils.getState(aAccessible).contains(States.SELECTED)
-    }];
-  }
-
-  /**
-   * An object's action has been invoked.
-   */
-  actionInvoked() {
-    return [{ eventType: AndroidEvents.VIEW_CLICKED }];
-  }
-
-  /**
-   * Text has changed, either by the user or by the system. TODO.
-   */
-  textChanged(aAccessible, aIsInserted, aStart, aLength, aText, aModifiedText) {
-    let androidEvent = {
-      eventType: AndroidEvents.VIEW_TEXT_CHANGED,
-      text: [aText],
-      fromIndex: aStart,
-      removedCount: 0,
-      addedCount: 0
-    };
-
-    if (aIsInserted) {
-      androidEvent.addedCount = aLength;
-      androidEvent.beforeText =
-        aText.substring(0, aStart) + aText.substring(aStart + aLength);
-    } else {
-      androidEvent.removedCount = aLength;
-      androidEvent.beforeText =
-        aText.substring(0, aStart) + aModifiedText + aText.substring(aStart);
-    }
-
-    return [androidEvent];
-  }
-
-  /**
-   * Text selection has changed. TODO.
-   */
-  textSelectionChanged(aText, aStart, aEnd, aOldStart, aOldEnd, aIsFromUserInput) {
-    let androidEvents = [];
-
-    if (aIsFromUserInput) {
-      let [from, to] = aOldStart < aStart ?
-        [aOldStart, aStart] : [aStart, aOldStart];
-      androidEvents.push({
-        eventType: AndroidEvents.VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY,
-        text: [aText],
-        fromIndex: from,
-        toIndex: to
-      });
-    } else {
-      androidEvents.push({
-        eventType: AndroidEvents.VIEW_TEXT_SELECTION_CHANGED,
-        text: [aText],
-        fromIndex: aStart,
-        toIndex: aEnd,
-        itemCount: aText.length
-      });
-    }
-
-    return androidEvents;
-  }
-
-  /**
-   * Selection has changed.
-   * XXX: Implement android event?
-   * @param {nsIAccessible} aObject the object that has been selected.
-   */
-  selectionChanged(aObject) {
-    return ["todo.selection-changed"];
-  }
-
-  /**
-   * Name has changed.
-   * XXX: Implement android event?
-   * @param {nsIAccessible} aAccessible the object whose value has changed.
-   */
-  nameChanged(aAccessible) {
-    return ["todo.name-changed"];
-  }
-
-  /**
-   * Value has changed.
-   * XXX: Implement android event?
-   * @param {nsIAccessible} aAccessible the object whose value has changed.
-   */
-  valueChanged(aAccessible) {
-    return ["todo.value-changed"];
-  }
-
-  /**
-   * The tab, or the tab's document state has changed.
-   * @param {nsIAccessible} aDocObj the tab document accessible that has had its
-   *    state changed, or null if the tab has no associated document yet.
-   * @param {string} aPageState the state name for the tab, valid states are:
-   *    'newtab', 'loading', 'newdoc', 'loaded', 'stopped', and 'reload'.
-   */
-  tabStateChanged(aDocObj, aPageState) {
-    return this.announce(
-      UtteranceGenerator.genForTabStateChange(aDocObj, aPageState));
-  }
-
-  /**
-   * The viewport has changed because of scroll.
-   * @param {Window} aWindow window of viewport that changed.
-   */
-  viewportScrolled(aWindow) {
-    const { windowUtils, devicePixelRatio } = aWindow;
-    const resolution = { value: 1 };
-    windowUtils.getResolution(resolution);
-    const scale = devicePixelRatio * resolution.value;
-    return [{
-      eventType: AndroidEvents.VIEW_SCROLLED,
-      scrollX: aWindow.scrollX * scale,
-      scrollY: aWindow.scrollY * scale,
-      maxScrollX: aWindow.scrollMaxX * scale,
-      maxScrollY: aWindow.scrollMaxY * scale,
-    }];
-  }
-
-  /**
-   * The viewport has changed, either a pan, zoom, or landscape/portrait toggle.
-   * @param {Window} aWindow window of viewport that changed.
-   */
-  viewportChanged(aWindow) {
-    const currentContext = this.displayedAccessibles.get(aWindow);
-    if (!currentContext) {
-      return;
-    }
-
-    const currentAcc = currentContext.accessibleForBounds;
-    if (Utils.isAliveAndVisible(currentAcc)) {
-      return [{
-        eventType: AndroidEvents.WINDOW_CONTENT_CHANGED,
-        bounds: Utils.getBounds(currentAcc)
-      }];
-    }
-  }
-
-  /**
-   * Announce something. Typically an app state change.
-   */
-  announce(aAnnouncement) {
-    let localizedAnnouncement = Utils.localize(aAnnouncement).join(" ");
-    return [{
-      eventType: AndroidEvents.ANNOUNCEMENT,
-      text: [localizedAnnouncement],
-      addedCount: localizedAnnouncement.length,
-      removedCount: 0,
-      fromIndex: 0
-    }];
-  }
-
-
-  /**
-   * User tried to move cursor forward or backward with no success.
-   * @param {string} aMoveMethod move method that was used (eg. 'moveNext').
-   */
-  noMove(aMoveMethod) {
-    return [{
-      eventType: AndroidEvents.VIEW_ACCESSIBILITY_FOCUSED,
-      exitView: aMoveMethod,
-      text: [""]
-    }];
-  }
-
-  /**
-   * Announce a live region.
-   * @param  {PivotContext} aContext context object for an accessible.
-   * @param  {boolean} aIsPolite A politeness level for a live region.
-   * @param  {boolean} aIsHide An indicator of hide/remove event.
-   * @param  {string} aModifiedText Optional modified text.
-   */
-  liveRegion(aAccessible, aIsPolite, aIsHide, aModifiedText) {
-    let context = !aModifiedText ?
-      new PivotContext(aAccessible, null, -1, -1, true, !!aIsHide) : null;
-    return this.announce(
-      UtteranceGenerator.genForLiveRegion(context, aIsHide, aModifiedText));
-  }
-
-  _infoFromContext(aContext) {
-    const state = Utils.getState(aContext.accessible);
-    const info = {
-      bounds: aContext.bounds,
-      focusable: state.contains(States.FOCUSABLE),
-      focused: state.contains(States.FOCUSED),
-      clickable: aContext.accessible.actionCount > 0,
-      checkable: state.contains(States.CHECKABLE),
-      checked: state.contains(States.CHECKED),
-      editable: state.contains(States.EDITABLE),
-      selected: state.contains(States.SELECTED)
-    };
-
-    if (EDIT_TEXT_ROLES.has(aContext.accessible.role)) {
-      let textAcc = aContext.accessible.QueryInterface(Ci.nsIAccessibleText);
-      return {
-        ...info,
-        className: "android.widget.EditText",
-        hint: aContext.accessible.name,
-        text: [textAcc.getText(0, -1)]
-      };
-    }
-
-    return {
-      ...info,
-      className: "android.view.View",
-      text: Utils.localize(UtteranceGenerator.genForContext(aContext)),
-    };
-  }
-}
-
-const Presentation = new AndroidPresentor();
--- a/accessible/jsat/moz.build
+++ b/accessible/jsat/moz.build
@@ -4,15 +4,13 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 EXTRA_JS_MODULES.accessibility += [
     'AccessFu.jsm',
     'Constants.jsm',
     'ContentControl.jsm',
     'EventManager.jsm',
-    'OutputGenerator.jsm',
-    'Presentation.jsm',
     'Traversal.jsm',
     'Utils.jsm'
 ]
 
 JAR_MANIFESTS += ['jar.mn']
--- a/accessible/tests/mochitest/jsat/a11y.ini
+++ b/accessible/tests/mochitest/jsat/a11y.ini
@@ -5,24 +5,33 @@ support-files =
   doc_traversal.html
   doc_content_integration.html
   doc_content_text.html
   !/accessible/tests/mochitest/*.js
   !/accessible/tests/mochitest/moz.png
 
 [test_alive.html]
 [test_content_integration.html]
+skip-if = true
 [test_explicit_names.html]
+skip-if = true
 [test_hints.html]
+skip-if = true
 [test_landmarks.html]
+skip-if = true
 [test_live_regions.html]
+skip-if = true
 [test_output_mathml.html]
+skip-if = true
 [test_output.html]
+skip-if = true
 [test_tables.html]
+skip-if = true
 [test_text_editable_navigation.html]
-skip-if = (verify && !debug && (os == 'linux'))
+skip-if = true
 [test_text_editing.html]
-skip-if = (verify && !debug && (os == 'linux'))
+skip-if = true
 [test_text_navigation_focus.html]
-skip-if = (verify && !debug && (os == 'linux'))
+skip-if = true
 [test_text_navigation.html]
+skip-if = true
 [test_traversal.html]
 [test_traversal_helper.html]
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1498,21 +1498,31 @@ pref("browser.ping-centre.log", false);
 pref("browser.ping-centre.staging.endpoint", "https://onyx_tiles.stage.mozaws.net/v3/links/ping-centre");
 pref("browser.ping-centre.production.endpoint", "https://tiles.services.mozilla.com/v3/links/ping-centre");
 
 // Enable GMP support in the addon manager.
 pref("media.gmp-provider.enabled", true);
 
 pref("browser.contentblocking.allowlist.storage.enabled", true);
 
+#ifdef NIGHTLY_BUILD
 pref("browser.contentblocking.global-toggle.enabled", true);
+#else
+pref("browser.contentblocking.global-toggle.enabled", false);
+#endif
 
 // Define a set of default features for the Content Blocking UI
+#ifdef EARLY_BETA_OR_EARLIER
 pref("browser.contentblocking.fastblock.ui.enabled", true);
 pref("browser.contentblocking.fastblock.control-center.ui.enabled", true);
+#else
+pref("browser.contentblocking.fastblock.ui.enabled", false);
+pref("browser.contentblocking.fastblock.control-center.ui.enabled", false);
+#endif
+
 pref("browser.contentblocking.trackingprotection.ui.enabled", true);
 pref("browser.contentblocking.trackingprotection.control-center.ui.enabled", true);
 pref("browser.contentblocking.rejecttrackers.ui.enabled", true);
 pref("browser.contentblocking.rejecttrackers.ui.recommended", true);
 pref("browser.contentblocking.rejecttrackers.control-center.ui.enabled", true);
 pref("browser.contentblocking.cookies-site-data.ui.reject-trackers.recommended", true);
 pref("browser.contentblocking.cookies-site-data.ui.reject-trackers.enabled", true);
 
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -1426,16 +1426,17 @@ var BookmarkingUI = {
     CustomizableUI.removeListener(this);
 
     this.star.removeEventListener("mouseover", this);
 
     this._uninitView();
 
     if (this._hasBookmarksObserver) {
       PlacesUtils.bookmarks.removeObserver(this);
+      PlacesUtils.observers.removeListener(["bookmark-added"], this.handlePlacesEvents);
     }
 
     if (this._pendingUpdate) {
       delete this._pendingUpdate;
     }
   },
 
   onLocationChange: function BUI_onLocationChange() {
@@ -1456,31 +1457,33 @@ var BookmarkingUI = {
 
     PlacesUtils.bookmarks.fetch({url: this._uri}, b => guids.add(b.guid), { concurrent: true })
       .catch(Cu.reportError)
       .then(() => {
          if (pendingUpdate != this._pendingUpdate) {
            return;
          }
 
-         // It's possible that onItemAdded gets called before the async statement
+         // It's possible that "bookmark-added" gets called before the async statement
          // calls back.  For such an edge case, retain all unique entries from the
          // array.
          if (this._itemGuids.size > 0) {
            this._itemGuids = new Set(...this._itemGuids, ...guids);
          } else {
            this._itemGuids = guids;
          }
 
          this._updateStar();
 
          // Start observing bookmarks if needed.
          if (!this._hasBookmarksObserver) {
            try {
              PlacesUtils.bookmarks.addObserver(this);
+            this.handlePlacesEvents = this.handlePlacesEvents.bind(this);
+            PlacesUtils.observers.addListener(["bookmark-added"], this.handlePlacesEvents);
              this._hasBookmarksObserver = true;
            } catch (ex) {
              Cu.reportError("BookmarkingUI failed adding a bookmarks observer: " + ex);
            }
          }
 
          delete this._pendingUpdate;
        });
@@ -1705,30 +1708,32 @@ var BookmarkingUI = {
     } else {
       // Move it back to the palette.
       CustomizableUI.removeWidgetFromArea(this.BOOKMARK_BUTTON_ID);
     }
     triggerNode.setAttribute("checked", !placement);
     updateToggleControlLabel(triggerNode);
   },
 
-  // nsINavBookmarkObserver
-  onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded, aGuid) {
-    if (aURI && aURI.equals(this._uri)) {
-      // If a new bookmark has been added to the tracked uri, register it.
-      if (!this._itemGuids.has(aGuid)) {
-        this._itemGuids.add(aGuid);
-        // Only need to update the UI if it wasn't marked as starred before:
-        if (this._itemGuids.size == 1) {
-          this._updateStar();
+  handlePlacesEvents(aEvents) {
+    // Only need to update the UI if it wasn't marked as starred before:
+    if (this._itemGuids.size == 0) {
+      for (let {url, guid} of aEvents) {
+        if (url && url == this._uri.spec) {
+          // If a new bookmark has been added to the tracked uri, register it.
+          if (!this._itemGuids.has(guid)) {
+            this._itemGuids.add(guid);
+            this._updateStar();
+          }
         }
       }
     }
   },
 
+  // nsINavBookmarkObserver
   onItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGuid) {
     // If one of the tracked bookmarks has been removed, unregister it.
     if (this._itemGuids.has(aGuid)) {
       this._itemGuids.delete(aGuid);
       // Only need to update the UI if the page is no longer starred
       if (this._itemGuids.size == 0) {
         this._updateStar();
       }
--- a/browser/base/content/browser-siteIdentity.js
+++ b/browser/base/content/browser-siteIdentity.js
@@ -110,25 +110,24 @@ var gIdentityHandler = {
   get _identityPopupMultiView() {
     delete this._identityPopupMultiView;
     return this._identityPopupMultiView = document.getElementById("identity-popup-multiView");
   },
   get _identityPopupMainView() {
     delete this._identityPopupMainView;
     return this._identityPopupMainView = document.getElementById("identity-popup-mainView");
   },
-  get _identityPopupContentHosts() {
-    delete this._identityPopupContentHosts;
-    return this._identityPopupContentHosts =
-      [...document.querySelectorAll(".identity-popup-host")];
+  get _identityPopupMainViewHeaderLabel() {
+    delete this._identityPopupMainViewHeaderLabel;
+    return this._identityPopupMainViewHeaderLabel =
+      document.getElementById("identity-popup-mainView-panel-header-span");
   },
-  get _identityPopupContentHostless() {
-    delete this._identityPopupContentHostless;
-    return this._identityPopupContentHostless =
-      [...document.querySelectorAll(".identity-popup-hostless")];
+  get _identityPopupContentHost() {
+    delete this._identityPopupContentHost;
+    return this._identityPopupContentHost = document.getElementById("identity-popup-host");
   },
   get _identityPopupContentOwner() {
     delete this._identityPopupContentOwner;
     return this._identityPopupContentOwner =
       document.getElementById("identity-popup-content-owner");
   },
   get _identityPopupContentSupp() {
     delete this._identityPopupContentSupp;
@@ -698,34 +697,30 @@ var gIdentityHandler = {
       updateAttribute(element, "isbroken", this._isBroken);
     }
 
     // Initialize the optional strings to empty values
     let supplemental = "";
     let verifier = "";
     let host = "";
     let owner = "";
-    let hostless = false;
 
     try {
       host = this.getEffectiveHost();
     } catch (e) {
       // Some URIs might have no hosts.
     }
 
+    if (this._pageExtensionPolicy) {
+      host = this._pageExtensionPolicy.name;
+    }
+
     // Fallback for special protocols.
     if (!host) {
       host = this._uri.specIgnoringRef;
-      // Special URIs without a host (eg, about:) should crop the end so
-      // the protocol can be seen.
-      hostless = true;
-    }
-
-    if (this._pageExtensionPolicy) {
-      host = this._pageExtensionPolicy.name;
     }
 
     // Fill in the CA name if we have a valid TLS certificate.
     if (this._isSecure || this._isCertUserOverridden) {
       verifier = this._identityIconLabels.tooltipText;
     }
 
     // Fill in organization information if we have a valid EV certificate.
@@ -742,24 +737,19 @@ var gIdentityHandler = {
                                                             [iData.state, iData.country]);
       else if (iData.state) // State only
         supplemental += iData.state;
       else if (iData.country) // Country only
         supplemental += iData.country;
     }
 
     // Push the appropriate strings out to the UI.
-    this._identityPopupContentHosts.forEach((el) => {
-      el.textContent = host;
-      el.hidden = hostless;
-    });
-    this._identityPopupContentHostless.forEach((el) => {
-      el.setAttribute("value", host);
-      el.hidden = !hostless;
-    });
+    this._identityPopupMainViewHeaderLabel.textContent =
+      gNavigatorBundle.getFormattedString("identity.headerWithHost", [host]);
+    this._identityPopupContentHost.textContent = host;
     this._identityPopupContentOwner.textContent = owner;
     this._identityPopupContentSupp.textContent = supplemental;
     this._identityPopupContentVerif.textContent = verifier;
 
     // Update per-site permissions section.
     this.updateSitePermissions();
   },
 
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -2756,17 +2756,20 @@ window._gBrowser = {
     var isLastTab = (this.tabs.length - this._removingTabs.length == 1);
     let windowUtils = window.windowUtils;
     // We have to sample the tab width now, since _beginRemoveTab might
     // end up modifying the DOM in such a way that aTab gets a new
     // frame created for it (for example, by updating the visually selected
     // state).
     let tabWidth = windowUtils.getBoundsWithoutFlushing(aTab).width;
 
-    if (!this._beginRemoveTab(aTab, null, null, true, skipPermitUnload)) {
+    if (!this._beginRemoveTab(aTab, {
+          closeWindowFastpath: true,
+          skipPermitUnload,
+        })) {
       TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_ANIM_MS", aTab);
       TelemetryStopwatch.cancel("FX_TAB_CLOSE_TIME_NO_ANIM_MS", aTab);
       return;
     }
 
     if (!aTab.pinned && !aTab.hidden && aTab._fullyOpen && byMouse)
       this.tabContainer._lockTabSizing(aTab, tabWidth);
     else
@@ -2804,23 +2807,29 @@ window._gBrowser = {
 
   _hasBeforeUnload(aTab) {
     let browser = aTab.linkedBrowser;
     return browser.isRemoteBrowser && browser.frameLoader &&
            browser.frameLoader.tabParent &&
            browser.frameLoader.tabParent.hasBeforeUnload;
   },
 
-  _beginRemoveTab(aTab, aAdoptedByTab, aCloseWindowWithLastTab, aCloseWindowFastpath, aSkipPermitUnload) {
+  _beginRemoveTab(aTab, {
+    adoptedByTab,
+    closeWindowWithLastTab,
+    closeWindowFastpath,
+    skipPermitUnload,
+  } = {}) {
     if (aTab.closing ||
-      this._windowIsClosing)
+        this._windowIsClosing) {
       return false;
+    }
 
     var browser = this.getBrowserForTab(aTab);
-    if (!aSkipPermitUnload && !aAdoptedByTab &&
+    if (!skipPermitUnload && !adoptedByTab &&
         aTab.linkedPanel && !aTab._pendingPermitUnload &&
         (!browser.isRemoteBrowser || this._hasBeforeUnload(aTab))) {
       TelemetryStopwatch.start("FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS", aTab);
 
       // We need to block while calling permitUnload() because it
       // processes the event queue and may lead to another removeTab()
       // call before permitUnload() returns.
       aTab._pendingPermitUnload = true;
@@ -2844,44 +2853,44 @@ window._gBrowser = {
       this._tabLayerCache.splice(tabCacheIndex, 1);
     }
 
     this._blurTab(aTab);
 
     var closeWindow = false;
     var newTab = false;
     if (this.tabs.length - this._removingTabs.length == 1) {
-      closeWindow = aCloseWindowWithLastTab != null ? aCloseWindowWithLastTab :
+      closeWindow = closeWindowWithLastTab != null ? closeWindowWithLastTab :
         !window.toolbar.visible ||
         Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab");
 
       if (closeWindow) {
         // We've already called beforeunload on all the relevant tabs if we get here,
         // so avoid calling it again:
         window.skipNextCanClose = true;
       }
 
       // Closing the tab and replacing it with a blank one is notably slower
       // than closing the window right away. If the caller opts in, take
       // the fast path.
       if (closeWindow &&
-          aCloseWindowFastpath &&
+          closeWindowFastpath &&
           this._removingTabs.length == 0) {
         // This call actually closes the window, unless the user
         // cancels the operation.  We are finished here in both cases.
         this._windowIsClosing = window.closeWindow(true, window.warnAboutClosingWindow);
         return false;
       }
 
       newTab = true;
     }
     aTab._endRemoveArgs = [closeWindow, newTab];
 
     // swapBrowsersAndCloseOther will take care of closing the window without animation.
-    if (closeWindow && aAdoptedByTab) {
+    if (closeWindow && adoptedByTab) {
       // Remove the tab's filter and progress listener to avoid leaking.
       if (aTab.linkedPanel) {
         const filter = this._tabFilters.get(aTab);
         browser.webProgress.removeProgressListener(filter);
         const listener = this._tabListeners.get(aTab);
         filter.removeProgressListener(listener);
         listener.destroy();
         this._tabListeners.delete(aTab);
@@ -2894,17 +2903,17 @@ window._gBrowser = {
       // If the opening tab animation hasn't finished before we start closing the
       // tab, decrement the animation count since _handleNewTab will not get called.
       this.tabAnimationsInProgress--;
     }
 
     this.tabAnimationsInProgress++;
 
     // Mute audio immediately to improve perceived speed of tab closure.
-    if (!aAdoptedByTab && aTab.hasAttribute("soundplaying")) {
+    if (!adoptedByTab && aTab.hasAttribute("soundplaying")) {
       // Don't persist the muted state as this wasn't a user action.
       // This lets undo-close-tab return it to an unmuted state.
       aTab.linkedBrowser.mute(true);
     }
 
     aTab.closing = true;
     this._removingTabs.push(aTab);
     this._visibleTabs = null; // invalidate cache
@@ -2919,43 +2928,43 @@ window._gBrowser = {
       });
     else
       TabBarVisibility.update();
 
     // We're committed to closing the tab now.
     // Dispatch a notification.
     // We dispatch it before any teardown so that event listeners can
     // inspect the tab that's about to close.
-    var evt = new CustomEvent("TabClose", { bubbles: true, detail: { adoptedBy: aAdoptedByTab } });
+    let evt = new CustomEvent("TabClose", { bubbles: true, detail: { adoptedBy: adoptedByTab } });
     aTab.dispatchEvent(evt);
 
     if (this.tabs.length == 2) {
       // We're closing one of our two open tabs, inform the other tab that its
       // sibling is going away.
       window.messageManager
             .broadcastAsyncMessage("Browser:HasSiblings", false);
     }
 
     if (aTab.linkedPanel) {
-      if (!aAdoptedByTab && !gMultiProcessBrowser) {
+      if (!adoptedByTab && !gMultiProcessBrowser) {
         // Prevent this tab from showing further dialogs, since we're closing it
         browser.contentWindow.windowUtils.disableDialogs();
       }
 
       // Remove the tab's filter and progress listener.
       const filter = this._tabFilters.get(aTab);
 
       browser.webProgress.removeProgressListener(filter);
 
       const listener = this._tabListeners.get(aTab);
       filter.removeProgressListener(listener);
       listener.destroy();
     }
 
-    if (browser.registeredOpenURI && !aAdoptedByTab) {
+    if (browser.registeredOpenURI && !adoptedByTab) {
       let userContextId = browser.getAttribute("usercontextid") || 0;
       this.UrlbarProviderOpenTabs.unregisterOpenTab(browser.registeredOpenURI.spec,
                                                     userContextId);
       delete browser.registeredOpenURI;
     }
 
     // We are no longer the primary content area.
     browser.removeAttribute("primary");
@@ -3131,29 +3140,35 @@ window._gBrowser = {
 
     return tab;
   },
 
   _blurTab(aTab) {
     this.selectedTab = this._findTabToBlurTo(aTab);
   },
 
+  /**
+   * @returns {boolean}
+   *   False if swapping isn't permitted, true otherwise.
+   */
   swapBrowsersAndCloseOther(aOurTab, aOtherTab) {
     // Do not allow transfering a private tab to a non-private window
     // and vice versa.
     if (PrivateBrowsingUtils.isWindowPrivate(window) !=
-      PrivateBrowsingUtils.isWindowPrivate(aOtherTab.ownerGlobal))
-      return;
+        PrivateBrowsingUtils.isWindowPrivate(aOtherTab.ownerGlobal)) {
+      return false;
+    }
 
     let ourBrowser = this.getBrowserForTab(aOurTab);
     let otherBrowser = aOtherTab.linkedBrowser;
 
     // Can't swap between chrome and content processes.
-    if (ourBrowser.isRemoteBrowser != otherBrowser.isRemoteBrowser)
-      return;
+    if (ourBrowser.isRemoteBrowser != otherBrowser.isRemoteBrowser) {
+      return false;
+    }
 
     // Keep the userContextId if set on other browser
     if (otherBrowser.hasAttribute("usercontextid")) {
       ourBrowser.setAttribute("usercontextid", otherBrowser.getAttribute("usercontextid"));
     }
 
     // That's gBrowser for the other window, not the tab's browser!
     var remoteBrowser = aOtherTab.ownerGlobal.gBrowser;
@@ -3168,18 +3183,22 @@ window._gBrowser = {
       aOtherTab._soundPlayingAttrRemovalTimer = 0;
       aOtherTab.removeAttribute("soundplaying");
       remoteBrowser._tabAttrModified(aOtherTab, ["soundplaying"]);
     }
 
     // First, start teardown of the other browser.  Make sure to not
     // fire the beforeunload event in the process.  Close the other
     // window if this was its last tab.
-    if (!remoteBrowser._beginRemoveTab(aOtherTab, aOurTab, true))
-      return;
+    if (!remoteBrowser._beginRemoveTab(aOtherTab, {
+          adoptedByTab: aOurTab,
+          closeWindowWithLastTab: true,
+        })) {
+      return false;
+    }
 
     // If this is the last tab of the window, hide the window
     // immediately without animation before the docshell swap, to avoid
     // about:blank being painted.
     let [closeWindow] = aOtherTab._endRemoveArgs;
     if (closeWindow) {
       let win = aOtherTab.ownerGlobal;
       win.windowUtils.suppressAnimation(true);
@@ -3274,16 +3293,18 @@ window._gBrowser = {
     // If the tab was already selected (this happpens in the scenario
     // of replaceTabWithWindow), notify onLocationChange, etc.
     if (aOurTab.selected)
       this.updateCurrentBrowser(true);
 
     if (modifiedAttrs.length) {
       this._tabAttrModified(aOurTab, modifiedAttrs);
     }
+
+    return true;
   },
 
   swapBrowsers(aOurTab, aOtherTab, aFlags) {
     let otherBrowser = aOtherTab.linkedBrowser;
     let otherTabBrowser = otherBrowser.getTabBrowser();
 
     // We aren't closing the other tab so, we also need to swap its tablisteners.
     let filter = otherTabBrowser._tabFilters.get(aOtherTab);
@@ -3669,16 +3690,19 @@ window._gBrowser = {
     if (nextTab)
       this.moveTabTo(this.selectedTab, nextTab._tPos);
     else if (this.arrowKeysShouldWrap)
       this.moveTabToStart();
   },
 
   /**
    * Adopts a tab from another browser window, and inserts it at aIndex
+   *
+   * @returns {object}
+   *    The new tab in the current window, null if the tab couldn't be adopted.
    */
   adoptTab(aTab, aIndex, aSelectTab) {
     // Swap the dropped tab with a new one we create and then close
     // it in the other window (making it seem to have moved between
     // windows). We also ensure that the tab we create to swap into has
     // the same remote type and process as the one we're swapping in.
     // This makes sure we don't get a short-lived process for the new tab.
     let linkedBrowser = aTab.linkedBrowser;
@@ -3697,39 +3721,33 @@ window._gBrowser = {
 
     if (aTab.hasAttribute("usercontextid")) {
       // new tab must have the same usercontextid as the old one
       params.userContextId = aTab.getAttribute("usercontextid");
     }
     let newTab = this.addWebTab("about:blank", params);
     let newBrowser = this.getBrowserForTab(newTab);
 
+    aTab.parentNode._finishAnimateTabMove();
+
     // Stop the about:blank load.
     newBrowser.stop();
     // Make sure it has a docshell.
     newBrowser.docShell;
 
-    // We need to select the tab before calling swapBrowsersAndCloseOther
-    // so that window.content in chrome windows points to the right tab
-    // when pagehide/show events are fired. This is no longer necessary
-    // for any exiting browser code, but it may be necessary for add-on
-    // compatibility.
+    if (!this.swapBrowsersAndCloseOther(newTab, aTab)) {
+      // Swapping wasn't permitted. Bail out.
+      this.removeTab(newTab);
+      return null;
+    }
+
     if (aSelectTab) {
       this.selectedTab = newTab;
     }
 
-    aTab.parentNode._finishAnimateTabMove();
-    this.swapBrowsersAndCloseOther(newTab, aTab);
-
-    if (aSelectTab) {
-      // Call updateCurrentBrowser to make sure the URL bar is up to date
-      // for our new tab after we've done swapBrowsersAndCloseOther.
-      this.updateCurrentBrowser(true);
-    }
-
     return newTab;
   },
 
   moveTabBackward() {
     let previousTab = this.selectedTab.previousElementSibling;
     while (previousTab && previousTab.hidden)
       previousTab = previousTab.previousElementSibling;
 
--- a/browser/base/content/test/general/browser_bookmark_titles.js
+++ b/browser/base/content/test/general/browser_bookmark_titles.js
@@ -73,18 +73,19 @@ add_task(async function check_default_bo
 });
 
 // Bookmark the current page and confirm that the new bookmark has the expected
 // title. (Then delete the bookmark.)
 async function checkBookmark(url, expected_title) {
   Assert.equal(gBrowser.selectedBrowser.currentURI.spec, url,
     "Trying to bookmark the expected uri");
 
-  let promiseBookmark = PlacesTestUtils.waitForNotification("onItemAdded",
-    (id, parentId, index, type, itemUrl) => itemUrl.equals(gBrowser.selectedBrowser.currentURI));
+  let promiseBookmark = PlacesTestUtils.waitForNotification("bookmark-added",
+    (events) => events.some(({url: eventUrl}) => eventUrl == gBrowser.selectedBrowser.currentURI.spec),
+    "places");
   PlacesCommandHook.bookmarkPage();
   await promiseBookmark;
 
   let bookmark = await PlacesUtils.bookmarks.fetch({url});
 
   Assert.ok(bookmark, "Found the expected bookmark");
   Assert.equal(bookmark.title, expected_title, "Bookmark got a good default title.");
 
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -460,38 +460,28 @@ function promiseNotificationShown(notifi
   return panelPromise;
 }
 
 /**
  * Resolves when a bookmark with the given uri is added.
  */
 function promiseOnBookmarkItemAdded(aExpectedURI) {
   return new Promise((resolve, reject) => {
-    let bookmarksObserver = {
-      onItemAdded(aItemId, aFolderId, aIndex, aItemType, aURI) {
-        info("Added a bookmark to " + aURI.spec);
-        PlacesUtils.bookmarks.removeObserver(bookmarksObserver);
-        if (aURI.equals(aExpectedURI)) {
-          resolve();
-        } else {
-          reject(new Error("Added an unexpected bookmark"));
-        }
-      },
-      onBeginUpdateBatch() {},
-      onEndUpdateBatch() {},
-      onItemRemoved() {},
-      onItemChanged() {},
-      onItemVisited() {},
-      onItemMoved() {},
-      QueryInterface: ChromeUtils.generateQI([
-        Ci.nsINavBookmarkObserver,
-      ]),
+    let listener = events => {
+      is(events.length, 1, "Should only receive one event.");
+      info("Added a bookmark to " + events[0].url);
+      PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+      if (events[0].url == aExpectedURI.spec) {
+        resolve();
+      } else {
+        reject(new Error("Added an unexpected bookmark"));
+      }
     };
     info("Waiting for a bookmark to be added");
-    PlacesUtils.bookmarks.addObserver(bookmarksObserver);
+    PlacesUtils.observers.addListener(["bookmark-added"], listener);
   });
 }
 
 async function loadBadCertPage(url) {
   const EXCEPTION_DIALOG_URI = "chrome://pippki/content/exceptionDialog.xul";
   let exceptionDialogResolved = new Promise(function(resolve) {
     // When the certificate exception dialog has opened, click the button to add
     // an exception.
--- a/browser/base/content/test/siteIdentity/browser_identity_UI.js
+++ b/browser/base/content/test/siteIdentity/browser_identity_UI.js
@@ -1,21 +1,11 @@
 /* Tests for correct behaviour of getEffectiveHost on identity handler */
 
-function test() {
-  waitForExplicitFinish();
-  requestLongerTimeout(2);
-
-  ok(gIdentityHandler, "gIdentityHandler should exist");
-
-  BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank", true).then(() => {
-    BrowserTestUtils.addContentEventListener(gBrowser.selectedBrowser, "load", checkResult, true);
-    nextTest();
-  });
-}
+requestLongerTimeout(2);
 
 // Greek IDN for 'example.test'.
 var idnDomain = "\u03C0\u03B1\u03C1\u03AC\u03B4\u03B5\u03B9\u03B3\u03BC\u03B1.\u03B4\u03BF\u03BA\u03B9\u03BC\u03AE";
 var tests = [
   {
     name: "normal domain",
     location: "http://test1.example.org/",
     effectiveHost: "test1.example.org",
@@ -24,127 +14,106 @@ var tests = [
     name: "view-source",
     location: "view-source:http://example.com/",
     effectiveHost: null,
   },
   {
     name: "normal HTTPS",
     location: "https://example.com/",
     effectiveHost: "example.com",
-    isHTTPS: true,
   },
   {
     name: "IDN subdomain",
     location: "http://sub1.xn--hxajbheg2az3al.xn--jxalpdlp/",
     effectiveHost: "sub1." + idnDomain,
   },
   {
     name: "subdomain with port",
     location: "http://sub1.test1.example.org:8000/",
     effectiveHost: "sub1.test1.example.org",
   },
   {
     name: "subdomain HTTPS",
     location: "https://test1.example.com/",
     effectiveHost: "test1.example.com",
-    isHTTPS: true,
   },
   {
     name: "view-source HTTPS",
     location: "view-source:https://example.com/",
     effectiveHost: null,
-    isHTTPS: true,
   },
   {
     name: "IP address",
     location: "http://127.0.0.1:8888/",
     effectiveHost: "127.0.0.1",
   },
 ];
 
-var gCurrentTest, gCurrentTestIndex = -1, gTestDesc, gPopupHidden;
-// Go through the tests in both directions, to add additional coverage for
-// transitions between different states.
-var gForward = true;
-var gCheckETLD = false;
-function nextTest() {
-  if (!gCheckETLD) {
-    if (gForward)
-      gCurrentTestIndex++;
-    else
-      gCurrentTestIndex--;
+add_task(async function test() {
+  ok(gIdentityHandler, "gIdentityHandler should exist");
+
+  await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+  for (let i = 0; i < tests.length; i++) {
+    await runTest(i, true);
+  }
 
-    if (gCurrentTestIndex == tests.length) {
-      // Went too far, reverse
-      gCurrentTestIndex--;
-      gForward = false;
-    }
+  gBrowser.removeCurrentTab();
+  await BrowserTestUtils.openNewForegroundTab(gBrowser);
 
-    if (gCurrentTestIndex == -1) {
-      gBrowser.removeCurrentTab();
-      finish();
-      return;
-    }
+  for (let i = tests.length - 1; i >= 0; i--) {
+    await runTest(i, false);
+  }
 
-    gCurrentTest = tests[gCurrentTestIndex];
-    gTestDesc = "#" + gCurrentTestIndex + " (" + gCurrentTest.name + ")";
-    if (!gForward)
-      gTestDesc += " (second time)";
-    if (gCurrentTest.isHTTPS) {
-      gCheckETLD = true;
-    }
+  gBrowser.removeCurrentTab();
+});
 
-    // Navigate to the next page, which will cause checkResult to fire.
-    let spec = gBrowser.selectedBrowser.currentURI.spec;
-    if (spec == "about:blank" || spec == gCurrentTest.location) {
-      BrowserTestUtils.loadURI(gBrowser.selectedBrowser, gCurrentTest.location);
-    } else {
-      // Open the Control Center and make sure it closes after nav (Bug 1207542).
-      let popupShown = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popupshown");
-      gPopupHidden = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popuphidden");
-      gIdentityHandler._identityBox.click();
-      info("Waiting for the Control Center to be shown");
-      popupShown.then(async () => {
-        ok(!BrowserTestUtils.is_hidden(gIdentityHandler._identityPopup), "Control Center is visible");
-        // Show the subview, which is an easy way in automation to reproduce
-        // Bug 1207542, where the CC wouldn't close on navigation.
-        let promiseViewShown = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "ViewShown");
-        gBrowser.ownerDocument.querySelector("#identity-popup-security-expander").click();
-        await promiseViewShown;
-        BrowserTestUtils.loadURI(gBrowser.selectedBrowser, gCurrentTest.location);
-      });
-    }
-  } else {
-    gCheckETLD = false;
-    gTestDesc = "#" + gCurrentTestIndex + " (" + gCurrentTest.name + " without eTLD in identity icon label)";
-    if (!gForward)
-      gTestDesc += " (second time)";
-    gBrowser.selectedBrowser.reloadWithFlags(Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE |
-                                             Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY);
+async function runTest(i, forward) {
+  let currentTest = tests[i];
+  let testDesc = "#" + i + " (" + currentTest.name + ")";
+  if (!forward) {
+    testDesc += " (second time)";
   }
-}
+
+  info("Running test " + testDesc);
 
-function checkResult() {
-  if (gBrowser.selectedBrowser.currentURI.spec == "about:blank")
-    return;
+  let popupHidden = null;
+  if ((forward && i > 0) || (!forward && i < tests.length - 1)) {
+    popupHidden = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popuphidden");
+  }
+
+  let loaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser, false, currentTest.location);
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, currentTest.location);
+  await loaded;
+  await popupHidden;
+  ok(BrowserTestUtils.is_hidden(gIdentityHandler._identityPopup), "Control Center is hidden");
 
   // Sanity check other values, and the value of gIdentityHandler.getEffectiveHost()
-  is(gIdentityHandler._uri.spec, gCurrentTest.location, "location matches for test " + gTestDesc);
+  is(gIdentityHandler._uri.spec, currentTest.location, "location matches for test " + testDesc);
   // getEffectiveHost can't be called for all modes
-  if (gCurrentTest.effectiveHost === null) {
+  if (currentTest.effectiveHost === null) {
     let identityBox = document.getElementById("identity-box");
     ok(identityBox.className == "unknownIdentity" ||
        identityBox.className == "chromeUI", "mode matched");
   } else {
-    is(gIdentityHandler.getEffectiveHost(), gCurrentTest.effectiveHost, "effectiveHost matches for test " + gTestDesc);
+    is(gIdentityHandler.getEffectiveHost(), currentTest.effectiveHost, "effectiveHost matches for test " + testDesc);
   }
 
-  if (gPopupHidden) {
-    info("Waiting for the Control Center to hide");
-    gPopupHidden.then(() => {
-      gPopupHidden = null;
-      ok(BrowserTestUtils.is_hidden(gIdentityHandler._identityPopup), "Control Center is hidden");
-      executeSoon(nextTest);
-    });
-  } else {
-    executeSoon(nextTest);
-  }
+  // Open the Control Center and make sure it closes after nav (Bug 1207542).
+  let popupShown = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "popupshown");
+  gIdentityHandler._identityBox.click();
+  info("Waiting for the Control Center to be shown");
+  await popupShown;
+  ok(!BrowserTestUtils.is_hidden(gIdentityHandler._identityPopup), "Control Center is visible");
+  let displayedHost = currentTest.effectiveHost || currentTest.location;
+  ok(gIdentityHandler._identityPopupMainViewHeaderLabel.textContent.includes(displayedHost),
+     "identity UI header shows the host for test " + testDesc);
+
+  // Show the subview, which is an easy way in automation to reproduce
+  // Bug 1207542, where the CC wouldn't close on navigation.
+  let promiseViewShown = BrowserTestUtils.waitForEvent(gIdentityHandler._identityPopup, "ViewShown");
+  gBrowser.ownerDocument.querySelector("#identity-popup-security-expander").click();
+  await promiseViewShown;
+
+  displayedHost = currentTest.effectiveHost || currentTest.location;
+  ok(gIdentityHandler._identityPopupContentHost.textContent.includes(displayedHost),
+     "security subview header shows the host for test " + testDesc);
 }
--- a/browser/components/controlcenter/content/panel.inc.xul
+++ b/browser/components/controlcenter/content/panel.inc.xul
@@ -10,22 +10,27 @@
        onpopupshown="gIdentityHandler.onPopupShown(event);"
        onpopuphidden="gIdentityHandler.onPopupHidden(event);"
        orient="vertical">
 
   <panelmultiview id="identity-popup-multiView"
                   mainViewId="identity-popup-mainView">
     <panelview id="identity-popup-mainView"
                descriptionheightworkaround="true">
+      <vbox id="identity-popup-mainView-panel-header">
+        <label>
+          <html:span id="identity-popup-mainView-panel-header-span"/>
+        </label>
+      </vbox>
+
       <!-- Security Section -->
       <hbox id="identity-popup-security" class="identity-popup-section">
         <vbox class="identity-popup-security-content" flex="1">
           <label class="plain">
-            <label class="identity-popup-headline identity-popup-host"></label>
-            <label class="identity-popup-headline identity-popup-hostless" crop="end"/>
+            <label class="identity-popup-headline">&identity.connection;</label>
           </label>
           <description class="identity-popup-connection-not-secure"
                        when-connection="not-secure secure-cert-user-overridden">&identity.connectionNotSecure;</description>
           <description class="identity-popup-connection-secure"
                        when-connection="secure secure-ev">&identity.connectionSecure;</description>
           <description when-connection="chrome">&identity.connectionInternal;</description>
           <description when-connection="file">&identity.connectionFile;</description>
           <description when-connection="extension">&identity.extensionPage;</description>
@@ -160,18 +165,17 @@
     </panelview>
 
     <!-- Security SubView -->
     <panelview id="identity-popup-securityView"
                title="&identity.securityView.label;"
                descriptionheightworkaround="true">
       <vbox class="identity-popup-security-content">
         <label class="plain">
-          <label class="identity-popup-headline identity-popup-host"></label>
-          <label class="identity-popup-headline identity-popup-hostless" crop="end"/>
+          <label class="identity-popup-headline" id="identity-popup-host"></label>
         </label>
         <description class="identity-popup-connection-not-secure"
                      when-connection="not-secure secure-cert-user-overridden">&identity.connectionNotSecure;</description>
         <description class="identity-popup-connection-secure"
                      when-connection="secure secure-ev">&identity.connectionSecure;</description>
       </vbox>
 
       <vbox id="identity-popup-securityView-body" class="panel-view-body-unscrollable">
--- a/browser/components/enterprisepolicies/tests/browser/browser_policy_bookmarks.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policy_bookmarks.js
@@ -64,40 +64,43 @@ function findBookmarkInPolicy(bookmark) 
       return entry;
     }
   }
   return null;
 }
 
 async function promiseAllChangesMade({itemsToAdd, itemsToRemove}) {
   return new Promise(resolve => {
+    let listener = events => {
+      is(events.length, 1, "Should only have 1 event.");
+      itemsToAdd--;
+      if (itemsToAdd == 0 && itemsToRemove == 0) {
+        PlacesUtils.bookmarks.removeObserver(bmObserver);
+        PlacesUtils.observers.removeListener(["bookmark-added"], listener);
+        resolve();
+      }
+    };
     let bmObserver = {
-      onItemAdded() {
-        itemsToAdd--;
-        if (itemsToAdd == 0 && itemsToRemove == 0) {
-          PlacesUtils.bookmarks.removeObserver(bmObserver);
-          resolve();
-        }
-      },
-
       onItemRemoved() {
         itemsToRemove--;
         if (itemsToAdd == 0 && itemsToRemove == 0) {
           PlacesUtils.bookmarks.removeObserver(bmObserver);
+          PlacesUtils.observers.removeListener(["bookmark-added"], listener);
           resolve();
         }
       },
 
       onBeginUpdateBatch() {},
       onEndUpdateBatch() {},
       onItemChanged() {},
       onItemVisited() {},
       onItemMoved() {},
     };
     PlacesUtils.bookmarks.addObserver(bmObserver);
+    PlacesUtils.observers.addListener(["bookmark-added"], listener);
   });
 }
 
 /*
  * ==================
  * = CHECK FUNCTION =
  * ==================
  *
--- a/browser/components/extensions/parent/ext-bookmarks.js
+++ b/browser/components/extensions/parent/ext-bookmarks.js
@@ -102,37 +102,44 @@ const convertBookmarks = result => {
 };
 
 let observer = new class extends EventEmitter {
   constructor() {
     super();
 
     this.skipTags = true;
     this.skipDescendantsOnItemRemoval = true;
+
+    this.handlePlacesEvents = this.handlePlacesEvents.bind(this);
   }
 
   onBeginUpdateBatch() {}
   onEndUpdateBatch() {}
 
-  onItemAdded(id, parentId, index, itemType, uri, title, dateAdded, guid, parentGuid, source) {
-    let bookmark = {
-      id: guid,
-      parentId: parentGuid,
-      index,
-      title,
-      dateAdded: dateAdded / 1000,
-      type: BOOKMARKS_TYPES_TO_API_TYPES_MAP.get(itemType),
-      url: getUrl(itemType, uri && uri.spec),
-    };
+  handlePlacesEvents(events) {
+    for (let event of events) {
+      if (event.isTagging) {
+        continue;
+      }
+      let bookmark = {
+        id: event.guid,
+        parentId: event.parentGuid,
+        index: event.index,
+        title: event.title,
+        dateAdded: event.dateAdded,
+        type: BOOKMARKS_TYPES_TO_API_TYPES_MAP.get(event.itemType),
+        url: getUrl(event.itemType, event.url),
+      };
 
-    if (itemType == TYPE_FOLDER) {
-      bookmark.dateGroupModified = bookmark.dateAdded;
+      if (event.itemType == TYPE_FOLDER) {
+        bookmark.dateGroupModified = bookmark.dateAdded;
+      }
+
+      this.emit("created", bookmark);
     }
-
-    this.emit("created", bookmark);
   }
 
   onItemVisited() {}
 
   onItemMoved(id, oldParentId, oldIndex, newParentId, newIndex, itemType, guid, oldParentGuid, newParentGuid, source) {
     let info = {
       parentId: newParentGuid,
       index: newIndex,
@@ -168,23 +175,25 @@ let observer = new class extends EventEm
     this.emit("changed", {guid, info});
   }
 }();
 
 const decrementListeners = () => {
   listenerCount -= 1;
   if (!listenerCount) {
     PlacesUtils.bookmarks.removeObserver(observer);
+    PlacesUtils.observers.removeListener(["bookmark-added"], observer.handlePlacesEvents);
   }
 };
 
 const incrementListeners = () => {
   listenerCount++;
   if (listenerCount == 1) {
     PlacesUtils.bookmarks.addObserver(observer);
+    PlacesUtils.observers.addListener(["bookmark-added"], observer.handlePlacesEvents);
   }
 };
 
 this.bookmarks = class extends ExtensionAPI {
   getAPI(context) {
     return {
       bookmarks: {
         async get(idOrIdList) {
--- a/browser/components/migration/tests/unit/test_360se_bookmarks.js
+++ b/browser/components/migration/tests/unit/test_360se_bookmarks.js
@@ -16,43 +16,37 @@ add_task(async function() {
   // folders are created on the toolbar.
   let source = MigrationUtils.getLocalizedString("sourceName360se");
   let label = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
 
   let expectedParents = [ PlacesUtils.toolbarFolderId ];
   let itemCount = 0;
 
   let gotFolder = false;
-  let bmObserver = {
-    onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle) {
-      if (aTitle != label) {
+  let listener = events => {
+    for (let event of events) {
+      if (event.title != label) {
         itemCount++;
       }
-      if (aItemType == PlacesUtils.bookmarks.TYPE_FOLDER && aTitle == "360 \u76f8\u5173") {
+      if (event.itemType == PlacesUtils.bookmarks.TYPE_FOLDER && event.title == "360 \u76f8\u5173") {
         gotFolder = true;
       }
-      if (expectedParents.length > 0 && aTitle == label) {
-        let index = expectedParents.indexOf(aParentId);
+      if (expectedParents.length > 0 && event.title == label) {
+        let index = expectedParents.indexOf(event.parentId);
         Assert.ok(index != -1, "Found expected parent");
         expectedParents.splice(index, 1);
       }
-    },
-    onBeginUpdateBatch() {},
-    onEndUpdateBatch() {},
-    onItemRemoved() {},
-    onItemChanged() {},
-    onItemVisited() {},
-    onItemMoved() {},
+    }
   };
-  PlacesUtils.bookmarks.addObserver(bmObserver);
+  PlacesUtils.observers.addListener(["bookmark-added"], listener);
 
   await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS, {
     id: "default",
   });
-  PlacesUtils.bookmarks.removeObserver(bmObserver);
+  PlacesUtils.observers.removeListener(["bookmark-added"], listener);
 
   // Check the bookmarks have been imported to all the expected parents.
   Assert.ok(!expectedParents.length, "No more expected parents");
   Assert.ok(gotFolder, "Should have seen the folder get imported");
   Assert.equal(itemCount, 10, "Should import all 10 items.");
   // Check that the telemetry matches:
   Assert.equal(MigrationUtils._importQuantities.bookmarks, itemCount, "Telemetry reporting correct.");
 });
--- a/browser/components/migration/tests/unit/test_Chrome_bookmarks.js
+++ b/browser/components/migration/tests/unit/test_Chrome_bookmarks.js
@@ -72,34 +72,28 @@ add_task(async function() {
 
   await OS.File.writeAtomic(target.path, JSON.stringify(bookmarksData), {encoding: "utf-8"});
 
   let migrator = await MigrationUtils.getMigrator("chrome");
   // Sanity check for the source.
   Assert.ok(await migrator.isSourceAvailable());
 
   let itemsSeen = {bookmarks: 0, folders: 0};
-  let bmObserver = {
-    onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle) {
-      if (!aTitle.includes("Chrome")) {
-        itemsSeen[aItemType == PlacesUtils.bookmarks.TYPE_FOLDER ? "folders" : "bookmarks"]++;
+  let listener = events => {
+    for (let event of events) {
+      if (!event.title.includes("Chrome")) {
+        itemsSeen[event.itemType == PlacesUtils.bookmarks.TYPE_FOLDER ? "folders" : "bookmarks"]++;
       }
-    },
-    onBeginUpdateBatch() {},
-    onEndUpdateBatch() {},
-    onItemRemoved() {},
-    onItemChanged() {},
-    onItemVisited() {},
-    onItemMoved() {},
+    }
   };
 
-  PlacesUtils.bookmarks.addObserver(bmObserver);
+  PlacesUtils.observers.addListener(["bookmark-added"], listener);
   const PROFILE = {
     id: "Default",
     name: "Default",
   };
   await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS, PROFILE);
-  PlacesUtils.bookmarks.removeObserver(bmObserver);
+  PlacesUtils.observers.removeListener(["bookmark-added"], listener);
 
   Assert.equal(itemsSeen.bookmarks, 200, "Should have seen 200 bookmarks.");
   Assert.equal(itemsSeen.folders, 10, "Should have seen 10 folders.");
   Assert.equal(MigrationUtils._importQuantities.bookmarks, itemsSeen.bookmarks + itemsSeen.folders, "Telemetry reporting correct.");
 });
--- a/browser/components/migration/tests/unit/test_Edge_db_migration.js
+++ b/browser/components/migration/tests/unit/test_Edge_db_migration.js
@@ -418,50 +418,55 @@ add_task(async function() {
                  .createInstance(Ci.nsIBrowserProfileMigrator);
   let bookmarksMigrator = migrator.wrappedJSObject.getBookmarksMigratorForTesting(db);
   Assert.ok(bookmarksMigrator.exists, "Should recognize db we just created");
 
   let source = MigrationUtils.getLocalizedString("sourceNameEdge");
   let sourceLabel = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
 
   let seenBookmarks = [];
-  let bookmarkObserver = {
-    onItemAdded(itemId, parentId, index, itemType, url, title, dateAdded, itemGuid, parentGuid) {
+  let listener = events => {
+    for (let event of events) {
+      let {
+        id,
+        itemType,
+        url,
+        title,
+        dateAdded,
+        guid,
+        index,
+        parentGuid,
+        parentId,
+      } = event;
       if (title.startsWith("Deleted")) {
         ok(false, "Should not see deleted items being bookmarked!");
       }
-      seenBookmarks.push({itemId, parentId, index, itemType, url, title, dateAdded, itemGuid, parentGuid});
-    },
-    onBeginUpdateBatch() {},
-    onEndUpdateBatch() {},
-    onItemRemoved() {},
-    onItemChanged() {},
-    onItemVisited() {},
-    onItemMoved() {},
+      seenBookmarks.push({id, parentId, index, itemType, url, title, dateAdded, guid, parentGuid});
+    }
   };
-  PlacesUtils.bookmarks.addObserver(bookmarkObserver);
+  PlacesUtils.observers.addListener(["bookmark-added"], listener);
 
   let migrateResult = await new Promise(resolve => bookmarksMigrator.migrate(resolve)).catch(ex => {
     Cu.reportError(ex);
     Assert.ok(false, "Got an exception trying to migrate data! " + ex);
     return false;
   });
-  PlacesUtils.bookmarks.removeObserver(bookmarkObserver);
+  PlacesUtils.observers.removeListener(["bookmark-added"], listener);
   Assert.ok(migrateResult, "Migration should succeed");
   Assert.equal(seenBookmarks.length, 7, "Should have seen 7 items being bookmarked.");
   Assert.equal(seenBookmarks.filter(bm => bm.title != sourceLabel).length,
                MigrationUtils._importQuantities.bookmarks,
                "Telemetry should have items except for 'From Microsoft Edge' folders");
 
   let menuParents = seenBookmarks.filter(item => item.parentGuid == PlacesUtils.bookmarks.menuGuid);
   Assert.equal(menuParents.length, 1, "Should have a single folder added to the menu");
   let toolbarParents = seenBookmarks.filter(item => item.parentGuid == PlacesUtils.bookmarks.toolbarGuid);
   Assert.equal(toolbarParents.length, 1, "Should have a single item added to the toolbar");
-  let menuParentGuid = menuParents[0].itemGuid;
-  let toolbarParentGuid = toolbarParents[0].itemGuid;
+  let menuParentGuid = menuParents[0].guid;
+  let toolbarParentGuid = toolbarParents[0].guid;
 
   let expectedTitlesInMenu = bookmarkReferenceItems.filter(item => item.ParentId == kEdgeMenuParent).map(item => item.Title);
   // Hacky, but seems like much the simplest way:
   expectedTitlesInMenu.push("Item in deleted folder (should be in root)");
   let expectedTitlesInToolbar = bookmarkReferenceItems.filter(item => item.ParentId == "921dc8a0-6c83-40ef-8df1-9bd1c5c56aaf").map(item => item.Title);
 
   let edgeNameStr = MigrationUtils.getLocalizedString("sourceNameEdge");
   let importParentFolderName = MigrationUtils.getLocalizedString("importedBookmarksFolder", [edgeNameStr]);
@@ -476,68 +481,73 @@ add_task(async function() {
       Assert.notEqual(bookmark.itemType, PlacesUtils.bookmarks.TYPE_FOLDER,
           "Bookmark " + bookmark.title + " should not be a folder");
     }
 
     if (shouldBeInMenu) {
       Assert.equal(bookmark.parentGuid, menuParentGuid, "Item '" + bookmark.title + "' should be in menu");
     } else if (shouldBeInToolbar) {
       Assert.equal(bookmark.parentGuid, toolbarParentGuid, "Item '" + bookmark.title + "' should be in toolbar");
-    } else if (bookmark.itemGuid == menuParentGuid || bookmark.itemGuid == toolbarParentGuid) {
+    } else if (bookmark.guid == menuParentGuid || bookmark.guid == toolbarParentGuid) {
       Assert.ok(true, "Expect toolbar and menu folders to not be in menu or toolbar");
     } else {
       // Bit hacky, but we do need to check this.
       Assert.equal(bookmark.title, "Item in folder", "Subfoldered item shouldn't be in menu or toolbar");
-      let parent = seenBookmarks.find(maybeParent => maybeParent.itemGuid == bookmark.parentGuid);
+      let parent = seenBookmarks.find(maybeParent => maybeParent.guid == bookmark.parentGuid);
       Assert.equal(parent && parent.title, "Folder", "Subfoldered item should be in subfolder labeled 'Folder'");
     }
 
     let dbItem = bookmarkReferenceItems.find(someItem => bookmark.title == someItem.Title);
     if (!dbItem) {
       Assert.equal(bookmark.title, importParentFolderName, "Only the extra layer of folders isn't in the input we stuck in the DB.");
-      Assert.ok([menuParentGuid, toolbarParentGuid].includes(bookmark.itemGuid), "This item should be one of the containers");
+      Assert.ok([menuParentGuid, toolbarParentGuid].includes(bookmark.guid), "This item should be one of the containers");
     } else {
-      Assert.equal(dbItem.URL || null, bookmark.url && bookmark.url.spec, "URL is correct");
-      Assert.equal(dbItem.DateUpdated.valueOf(), (new Date(bookmark.dateAdded / 1000)).valueOf(), "Date added is correct");
+      Assert.equal(dbItem.URL || "", bookmark.url, "URL is correct");
+      Assert.equal(dbItem.DateUpdated.valueOf(), (new Date(bookmark.dateAdded)).valueOf(), "Date added is correct");
     }
   }
 
   MigrationUtils._importQuantities.bookmarks = 0;
   seenBookmarks = [];
-  bookmarkObserver = {
-    onItemAdded(itemId, parentId, index, itemType, url, title, dateAdded, itemGuid, parentGuid) {
-      seenBookmarks.push({itemId, parentId, index, itemType, url, title, dateAdded, itemGuid, parentGuid});
-    },
-    onBeginUpdateBatch() {},
-    onEndUpdateBatch() {},
-    onItemRemoved() {},
-    onItemChanged() {},
-    onItemVisited() {},
-    onItemMoved() {},
+  listener = events => {
+    for (let event of events) {
+      let {
+        id,
+        itemType,
+        url,
+        title,
+        dateAdded,
+        guid,
+        index,
+        parentGuid,
+        parentId,
+      } = event;
+      seenBookmarks.push({id, parentId, index, itemType, url, title, dateAdded, guid, parentGuid});
+    }
   };
-  PlacesUtils.bookmarks.addObserver(bookmarkObserver);
+  PlacesUtils.observers.addListener(["bookmark-added"], listener);
 
   let readingListMigrator = migrator.wrappedJSObject.getReadingListMigratorForTesting(db);
   Assert.ok(readingListMigrator.exists, "Should recognize db we just created");
   migrateResult = await new Promise(resolve => readingListMigrator.migrate(resolve)).catch(ex => {
     Cu.reportError(ex);
     Assert.ok(false, "Got an exception trying to migrate data! " + ex);
     return false;
   });
-  PlacesUtils.bookmarks.removeObserver(bookmarkObserver);
+  PlacesUtils.observers.removeListener(["bookmark-added"], listener);
   Assert.ok(migrateResult, "Migration should succeed");
   Assert.equal(seenBookmarks.length, 3, "Should have seen 3 items being bookmarked (2 items + 1 folder).");
   Assert.equal(seenBookmarks.filter(bm => bm.title != sourceLabel).length,
                MigrationUtils._importQuantities.bookmarks,
                "Telemetry should have items except for 'From Microsoft Edge' folders");
   let readingListContainerLabel = MigrationUtils.getLocalizedString("importedEdgeReadingList");
 
   for (let bookmark of seenBookmarks) {
     if (readingListContainerLabel == bookmark.title) {
       continue;
     }
     let referenceItem = readingListReferenceItems.find(item => item.Title == bookmark.title);
     Assert.ok(referenceItem, "Should have imported what we expected");
-    Assert.equal(referenceItem.URL, bookmark.url.spec, "Should have the right URL");
+    Assert.equal(referenceItem.URL, bookmark.url, "Should have the right URL");
     readingListReferenceItems.splice(readingListReferenceItems.findIndex(item => item.Title == bookmark.title), 1);
   }
   Assert.ok(!readingListReferenceItems.length, "Should have seen all expected items.");
 });
--- a/browser/components/migration/tests/unit/test_IE_bookmarks.js
+++ b/browser/components/migration/tests/unit/test_IE_bookmarks.js
@@ -9,36 +9,30 @@ add_task(async function() {
   // folders are created in the menu and on the toolbar.
   let source = MigrationUtils.getLocalizedString("sourceNameIE");
   let label = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
 
   let expectedParents = [ PlacesUtils.bookmarksMenuFolderId,
                           PlacesUtils.toolbarFolderId ];
 
   let itemCount = 0;
-  let bmObserver = {
-    onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle) {
-      if (aTitle != label) {
+  let listener = events => {
+    for (let event of events) {
+      if (event.title != label) {
         itemCount++;
       }
-      if (expectedParents.length > 0 && aTitle == label) {
-        let index = expectedParents.indexOf(aParentId);
+      if (expectedParents.length > 0 && event.title == label) {
+        let index = expectedParents.indexOf(event.parentId);
         Assert.notEqual(index, -1);
         expectedParents.splice(index, 1);
       }
-    },
-    onBeginUpdateBatch() {},
-    onEndUpdateBatch() {},
-    onItemRemoved() {},
-    onItemChanged() {},
-    onItemVisited() {},
-    onItemMoved() {},
+    }
   };
-  PlacesUtils.bookmarks.addObserver(bmObserver);
+  PlacesUtils.observers.addListener(["bookmark-added"], listener);
 
   await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS);
-  PlacesUtils.bookmarks.removeObserver(bmObserver);
+  PlacesUtils.observers.removeListener(["bookmark-added"], listener);
   Assert.equal(MigrationUtils._importQuantities.bookmarks, itemCount,
                "Ensure telemetry matches actual number of imported items.");
 
   // Check the bookmarks have been imported to all the expected parents.
   Assert.equal(expectedParents.length, 0, "Got all the expected parents");
 });
--- a/browser/components/migration/tests/unit/test_Safari_bookmarks.js
+++ b/browser/components/migration/tests/unit/test_Safari_bookmarks.js
@@ -11,41 +11,35 @@ add_task(async function() {
   // folders are created on the toolbar.
   let source = MigrationUtils.getLocalizedString("sourceNameSafari");
   let label = MigrationUtils.getLocalizedString("importedBookmarksFolder", [source]);
 
   let expectedParents = [ PlacesUtils.toolbarFolderId ];
   let itemCount = 0;
 
   let gotFolder = false;
-  let bmObserver = {
-    onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle) {
-      if (aTitle != label) {
+  let listener = events => {
+    for (let event of events) {
+      if (event.title != label) {
         itemCount++;
       }
-      if (aItemType == PlacesUtils.bookmarks.TYPE_FOLDER && aTitle == "Stuff") {
+      if (event.itemType == PlacesUtils.bookmarks.TYPE_FOLDER && event.title == "Stuff") {
         gotFolder = true;
       }
-      if (expectedParents.length > 0 && aTitle == label) {
-        let index = expectedParents.indexOf(aParentId);
+      if (expectedParents.length > 0 && event.title == label) {
+        let index = expectedParents.indexOf(event.parentId);
         Assert.ok(index != -1, "Found expected parent");
         expectedParents.splice(index, 1);
       }
-    },
-    onBeginUpdateBatch() {},
-    onEndUpdateBatch() {},
-    onItemRemoved() {},
-    onItemChanged() {},
-    onItemVisited() {},
-    onItemMoved() {},
+    }
   };
-  PlacesUtils.bookmarks.addObserver(bmObserver);
+  PlacesUtils.observers.addListener(["bookmark-added"], listener);
 
   await promiseMigration(migrator, MigrationUtils.resourceTypes.BOOKMARKS);
-  PlacesUtils.bookmarks.removeObserver(bmObserver);
+  PlacesUtils.observers.removeListener(["bookmark-added"], listener);
 
   // Check the bookmarks have been imported to all the expected parents.
   Assert.ok(!expectedParents.length, "No more expected parents");
   Assert.ok(gotFolder, "Should have seen the folder get imported");
   Assert.equal(itemCount, 13, "Should import all 13 items.");
   // Check that the telemetry matches:
   Assert.equal(MigrationUtils._importQuantities.bookmarks, itemCount, "Telemetry reporting correct.");
 });
--- a/browser/components/newtab/lib/PlacesFeed.jsm
+++ b/browser/components/newtab/lib/PlacesFeed.jsm
@@ -75,56 +75,16 @@ class HistoryObserver extends Observer {
  */
 class BookmarksObserver extends Observer {
   constructor(dispatch) {
     super(dispatch, Ci.nsINavBookmarkObserver);
     this.skipTags = true;
   }
 
   /**
-   * onItemAdded - Called when a bookmark is added
-   *
-   * @param  {str} id
-   * @param  {str} folderId
-   * @param  {int} index
-   * @param  {int} type       Indicates if the bookmark is an actual bookmark,
-   *                          a folder, or a separator.
-   * @param  {str} uri
-   * @param  {str} title
-   * @param  {int} dateAdded
-   * @param  {str} guid      The unique id of the bookmark
-   * @param  {str} parent guid
-   * @param  {int} source    Used to distinguish bookmarks made by different
-   *                         actions: sync, bookmarks import, other.
-   */
-  onItemAdded(id, folderId, index, type, uri, bookmarkTitle, dateAdded, bookmarkGuid, parentGuid, source) { // eslint-disable-line max-params
-    // Skips items that are not bookmarks (like folders), about:* pages or
-    // default bookmarks, added when the profile is created.
-    if (type !== PlacesUtils.bookmarks.TYPE_BOOKMARK ||
-        source === PlacesUtils.bookmarks.SOURCES.IMPORT ||
-        source === PlacesUtils.bookmarks.SOURCES.RESTORE ||
-        source === PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP ||
-        source === PlacesUtils.bookmarks.SOURCES.SYNC ||
-        (uri.scheme !== "http" && uri.scheme !== "https")) {
-      return;
-    }
-
-    this.dispatch({type: at.PLACES_LINKS_CHANGED});
-    this.dispatch({
-      type: at.PLACES_BOOKMARK_ADDED,
-      data: {
-        bookmarkGuid,
-        bookmarkTitle,
-        dateAdded,
-        url: uri.spec,
-      },
-    });
-  }
-
-  /**
    * onItemRemoved - Called when a bookmark is removed
    *
    * @param  {str} id
    * @param  {str} folderId
    * @param  {int} index
    * @param  {int} type       Indicates if the bookmark is an actual bookmark,
    *                          a folder, or a separator.
    * @param  {str} uri
@@ -153,32 +113,72 @@ class BookmarksObserver extends Observer
 
   onItemMoved() {}
 
   // Disabled due to performance cost, see Issue 3203 /
   // https://bugzilla.mozilla.org/show_bug.cgi?id=1392267.
   onItemChanged() {}
 }
 
+/**
+ * PlacesObserver - observes events from PlacesUtils.observers
+ */
+class PlacesObserver extends Observer {
+  constructor(dispatch) {
+    super(dispatch, Ci.nsINavBookmarkObserver);
+    this.handlePlacesEvents = this.handlePlacesEvents.bind(this);
+  }
+
+  handlePlacesEvents(events) {
+    for (let {itemType, source, dateAdded, guid, title, url, isTagging} of events) {
+      // Skips items that are not bookmarks (like folders), about:* pages or
+      // default bookmarks, added when the profile is created.
+      if (isTagging ||
+          itemType !== PlacesUtils.bookmarks.TYPE_BOOKMARK ||
+          source === PlacesUtils.bookmarks.SOURCES.IMPORT ||
+          source === PlacesUtils.bookmarks.SOURCES.RESTORE ||
+          source === PlacesUtils.bookmarks.SOURCES.RESTORE_ON_STARTUP ||
+          source === PlacesUtils.bookmarks.SOURCES.SYNC ||
+          (!url.startsWith("http://") && !url.startsWith("https://"))) {
+        return;
+      }
+
+      this.dispatch({type: at.PLACES_LINKS_CHANGED});
+      this.dispatch({
+        type: at.PLACES_BOOKMARK_ADDED,
+        data: {
+          bookmarkGuid: guid,
+          bookmarkTitle: title,
+          dateAdded: dateAdded * 1000,
+          url
+        }
+      });
+    }
+  }
+}
+
 class PlacesFeed {
   constructor() {
     this.placesChangedTimer = null;
     this.customDispatch = this.customDispatch.bind(this);
     this.historyObserver = new HistoryObserver(this.customDispatch);
     this.bookmarksObserver = new BookmarksObserver(this.customDispatch);
+    this.placesObserver = new PlacesObserver(this.customDispatch);
   }
 
   addObservers() {
     // NB: Directly get services without importing the *BIG* PlacesUtils module
     Cc["@mozilla.org/browser/nav-history-service;1"]
       .getService(Ci.nsINavHistoryService)
       .addObserver(this.historyObserver, true);
     Cc["@mozilla.org/browser/nav-bookmarks-service;1"]
       .getService(Ci.nsINavBookmarksService)
       .addObserver(this.bookmarksObserver, true);
+    PlacesUtils.observers.addListener(["bookmark-added"],
+                                      this.placesObserver.handlePlacesEvents);
 
     Services.obs.addObserver(this, LINK_BLOCKED_EVENT);
   }
 
   /**
    * setTimeout - A custom function that creates an nsITimer that can be cancelled
    *
    * @param {func} callback       A function to be executed after the timer expires
@@ -209,16 +209,18 @@ class PlacesFeed {
 
   removeObservers() {
     if (this.placesChangedTimer) {
       this.placesChangedTimer.cancel();
       this.placesChangedTimer = null;
     }
     PlacesUtils.history.removeObserver(this.historyObserver);
     PlacesUtils.bookmarks.removeObserver(this.bookmarksObserver);
+    PlacesUtils.observers.removeListener(["bookmark-added"],
+                                         this.placesObserver.handlePlacesEvents);
     Services.obs.removeObserver(this, LINK_BLOCKED_EVENT);
   }
 
   /**
    * observe - An observer for the LINK_BLOCKED_EVENT.
    *           Called when a link is blocked.
    *
    * @param  {null} subject
--- a/browser/components/newtab/test/unit/lib/PlacesFeed.test.js
+++ b/browser/components/newtab/test/unit/lib/PlacesFeed.test.js
@@ -339,24 +339,28 @@ describe("PlacesFeed", () => {
         observer.onPageChanged();
         observer.onDeleteVisits();
       });
     });
   });
 
   describe("Custom dispatch", () => {
     it("should only dispatch 1 PLACES_LINKS_CHANGED action if many onItemAdded notifications happened at once", async () => {
-      // Yes, onItemAdded has at least 8 arguments. See function definition for docs.
-      const args = [null, null, null, TYPE_BOOKMARK,
-        {spec: FAKE_BOOKMARK.url, scheme: "http"}, FAKE_BOOKMARK.bookmarkTitle,
-        FAKE_BOOKMARK.dateAdded, FAKE_BOOKMARK.bookmarkGuid, "", SOURCES.DEFAULT];
-      await feed.bookmarksObserver.onItemAdded(...args);
-      await feed.bookmarksObserver.onItemAdded(...args);
-      await feed.bookmarksObserver.onItemAdded(...args);
-      await feed.bookmarksObserver.onItemAdded(...args);
+      const args = {
+        type: "bookmark-added",
+        itemType: TYPE_BOOKMARK,
+        url: "https://" + FAKE_BOOKMARK.url,
+        title: FAKE_BOOKMARK.bookmarkTitle,
+        dateAdded: FAKE_BOOKMARK.dateAdded,
+        guid: FAKE_BOOKMARK.bookmarkGuid,
+        source: SOURCES.DEFAULT,
+      };
+      await feed.placesObserver.handlePlacesEvents([args]);
+      await feed.placesObserver.handlePlacesEvents([args]);
+      await feed.placesObserver.handlePlacesEvents([args]);
       assert.calledOnce(feed.store.dispatch.withArgs(ac.OnlyToMain({type: at.PLACES_LINKS_CHANGED})));
     });
     it("should only dispatch 1 PLACES_LINKS_CHANGED action if many onItemRemoved notifications happened at once", async () => {
       const args = [null, null, null, TYPE_BOOKMARK, {spec: "foo.com"}, "123foo", "", SOURCES.DEFAULT];
       await feed.bookmarksObserver.onItemRemoved(...args);
       await feed.bookmarksObserver.onItemRemoved(...args);
       await feed.bookmarksObserver.onItemRemoved(...args);
       await feed.bookmarksObserver.onItemRemoved(...args);
@@ -367,98 +371,152 @@ describe("PlacesFeed", () => {
       await feed.historyObserver.onDeleteURI({spec: "foo.com"});
       await feed.historyObserver.onDeleteURI({spec: "foo1.com"});
       await feed.historyObserver.onDeleteURI({spec: "foo2.com"});
 
       assert.calledOnce(feed.store.dispatch.withArgs(ac.OnlyToMain({type: at.PLACES_LINKS_CHANGED})));
     });
   });
 
+  describe("PlacesObserver", () => {
+    let dispatch;
+    let observer;
+    beforeEach(() => {
+      dispatch = sandbox.spy();
+      observer = new PlacesObserver(dispatch);
+    });
+
+    describe("#handlePlacesEvents", () => {
+      beforeEach(() => {
+      });
+
+      it("should dispatch a PLACES_BOOKMARK_ADDED action with the bookmark data - http", async () => {
+        const args = {
+          type: "bookmark-added",
+          itemType: TYPE_BOOKMARK,
+          url: "http://" + FAKE_BOOKMARK.url,
+          title: FAKE_BOOKMARK.bookmarkTitle,
+          dateAdded: FAKE_BOOKMARK.dateAdded,
+          guid: FAKE_BOOKMARK.bookmarkGuid,
+          source: SOURCES.DEFAULT,
+        };
+        await observer.handlePlacesEvents([args]);
+
+        assert.calledWith(dispatch, {type: at.PLACES_BOOKMARK_ADDED, data: FAKE_BOOKMARK});
+      });
+      it("should dispatch a PLACES_BOOKMARK_ADDED action with the bookmark data - https", async () => {
+        const args = {
+          type: "bookmark-added",
+          itemType: TYPE_BOOKMARK,
+          url: "https://" + FAKE_BOOKMARK.url,
+          title: FAKE_BOOKMARK.bookmarkTitle,
+          dateAdded: FAKE_BOOKMARK.dateAdded,
+          guid: FAKE_BOOKMARK.bookmarkGuid,
+          source: SOURCES.DEFAULT,
+        };
+        await observer.handlePlacesEvents([args]);
+
+        assert.calledWith(dispatch, {type: at.PLACES_BOOKMARK_ADDED, data: FAKE_BOOKMARK});
+      });
+      it("should not dispatch a PLACES_BOOKMARK_ADDED action - not http/https", async () => {
+        const args = {
+          type: "bookmark-added",
+          itemType: TYPE_BOOKMARK,
+          url: "places://" + FAKE_BOOKMARK.url,
+          title: FAKE_BOOKMARK.bookmarkTitle,
+          dateAdded: FAKE_BOOKMARK.dateAdded,
+          guid: FAKE_BOOKMARK.bookmarkGuid,
+          source: SOURCES.DEFAULT,
+        };
+        await observer.handlePlacesEvents([args]);
+        assert.notCalled(dispatch);
+      });
+      it("should not dispatch a PLACES_BOOKMARK_ADDED action - has IMPORT source", async () => {
+        const args = {
+          type: "bookmark-added",
+          itemType: TYPE_BOOKMARK,
+          url: "http://" + FAKE_BOOKMARK.url,
+          title: FAKE_BOOKMARK.bookmarkTitle,
+          dateAdded: FAKE_BOOKMARK.dateAdded,
+          guid: FAKE_BOOKMARK.bookmarkGuid,
+          source: SOURCES.IMPORT,
+        };
+        await observer.handlePlacesEvents([args]);
+
+        assert.notCalled(dispatch);
+      });
+      it("should not dispatch a PLACES_BOOKMARK_ADDED action - has RESTORE source", async () => {
+        const args = {
+          type: "bookmark-added",
+          itemType: TYPE_BOOKMARK,
+          url: "http://" + FAKE_BOOKMARK.url,
+          title: FAKE_BOOKMARK.bookmarkTitle,
+          dateAdded: FAKE_BOOKMARK.dateAdded,
+          guid: FAKE_BOOKMARK.bookmarkGuid,
+          source: SOURCES.RESTORE,
+        };
+        await observer.handlePlacesEvents([args]);
+
+        assert.notCalled(dispatch);
+      });
+      it("should not dispatch a PLACES_BOOKMARK_ADDED action - has RESTORE_ON_STARTUP source", async () => {
+        const args = {
+          type: "bookmark-added",
+          itemType: TYPE_BOOKMARK,
+          url: "http://" + FAKE_BOOKMARK.url,
+          title: FAKE_BOOKMARK.bookmarkTitle,
+          dateAdded: FAKE_BOOKMARK.dateAdded,
+          guid: FAKE_BOOKMARK.bookmarkGuid,
+          source: SOURCES.RESTORE_ON_STARTUP,
+        };
+        await observer.handlePlacesEvents([args]);
+
+        assert.notCalled(dispatch);
+      });
+      it("should not dispatch a PLACES_BOOKMARK_ADDED action - has SYNC source", async () => {
+        const args = {
+          type: "bookmark-added",
+          itemType: TYPE_BOOKMARK,
+          url: "http://" + FAKE_BOOKMARK.url,
+          title: FAKE_BOOKMARK.bookmarkTitle,
+          dateAdded: FAKE_BOOKMARK.dateAdded,
+          guid: FAKE_BOOKMARK.bookmarkGuid,
+          source: SOURCES.SYNC,
+        };
+        await observer.handlePlacesEvents([args]);
+
+        assert.notCalled(dispatch);
+      });
+      it("should ignore events that are not of TYPE_BOOKMARK", async () => {
+        const args = {
+          type: "bookmark-added",
+          itemType: "nottypebookmark",
+          url: "http://" + FAKE_BOOKMARK.url,
+          title: FAKE_BOOKMARK.bookmarkTitle,
+          dateAdded: FAKE_BOOKMARK.dateAdded,
+          guid: FAKE_BOOKMARK.bookmarkGuid,
+          source: SOURCES.SYNC,
+        };
+        await observer.handlePlacesEvents([args]);
+
+        assert.notCalled(dispatch);
+      });
+    });
+  });
+
   describe("BookmarksObserver", () => {
     let dispatch;
     let observer;
     beforeEach(() => {
       dispatch = sandbox.spy();
       observer = new BookmarksObserver(dispatch);
     });
     it("should have a QueryInterface property", () => {
       assert.property(observer, "QueryInterface");
     });
-    describe("#onItemAdded", () => {
-      beforeEach(() => {
-      });
-      it("should dispatch a PLACES_BOOKMARK_ADDED action with the bookmark data - http", async () => {
-        // Yes, onItemAdded has at least 8 arguments. See function definition for docs.
-        const args = [null, null, null, TYPE_BOOKMARK,
-          {spec: FAKE_BOOKMARK.url, scheme: "http"}, FAKE_BOOKMARK.bookmarkTitle,
-          FAKE_BOOKMARK.dateAdded, FAKE_BOOKMARK.bookmarkGuid, "", SOURCES.DEFAULT];
-        await observer.onItemAdded(...args);
-
-        assert.calledWith(dispatch, {type: at.PLACES_BOOKMARK_ADDED, data: FAKE_BOOKMARK});
-      });
-      it("should dispatch a PLACES_BOOKMARK_ADDED action with the bookmark data - https", async () => {
-        // Yes, onItemAdded has at least 8 arguments. See function definition for docs.
-        const args = [null, null, null, TYPE_BOOKMARK,
-          {spec: FAKE_BOOKMARK.url, scheme: "https"}, FAKE_BOOKMARK.bookmarkTitle,
-          FAKE_BOOKMARK.dateAdded, FAKE_BOOKMARK.bookmarkGuid, "", SOURCES.DEFAULT];
-        await observer.onItemAdded(...args);
-
-        assert.calledWith(dispatch, {type: at.PLACES_BOOKMARK_ADDED, data: FAKE_BOOKMARK});
-      });
-      it("should not dispatch a PLACES_BOOKMARK_ADDED action - not http/https", async () => {
-        // Yes, onItemAdded has at least 8 arguments. See function definition for docs.
-        const args = [null, null, null, TYPE_BOOKMARK,
-          {spec: FAKE_BOOKMARK.url, scheme: "places"}, FAKE_BOOKMARK.bookmarkTitle,
-          FAKE_BOOKMARK.dateAdded, FAKE_BOOKMARK.bookmarkGuid, "", SOURCES.DEFAULT];
-        await observer.onItemAdded(...args);
-
-        assert.notCalled(dispatch);
-      });
-      it("should not dispatch a PLACES_BOOKMARK_ADDED action - has IMPORT source", async () => {
-        // Yes, onItemAdded has at least 8 arguments. See function definition for docs.
-        const args = [null, null, null, TYPE_BOOKMARK,
-          {spec: FAKE_BOOKMARK.url, scheme: "http"}, FAKE_BOOKMARK.bookmarkTitle,
-          FAKE_BOOKMARK.dateAdded, FAKE_BOOKMARK.bookmarkGuid, "", SOURCES.IMPORT];
-        await observer.onItemAdded(...args);
-
-        assert.notCalled(dispatch);
-      });
-      it("should not dispatch a PLACES_BOOKMARK_ADDED action - has RESTORE source", async () => {
-        // Yes, onItemAdded has at least 8 arguments. See function definition for docs.
-        const args = [null, null, null, TYPE_BOOKMARK,
-          {spec: FAKE_BOOKMARK.url, scheme: "http"}, FAKE_BOOKMARK.bookmarkTitle,
-          FAKE_BOOKMARK.dateAdded, FAKE_BOOKMARK.bookmarkGuid, "", SOURCES.RESTORE];
-        await observer.onItemAdded(...args);
-
-        assert.notCalled(dispatch);
-      });
-      it("should not dispatch a PLACES_BOOKMARK_ADDED action - has RESTORE_ON_STARTUP source", async () => {
-        // Yes, onItemAdded has at least 8 arguments. See function definition for docs.
-        const args = [null, null, null, TYPE_BOOKMARK,
-          {spec: FAKE_BOOKMARK.url, scheme: "http"}, FAKE_BOOKMARK.bookmarkTitle,
-          FAKE_BOOKMARK.dateAdded, FAKE_BOOKMARK.bookmarkGuid, "", SOURCES.RESTORE_ON_STARTUP];
-        await observer.onItemAdded(...args);
-
-        assert.notCalled(dispatch);
-      });
-      it("should not dispatch a PLACES_BOOKMARK_ADDED action - has SYNC source", async () => {
-        // Yes, onItemAdded has at least 8 arguments. See function definition for docs.
-        const args = [null, null, null, TYPE_BOOKMARK,
-          {spec: FAKE_BOOKMARK.url, scheme: "http"}, FAKE_BOOKMARK.bookmarkTitle,
-          FAKE_BOOKMARK.dateAdded, FAKE_BOOKMARK.bookmarkGuid, "", SOURCES.SYNC];
-        await observer.onItemAdded(...args);
-
-        assert.notCalled(dispatch);
-      });
-      it("should ignore events that are not of TYPE_BOOKMARK", async () => {
-        const args = [null, null, null, "nottypebookmark"];
-        await observer.onItemAdded(...args);
-        assert.notCalled(dispatch);
-      });
-    });
     describe("#onItemRemoved", () => {
       it("should ignore events that are not of TYPE_BOOKMARK", async () => {
         await observer.onItemRemoved(null, null, null, "nottypebookmark", null, "123foo", "", SOURCES.DEFAULT);
         assert.notCalled(dispatch);
       });
       it("should not dispatch a PLACES_BOOKMARK_REMOVED action - has SYNC source", async () => {
         const args = [null, null, null, TYPE_BOOKMARK, {spec: "foo.com"}, "123foo", "", SOURCES.SYNC];
         await observer.onItemRemoved(...args);
--- a/browser/components/places/content/editBookmark.js
+++ b/browser/components/places/content/editBookmark.js
@@ -1022,17 +1022,16 @@ var gEditItemOverlay = {
     // Just setting selectItem _does not_ trigger oncommand, so we don't
     // recurse.
     PlacesUtils.bookmarks.fetch(newParentGuid).then(bm => {
       this._folderMenuList.selectedItem = this._getFolderMenuItem(newParentGuid,
                                                                   bm.title);
     });
   },
 
-  onItemAdded() {},
   onItemRemoved() { },
   onBeginUpdateBatch() { },
   onEndUpdateBatch() { },
   onItemVisited() { },
 };
 
 for (let elt of ["folderMenuList", "folderTree", "namePicker",
                  "locationField", "keywordField",
--- a/browser/components/places/tests/browser/browser_bookmarkProperties_newFolder.js
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_newFolder.js
@@ -38,21 +38,27 @@ add_task(async function test_newFolder()
 
   let folderTree = document.getElementById("editBMPanel_folderTree");
 
   // Create new folder.
   let newFolderButton = document.getElementById("editBMPanel_newFolderButton");
   newFolderButton.click();
 
   let newFolderGuid;
-  let newFolderObserver = PlacesTestUtils.waitForNotification("onItemAdded",
-    (id, parentId, index, type, uri, title, dateAdded, guid) => {
-      newFolderGuid = guid;
-      return type == PlacesUtils.bookmarks.TYPE_FOLDER;
-  });
+  let newFolderObserver =
+    PlacesTestUtils.waitForNotification("bookmark-added",
+                                        events => {
+      for (let {guid, itemType} of events) {
+        newFolderGuid = guid;
+        if (itemType == PlacesUtils.bookmarks.TYPE_FOLDER) {
+          return true;
+        }
+      }
+      return false;
+    }, "places");
 
   let menulist = document.getElementById("editBMPanel_folderMenuList");
 
   await newFolderObserver;
 
   // Wait for the folder to be created and for editing to start.
   await BrowserTestUtils.waitForCondition(() => folderTree.hasAttribute("editing"),
      "Should be in edit mode for the new folder");
--- a/browser/components/places/tests/browser/browser_bookmark_add_tags.js
+++ b/browser/components/places/tests/browser/browser_bookmark_add_tags.js
@@ -45,22 +45,20 @@ add_task(async function test_add_bookmar
     await clickBookmarkStar();
     Assert.equal(bookmarkPanelTitle.value, gNavigatorBundle.getString("editBookmarkPanel.newBookmarkTitle"), "Bookmark title is correct");
     Assert.equal(bookmarkStar.getAttribute("starred"), "true", "Page is starred");
   });
 
   // Click the bookmark star again to add tags.
   await clickBookmarkStar();
   Assert.equal(bookmarkPanelTitle.value, gNavigatorBundle.getString("editBookmarkPanel.editBookmarkTitle"), "Bookmark title is correct");
-  let promiseNotification = PlacesTestUtils.waitForNotification("onItemAdded", (id, parentId, index, type, itemUrl) => {
-    if (itemUrl !== null) {
-      return itemUrl.equals(Services.io.newURI(TEST_URL));
-    }
-    return true;
-  });
+  let promiseNotification =
+    PlacesTestUtils.waitForNotification("bookmark-added",
+                                        events => events.some(({url}) => !url || url == TEST_URL),
+                                        "places");
   await fillBookmarkTextField("editBMPanel_tagsField", "tag1", window);
   await promiseNotification;
   let bookmarks = [];
   await PlacesUtils.bookmarks.fetch({ url: TEST_URL }, bm => bookmarks.push(bm));
   Assert.equal(PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL)).length, 1, "Found the right number of tags");
   Assert.deepEqual(PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL)), ["tag1"]);
   let doneButton = document.getElementById("editBookmarkPanelDoneButton");
   await hideBookmarksPanel(() => doneButton.click());
--- a/browser/components/places/tests/browser/browser_bookmark_backup_export_import.js
+++ b/browser/components/places/tests/browser/browser_bookmark_backup_export_import.js
@@ -120,20 +120,20 @@ add_task(async function test_export_json
 add_task(async function test_import_json() {
   let libraryWindow = await promiseLibrary();
   libraryWindow.document.querySelector("#maintenanceButtonPopup #restoreFromFile").click();
 
   await promiseImportExport();
   await BrowserTestUtils.promiseAlertDialogOpen("accept");
 
   let restored = 0;
-  let promiseBookmarksRestored = PlacesTestUtils.waitForNotification("onItemAdded", () => {
-    restored++;
-    return restored === actualBookmarks.length;
-  });
+  let promiseBookmarksRestored =
+    PlacesTestUtils.waitForNotification("bookmark-added",
+                                        events => events.some(() => ++restored == actualBookmarks.length),
+                                        "places");
 
   await promiseBookmarksRestored;
   await validateImportedBookmarks(PLACES);
   await promiseLibraryClosed(libraryWindow);
 
   registerCleanupFunction(async () => {
     if (saveDir) {
       saveDir.remove(true);
--- a/browser/components/places/tests/browser/browser_bookmarksProperties.js
+++ b/browser/components/places/tests/browser/browser_bookmarksProperties.js
@@ -264,17 +264,17 @@ gTests.push({
   action: ACTION_ADD,
   historyView: SIDEBAR_HISTORY_BYLASTVISITED_VIEW,
   window: null,
 
   async setup() {
     // Add a visit.
     await PlacesTestUtils.addVisits(TEST_URL);
 
-    this._addObserver = PlacesTestUtils.waitForNotification("onItemAdded");
+    this._addObserver = PlacesTestUtils.waitForNotification("bookmark-added", null, "places");
   },
 
   selectNode(tree) {
     var visitNode = tree.view.nodeForTreeIndex(0);
     tree.selectNode(visitNode);
     Assert.equal(tree.selectedNode.uri, TEST_URL, "The correct visit has been selected");
     Assert.equal(tree.selectedNode.itemId, -1, "The selected node is not bookmarked");
   },
--- a/browser/components/places/tests/browser/browser_editBookmark_keywords.js
+++ b/browser/components/places/tests/browser/browser_editBookmark_keywords.js
@@ -3,17 +3,16 @@
 const TEST_URL = "about:blank";
 
 add_task(async function() {
   function promiseOnItemChanged() {
     return new Promise(resolve => {
       PlacesUtils.bookmarks.addObserver({
         onBeginUpdateBatch() {},
         onEndUpdateBatch() {},
-        onItemAdded() {},
         onItemRemoved() {},
         onItemVisited() {},
         onItemMoved() {},
         onItemChanged(id, property, isAnno, value) {
           PlacesUtils.bookmarks.removeObserver(this);
           resolve({ property, value });
         },
         QueryInterface: ChromeUtils.generateQI([Ci.nsINavBookmarkObserver]),
--- a/browser/components/places/tests/browser/browser_toolbar_drop_text.js
+++ b/browser/components/places/tests/browser/browser_toolbar_drop_text.js
@@ -28,18 +28,20 @@ add_task(async function test() {
    *
    * @param aEffect
    *        The effect to use for the drop operation: move, copy, or link.
    * @param aMimeType
    *        The mime type to use for the drop operation.
    */
   let simulateDragDrop = async function(aEffect, aMimeType) {
     const url = "http://www.mozilla.org/D1995729-A152-4e30-8329-469B01F30AA7";
-    let promiseItemAddedNotification = PlacesTestUtils.waitForNotification(
-      "onItemAdded", (itemId, parentId, index, type, uri, guid) => uri.spec == url);
+    let promiseItemAddedNotification =
+      PlacesTestUtils.waitForNotification("bookmark-added",
+                                          events => events.some(({url: eventUrl}) => eventUrl == url),
+                                          "places");
 
     // We use the toolbar as the drag source, as we just need almost any node
     // to simulate the drag. The actual data for the drop is passed via the
     // drag data. Note: The toolbar is used rather than another bookmark node,
     // as we need something that is immovable from a places perspective, as this
     // forces the move into a copy.
     EventUtils.synthesizeDrop(toolbar,
                               placesItems,
@@ -74,18 +76,20 @@ add_task(async function test() {
       "http://www.mozilla.org/091A88BD-5743-4C16-A005-3D2EA3A3B71E",
     ];
     let data;
     if (aMimeType == "text/x-moz-url")
       data = urls.map(spec => spec + "\n" + spec).join("\n");
     else
       data = urls.join("\n");
 
-    let promiseItemAddedNotification = PlacesTestUtils.waitForNotification(
-      "onItemAdded", (itemId, parentId, index, type, uri, guid) => uri.spec == urls[2]);
+    let promiseItemAddedNotification =
+      PlacesTestUtils.waitForNotification("bookmark-added",
+                                          events => events.some(({url}) => url == urls[2]),
+                                          "places");
 
     // See notes for EventUtils.synthesizeDrop in simulateDragDrop().
     EventUtils.synthesizeDrop(toolbar,
                               placesItems,
                               [[{type: aMimeType,
                                  data}]],
                               aEffect, window);
 
--- a/browser/components/places/tests/browser/browser_views_liveupdate.js
+++ b/browser/components/places/tests/browser/browser_views_liveupdate.js
@@ -92,17 +92,19 @@ add_task(async function test() {
   // Open bookmarks menu.
   var popup = document.getElementById("bookmarksMenuPopup");
   ok(popup, "Menu popup element exists");
   fakeOpenPopup(popup);
 
   // Open bookmarks sidebar.
   await withSidebarTree("bookmarks", async () => {
     // Add observers.
+    bookmarksObserver.handlePlacesEvents = bookmarksObserver.handlePlacesEvents.bind(bookmarksObserver);
     PlacesUtils.bookmarks.addObserver(bookmarksObserver);
+    PlacesUtils.observers.addListener(["bookmark-added"], bookmarksObserver.handlePlacesEvents);
     var addedBookmarks = [];
 
     // MENU
     info("*** Acting on menu bookmarks");
     addedBookmarks = addedBookmarks.concat(await testInFolder(PlacesUtils.bookmarks.menuGuid, "bm"));
 
     // TOOLBAR
     info("*** Acting on toolbar bookmarks");
@@ -119,16 +121,17 @@ add_task(async function test() {
       try {
         await PlacesUtils.bookmarks.remove(bm);
       } catch (ex) {}
       await bookmarksObserver.assertViewsUpdatedCorrectly();
     }
 
     // Remove observers.
     PlacesUtils.bookmarks.removeObserver(bookmarksObserver);
+    PlacesUtils.observers.removeListener(["bookmark-added"], bookmarksObserver.handlePlacesEvents);
   });
 
   // Collapse the personal toolbar if needed.
   if (wasCollapsed) {
     await promiseSetToolbarVisibility(toolbar, false);
   }
 });
 
@@ -138,20 +141,20 @@ add_task(async function test() {
  */
 var bookmarksObserver = {
   _notifications: [],
 
   QueryInterface: ChromeUtils.generateQI([
     Ci.nsINavBookmarkObserver,
   ]),
 
-  // nsINavBookmarkObserver
-  onItemAdded(itemId, folderId, index, itemType, uri, title, dataAdded, guid,
-              parentGuid) {
-    this._notifications.push(["assertItemAdded", parentGuid, guid, index]);
+  handlePlacesEvents(events) {
+    for (let {parentGuid, guid, index} of events) {
+      this._notifications.push(["assertItemAdded", parentGuid, guid, index]);
+    }
   },
 
   onItemRemoved(itemId, folderId, index, itemType, uri, guid, parentGuid) {
     this._notifications.push(["assertItemRemoved", parentGuid, guid]);
   },
 
   onItemMoved(itemId, oldFolderId, oldIndex, newFolderId, newIndex, itemType, guid,
               oldParentGuid, newParentGuid) {
--- a/browser/components/urlbar/UrlbarProvidersManager.jsm
+++ b/browser/components/urlbar/UrlbarProvidersManager.jsm
@@ -24,16 +24,21 @@ XPCOMUtils.defineLazyGetter(this, "logge
   Log.repository.getLogger("Places.Urlbar.ProvidersManager"));
 
 // List of available local providers, each is implemented in its own jsm module
 // and will track different queries internally by queryContext.
 var localProviderModules = {
   UrlbarProviderOpenTabs: "resource:///modules/UrlbarProviderOpenTabs.jsm",
 };
 
+// To improve dataflow and reduce UI work, when a match is added by a
+// non-immediate provider, we notify it to the controller after a delay, so
+// that we can chunk matches coming in that timeframe into a single call.
+const CHUNK_MATCHES_DELAY_MS = 16;
+
 /**
  * Class used to create a manager.
  * The manager is responsible to keep a list of providers, instantiate query
  * objects and pass those to the providers.
  */
 class ProvidersManager {
   constructor() {
     // Tracks the available providers.
@@ -79,17 +84,17 @@ class ProvidersManager {
 
   /**
    * Starts querying.
    * @param {object} queryContext The query context object
    * @param {object} controller a UrlbarController instance
    */
   async startQuery(queryContext, controller) {
     logger.info(`Query start ${queryContext.searchString}`);
-    let query = Object.seal(new Query(queryContext, controller, this.providers));
+    let query = new Query(queryContext, controller, this.providers);
     this.queries.set(queryContext, query);
     await query.start();
   }
 
   /**
    * Cancels a running query.
    * @param {object} queryContext
    */
@@ -143,19 +148,16 @@ class Query {
    * @param {object} providers
    *        Map of all the providers by type and name
    */
   constructor(queryContext, controller, providers) {
     this.context = queryContext;
     this.context.results = [];
     this.controller = controller;
     this.providers = providers;
-    // Track the delay timer.
-    this.sleepResolve = Promise.resolve();
-    this.sleepTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
     this.started = false;
     this.canceled = false;
     this.complete = false;
   }
 
   /**
    * Starts querying.
    */
@@ -169,69 +171,149 @@ class Query {
     let promises = [];
     for (let provider of this.providers.get(UrlbarUtils.PROVIDER_TYPE.IMMEDIATE).values()) {
       if (this.canceled) {
         break;
       }
       promises.push(provider.startQuery(this.context, this.add));
     }
 
-    await new Promise(resolve => {
-      let time = UrlbarPrefs.get("delay");
-      this.sleepResolve = resolve;
-      this.sleepTimer.initWithCallback(resolve, time, Ci.nsITimer.TYPE_ONE_SHOT);
-    });
+    // Tracks the delay timer. We will fire (in this specific case, cancel would
+    // do the same, since the callback is empty) the timer when the search is
+    // canceled, unblocking start().
+    this._sleepTimer = new SkippableTimer(() => {}, UrlbarPrefs.get("delay"));
+    await this._sleepTimer.promise;
 
     for (let providerType of [UrlbarUtils.PROVIDER_TYPE.NETWORK,
                               UrlbarUtils.PROVIDER_TYPE.PROFILE,
                               UrlbarUtils.PROVIDER_TYPE.EXTENSION]) {
       for (let provider of this.providers.get(providerType).values()) {
         if (this.canceled) {
           break;
         }
         promises.push(provider.startQuery(this.context, this.add.bind(this)));
       }
     }
 
     await Promise.all(promises.map(p => p.catch(Cu.reportError)));
 
+    if (this._chunkTimer) {
+      // All the providers are done returning results, so we can stop chunking.
+      await this._chunkTimer.fire();
+    }
+
     // Nothing should be failing above, since we catch all the promises, thus
     // this is not in a finally for now.
     this.complete = true;
   }
 
   /**
    * Cancels this query.
    * @note Invoking cancel multiple times is a no-op.
    */
   cancel() {
     if (this.canceled) {
       return;
     }
     this.canceled = true;
-    this.sleepTimer.cancel();
     for (let providers of this.providers.values()) {
       for (let provider of providers.values()) {
         provider.cancelQuery(this.context);
       }
     }
-    this.sleepResolve();
+    if (this._chunkTimer) {
+      this._chunkTimer.cancel().catch(Cu.reportError);
+    }
+    if (this._sleepTimer) {
+      this._sleepTimer.fire().catch(Cu.reportError);
+    }
   }
 
   /**
    * Adds a match returned from a provider to the results set.
    * @param {object} provider
    * @param {object} match
    */
   add(provider, match) {
     // Stop returning results as soon as we've been canceled.
     if (this.canceled) {
       return;
     }
-    // TODO:
-    //  * coalesce results in timed chunks: we don't want to notify every single
-    //    result as soon as it arrives, we'll rather collect results for a few
-    //    ms, then send them
-    //  * pass results to a muxer before sending them back to the controller.
     this.context.results.push(match);
-    this.controller.receiveResults(this.context);
+
+
+    let notifyResults = () => {
+      if (this._chunkTimer) {
+        this._chunkTimer.cancel().catch(Cu.reportError);
+        delete this._chunkTimer;
+      }
+      // TODO:
+      //  * pass results to a muxer before sending them back to the controller.
+      this.controller.receiveResults(this.context);
+    };
+
+    // If the provider is not of immediate type, chunk results, to improve the
+    // dataflow and reduce UI flicker.
+    if (provider.type == UrlbarUtils.PROVIDER_TYPE.IMMEDIATE) {
+      notifyResults();
+    } else if (!this._chunkTimer) {
+      this._chunkTimer = new SkippableTimer(notifyResults, CHUNK_MATCHES_DELAY_MS);
+    }
   }
 }
+
+/**
+ * Class used to create a timer that can be manually fired, to immediately
+ * invoke the callback, or canceled, as necessary.
+ * Examples:
+ *   let timer = new SkippableTimer();
+ *   // Invokes the callback immediately without waiting for the delay.
+ *   await timer.fire();
+ *   // Cancel the timer, the callback won't be invoked.
+ *   await timer.cancel();
+ *   // Wait for the timer to have elapsed.
+ *   await timer.promise;
+ */
+class SkippableTimer {
+  /**
+   * Creates a skippable timer for the given callback and time.
+   * @param {function} callback To be invoked when requested
+   * @param {number} time A delay in milliseconds to wait for
+   */
+  constructor(callback, time) {
+    let timerPromise = new Promise(resolve => {
+      this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+      this._timer.initWithCallback(() => {
+        logger.debug(`Elapsed ${time}ms timer`);
+        resolve();
+      }, time, Ci.nsITimer.TYPE_ONE_SHOT);
+      logger.debug(`Started ${time}ms timer`);
+    });
+
+    let firePromise = new Promise(resolve => {
+      this.fire = () => {
+        logger.debug(`Skipped ${time}ms timer`);
+        resolve();
+        return this.promise;
+      };
+    });
+
+    this.promise = Promise.race([timerPromise, firePromise]).then(() => {
+      // If we've been canceled, don't call back.
+      if (this._timer) {
+        callback();
+      }
+    });
+  }
+
+  /**
+   * Allows to cancel the timer and the callback won't be invoked.
+   * It is not strictly necessary to await for this, the promise can just be
+   * used to ensure all the internal work is complete.
+   * @returns {promise} Resolved once all the cancelation work is complete.
+   */
+  cancel() {
+    logger.debug(`Canceling timer for ${this._timer.delay}ms`);
+    this._timer.cancel();
+    delete this._timer;
+    return this.fire();
+  }
+}
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -781,16 +781,17 @@ you can use these alternative items. Oth
 <!ENTITY editBookmark.done.label                     "Done">
 <!ENTITY editBookmark.showForNewBookmarks.label       "Show editor when saving">
 <!ENTITY editBookmark.showForNewBookmarks.accesskey   "S">
 
 <!-- LOCALIZATION NOTE (identity.securityView.label)
      This is the header of the security subview in the Site Identity panel. -->
 <!ENTITY identity.securityView.label "Site Security">
 
+<!ENTITY identity.connection "Connection">
 <!ENTITY identity.connectionSecure "Secure Connection">
 <!ENTITY identity.connectionNotSecure "Connection Is Not Secure">
 <!ENTITY identity.connectionFile "This page is stored on your computer.">
 <!ENTITY identity.connectionVerified2 "You are securely connected to this site, owned by:">
 <!ENTITY identity.connectionInternal "This is a secure &brandShortName; page.">
 <!ENTITY identity.extensionPage "This page is loaded from an extension.">
 <!ENTITY identity.insecureLoginForms2 "Logins entered on this page could be compromised.">
 
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -485,16 +485,19 @@ webauthn.cancel=Cancel
 webauthn.cancel.accesskey=c
 webauthn.proceed=Proceed
 webauthn.proceed.accesskey=p
 webauthn.anonymize=Anonymize anyway
 
 # Spoof Accept-Language prompt
 privacy.spoof_english=Changing your language setting to English will make you more difficult to identify and enhance your privacy. Do you want to request English language versions of web pages?
 
+# LOCALIZATION NOTE (identity.headerWithHost):
+# %S is the hostname of the site that is being displayed.
+identity.headerWithHost=Site Information for %S
 identity.identified.verifier=Verified by: %S
 identity.identified.verified_by_you=You have added a security exception for this site.
 identity.identified.state_and_country=%S, %S
 
 # LOCALIZATION NOTE (identity.notSecure.label):
 # Keep this string as short as possible, this is displayed in the URL bar
 # use a synonym for "safe" or "private" if "secure" is too long.
 identity.notSecure.label=Not Secure
--- a/browser/themes/shared/controlcenter/panel.inc.css
+++ b/browser/themes/shared/controlcenter/panel.inc.css
@@ -1,15 +1,13 @@
 %if 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/. */
 %endif
-%filter substitution
-%define identityPopupExpanderWidth 38px
 
 /* Hide all conditional elements by default. */
 :-moz-any([when-connection],[when-mixedcontent],[when-ciphers],[when-loginforms]) {
   display: none;
 }
 
 /* This is used by screenshots tests to hide intermittently different
  * identity popup shadows (see bug 1425253). */
@@ -69,17 +67,17 @@
   fill-opacity: .6;
 }
 
 #identity-popup-mainView {
   min-width: 33em;
   max-width: 33em;
 }
 
-.identity-popup-section:not(:first-child) {
+.identity-popup-section {
   border-top: 1px solid var(--panel-separator-color);
 }
 
 .identity-popup-security-content,
 #identity-popup-permissions-content,
 #identity-popup-content-blocking-content {
   background-repeat: no-repeat;
   background-position: 1em 1em;
@@ -100,17 +98,17 @@
 }
 
 /* EXPAND BUTTON */
 
 .identity-popup-expander {
   margin: 0;
   padding: 4px 0;
   min-width: auto;
-  width: @identityPopupExpanderWidth@;
+  width: 38px;
   border-style: none;
   -moz-appearance: none;
   background: url("chrome://browser/skin/arrow-left.svg") center no-repeat;
   background-size: 16px, auto;
   -moz-context-properties: fill;
   fill: currentColor;
   color: inherit;
 }
@@ -142,52 +140,73 @@
 }
 
 .identity-popup-preferences-button > .toolbarbutton-text {
   display: none;
 }
 
 /* CONTENT */
 
+#identity-popup-mainView-panel-header > label,
+#identity-popup-securityView > .panel-header,
+#identity-popup-breakageReportView > .panel-header,
 #identity-popup-content-blocking-report-breakage,
 #identity-popup-content-blocking-disabled-label,
 .identity-popup-content-blocking-category-label,
 .identity-popup-content-blocking-category-state-label,
 .identity-popup-content-blocking-category-add-blocking,
 .identity-popup-permission-label,
 .identity-popup-permission-state-label,
 .identity-popup-security-content > description,
 #identity-popup-security-descriptions > description,
 #identity-popup-securityView-body > description,
 #identity-popup-permissions-content > description,
 #identity-popup-content-blocking-content > description {
   font-size: 110%;
   margin: 0;
 }
 
+#identity-popup-mainView-panel-header {
+  padding: 4px 1em;
+  min-height: 40px;
+  -moz-box-pack: center;
+  -moz-box-align: center;
+}
+
+#identity-popup-mainView-panel-header-span {
+  display: inline-block;
+  font-weight: 600;
+  text-align: center;
+  overflow-wrap: break-word;
+  /* This is needed for the overflow-wrap to work correctly.
+   * 33em is the panel width, panel-header has 1em padding on each side. */
+  max-width: calc(33rem - 2em);
+}
+
 #identity-popup-permissions-content > description,
 #identity-popup-content-blocking-content > description {
   color: var(--panel-disabled-color);
 }
 
 /* This element needs the pre-wrap because we add newlines to it in the code. */
 #identity-popup-content-supplemental {
   white-space: pre-wrap;
 }
 
 .identity-popup-headline {
   margin: 3px 0 4px;
   font-size: 150%;
 }
 
-.identity-popup-host {
-  word-wrap: break-word;
-  /* 1em + 2em + 24px is .identity-popup-security-content padding
-   * 30em is the panel width */
-  max-width: calc(30rem - 3rem - 24px - @identityPopupExpanderWidth@);
+#identity-popup-host {
+  overflow-wrap: break-word;
+  /* This is needed for the overflow-wrap to work correctly.
+   * 1em + 2em + 24px is .identity-popup-security-content padding
+   * 33em is the panel width */
+  max-width: calc(33rem - 3rem - 24px);
 }
 
 .identity-popup-warning-gray {
   padding-inline-start: 24px;
   background: url(chrome://browser/skin/controlcenter/warning.svg) no-repeat 0 50%;
   fill: #808080;
   stroke: #fff;
   -moz-context-properties: fill, stroke;
--- a/build/docs/mozbuild-files.rst
+++ b/build/docs/mozbuild-files.rst
@@ -142,17 +142,17 @@ Filesystem reading mode is utilized to p
 Technical Details
 -----------------
 
 The code for reading ``moz.build`` files lives in
 :py:mod:`mozbuild.frontend.reader`. The Python sandboxes evaluation results
 (:py:class:`mozbuild.frontend.context.Context`) are passed into
 :py:mod:`mozbuild.frontend.emitter`, which converts them to classes defined
 in :py:mod:`mozbuild.frontend.data`. Each class in this module defines a
-domain-specific component of tree metdata. e.g. there will be separate
+domain-specific component of tree metadata. e.g. there will be separate
 classes that represent a JavaScript file vs a compiled C++ file or test
 manifests. This means downstream consumers of this data can filter on class
 types to only consume what they are interested in.
 
 There is no well-defined mapping between ``moz.build`` file instances
 and the number of :py:mod:`mozbuild.frontend.data` classes derived from
 each. Depending on the content of the ``moz.build`` file, there may be 1
 object derived or 100.
--- a/build/moz.configure/rust.configure
+++ b/build/moz.configure/rust.configure
@@ -289,17 +289,17 @@ cbindgen = check_prog('CBINDGEN', add_ru
                       when=depends(build_project)
                       (lambda build_project: build_project != 'js'))
 
 
 @depends_if(cbindgen)
 @checking('cbindgen version')
 @imports(_from='textwrap', _import='dedent')
 def cbindgen_version(cbindgen):
-    cbindgen_min_version = Version('0.6.2')
+    cbindgen_min_version = Version('0.6.4')
 
     # cbindgen x.y.z
     version = Version(check_cmd_output(cbindgen, '--version').strip().split(" ")[1])
 
     if version < cbindgen_min_version:
         die(dedent('''\
         cbindgen version {} is too old. At least version {} is required.
 
--- a/build/valgrind/x86_64-pc-linux-gnu.sup
+++ b/build/valgrind/x86_64-pc-linux-gnu.sup
@@ -521,16 +521,31 @@
    Bug 1479055: style::properties::longhands::clip_path::cascade_property
    Memcheck:Cond
    fun:_ZN5style10properties9longhands9clip_path16cascade_property*
    fun:_ZN5style10properties13cascade_rules*
    fun:_ZN109_$LT$style*style_resolver*cascade_style_and_visited*
    fun:_ZN109_$LT$style*cascade_primary_style*
 }
 
+# Another suppression for Stylo, October 2018.  See bug 1496486.
+# Conditional jump or move depends on uninitialised value(s)
+#    at 0x108E2931: style::properties::longhands::font_language_override::cascade_property+81 (font.rs:2095)
+#    by 0x107E95B4: style::properties::cascade::Cascade::apply_properties+580 (cascade.rs:463)
+#    by 0x107E82D8: style::properties::cascade::cascade_rules+2440 (cascade.rs:303)
+#    by 0x107E7206: <style::style_resolver::StyleResolverForElement<'a, 'ctx, 'le, E>>::cascade_style_and_visited+310 (cascade.rs:93)
+#  Uninitialised value was created by a stack allocation
+#    at 0x10A42A40: style::properties::shorthands::font::parse_value (font.rs:376)
+{
+   Bug 1496486: style::properties::longhands::font_language_override::cascade_property
+   Memcheck:Cond
+   fun:_ZN5style10properties9longhands22font_language_override16cascade_property*
+   fun:_ZN5style10properties7cascade7Cascade16apply_properties*
+   fun:_ZN5style10properties7cascade13cascade_rules*
+}
 
 ###################################################
 #  For valgrind-mochitest ("tc-M-V [tier 2]") runs on taskcluster.
 #  See bug 1248365.
 #  These are specific to Ubuntu 12.04.5, 64-bit.
 ###################################################
 
 
new file mode 100644
--- /dev/null
+++ b/dom/base/PlacesBookmark.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_PlacesBookmark_h
+#define mozilla_dom_PlacesBookmark_h
+
+#include "mozilla/dom/PlacesEvent.h"
+
+namespace mozilla {
+namespace dom {
+
+class PlacesBookmark : public PlacesEvent
+{
+public:
+  explicit PlacesBookmark(PlacesEventType aEventType) : PlacesEvent(aEventType) {}
+
+  JSObject*
+  WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override
+  {
+    return PlacesBookmark_Binding::Wrap(aCx, this, aGivenProto);
+  }
+
+  const PlacesBookmark* AsPlacesBookmark() const override { return this; }
+
+  unsigned short ItemType() { return mItemType; }
+  int64_t Id() { return mId; }
+  int64_t ParentId() { return mParentId; }
+  void GetUrl(nsString& aUrl) { aUrl = mUrl; }
+  void GetGuid(nsCString& aGuid) { aGuid = mGuid; }
+  void GetParentGuid(nsCString& aParentGuid) { aParentGuid = mParentGuid; }
+  uint16_t Source() { return mSource; }
+  bool IsTagging() { return mIsTagging; }
+
+  unsigned short mItemType;
+  int64_t mId;
+  int64_t mParentId;
+  nsString mUrl;
+  nsCString mGuid;
+  nsCString mParentGuid;
+  uint16_t mSource;
+  bool mIsTagging;
+
+protected:
+  virtual ~PlacesBookmark() = default;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PlacesBookmark_h
new file mode 100644
--- /dev/null
+++ b/dom/base/PlacesBookmarkAddition.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_PlacesBookmarkAddition_h
+#define mozilla_dom_PlacesBookmarkAddition_h
+
+#include "mozilla/dom/PlacesBookmark.h"
+
+namespace mozilla {
+namespace dom {
+
+class PlacesBookmarkAddition final : public PlacesBookmark
+{
+public:
+  explicit PlacesBookmarkAddition() : PlacesBookmark(PlacesEventType::Bookmark_added) {}
+
+  static already_AddRefed<PlacesBookmarkAddition>
+  Constructor(const GlobalObject& aGlobal,
+              const PlacesBookmarkAdditionInit& aInitDict,
+              ErrorResult& aRv) {
+    RefPtr<PlacesBookmarkAddition> event = new PlacesBookmarkAddition();
+    event->mItemType = aInitDict.mItemType;
+    event->mId = aInitDict.mId;
+    event->mParentId = aInitDict.mParentId;
+    event->mIndex = aInitDict.mIndex;
+    event->mUrl = aInitDict.mUrl;
+    event->mTitle = aInitDict.mTitle;
+    event->mDateAdded = aInitDict.mDateAdded;
+    event->mGuid = aInitDict.mGuid;
+    event->mParentGuid = aInitDict.mParentGuid;
+    event->mSource = aInitDict.mSource;
+    event->mIsTagging = aInitDict.mIsTagging;
+    return event.forget();
+  }
+
+  JSObject*
+  WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override
+  {
+    return PlacesBookmarkAddition_Binding::Wrap(aCx, this, aGivenProto);
+  }
+
+  const PlacesBookmarkAddition* AsPlacesBookmarkAddition() const override { return this; }
+
+  int32_t Index() { return mIndex; }
+  void GetTitle(nsString& aTitle) { aTitle = mTitle; }
+  uint64_t DateAdded() { return mDateAdded; }
+
+  int32_t mIndex;
+  nsString mTitle;
+  uint64_t mDateAdded;
+
+private:
+  ~PlacesBookmarkAddition() = default;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PlacesBookmarkAddition_h
--- a/dom/base/PlacesEvent.h
+++ b/dom/base/PlacesEvent.h
@@ -32,16 +32,18 @@ public:
   nsISupports* GetParentObject() const;
 
   JSObject*
   WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   PlacesEventType Type() const { return mType; }
 
   virtual const PlacesVisit* AsPlacesVisit() const { return nullptr; }
+  virtual const PlacesBookmark* AsPlacesBookmark() const { return nullptr; }
+  virtual const PlacesBookmarkAddition* AsPlacesBookmarkAddition() const { return nullptr; }
 protected:
   virtual ~PlacesEvent() = default;
   PlacesEventType mType;
 };
 
 } // namespace dom
 } // namespace mozilla
 
--- a/dom/base/PlacesObservers.cpp
+++ b/dom/base/PlacesObservers.cpp
@@ -34,28 +34,28 @@ template <class T>
 using FlaggedArray = nsTArray<Flagged<T>>;
 
 template <class T>
 struct ListenerCollection
 {
   static StaticAutoPtr<FlaggedArray<T>> gListeners;
   static StaticAutoPtr<FlaggedArray<T>> gListenersToRemove;
 
-  static FlaggedArray<T>* GetListeners() {
+  static FlaggedArray<T>* GetListeners(bool aDoNotInit = false) {
     MOZ_ASSERT(NS_IsMainThread());
-    if (!gListeners) {
+    if (!gListeners && !aDoNotInit) {
       gListeners = new FlaggedArray<T>();
       ClearOnShutdown(&gListeners);
     }
     return gListeners;
   }
 
-  static FlaggedArray<T>* GetListenersToRemove() {
+  static FlaggedArray<T>* GetListenersToRemove(bool aDoNotInit = false) {
     MOZ_ASSERT(NS_IsMainThread());
-    if (!gListenersToRemove) {
+    if (!gListenersToRemove && !aDoNotInit) {
       gListenersToRemove = new FlaggedArray<T>();
       ClearOnShutdown(&gListenersToRemove);
     }
     return gListenersToRemove;
   }
 };
 
 template <class T>
@@ -221,67 +221,76 @@ PlacesObservers::RemoveListener(const ns
     RemoveListener(flags, aCallback);
   }
 }
 
 void
 PlacesObservers::RemoveListener(uint32_t aFlags,
                                 PlacesEventCallback& aCallback)
 {
-  FlaggedArray<RefPtr<PlacesEventCallback>>& listeners =
-    *JSListeners::GetListeners();
-  for (uint32_t i = 0; i < listeners.Length(); i++) {
-    Flagged<RefPtr<PlacesEventCallback>>& l = listeners[i];
+  FlaggedArray<RefPtr<PlacesEventCallback>>* listeners =
+    JSListeners::GetListeners(/* aDoNotInit: */ true);
+  if (!listeners) {
+    return;
+  }
+  for (uint32_t i = 0; i < listeners->Length(); i++) {
+    Flagged<RefPtr<PlacesEventCallback>>& l = listeners->ElementAt(i);
     if (!(*l.value == aCallback)) {
       continue;
     }
-    if (l.flags == aFlags) {
-      listeners.RemoveElementAt(i);
+    if (l.flags == (aFlags & l.flags)) {
+      listeners->RemoveElementAt(i);
       i--;
     } else {
       l.flags &= ~aFlags;
     }
   }
 }
 
 void
 PlacesObservers::RemoveListener(uint32_t aFlags,
                                 PlacesWeakCallbackWrapper& aCallback)
 {
-  FlaggedArray<WeakPtr<PlacesWeakCallbackWrapper>>& listeners =
-    *WeakJSListeners::GetListeners();
-  for (uint32_t i = 0; i < listeners.Length(); i++) {
-    Flagged<WeakPtr<PlacesWeakCallbackWrapper>>& l = listeners[i];
+  FlaggedArray<WeakPtr<PlacesWeakCallbackWrapper>>* listeners =
+    WeakJSListeners::GetListeners(/* aDoNotInit: */ true);
+  if (!listeners) {
+    return;
+  }
+  for (uint32_t i = 0; i < listeners->Length(); i++) {
+    Flagged<WeakPtr<PlacesWeakCallbackWrapper>>& l = listeners->ElementAt(i);
     RefPtr<PlacesWeakCallbackWrapper> unwrapped = l.value.get();
     if (unwrapped != &aCallback) {
       continue;
     }
-    if (l.flags == aFlags) {
-      listeners.RemoveElementAt(i);
+    if (l.flags == (aFlags & l.flags)) {
+      listeners->RemoveElementAt(i);
       i--;
     } else {
       l.flags &= ~aFlags;
     }
   }
 }
 
 void
 PlacesObservers::RemoveListener(uint32_t aFlags,
                                 places::INativePlacesEventCallback* aCallback)
 {
-  FlaggedArray<WeakPtr<places::INativePlacesEventCallback>>& listeners =
-    *WeakNativeListeners::GetListeners();
-  for (uint32_t i = 0; i < listeners.Length(); i++) {
-    Flagged<WeakPtr<places::INativePlacesEventCallback>>& l = listeners[i];
+  FlaggedArray<WeakPtr<places::INativePlacesEventCallback>>* listeners =
+    WeakNativeListeners::GetListeners(/* aDoNotInit: */ true);
+  if (!listeners) {
+    return;
+  }
+  for (uint32_t i = 0; i < listeners->Length(); i++) {
+    Flagged<WeakPtr<places::INativePlacesEventCallback>>& l = listeners->ElementAt(i);
     RefPtr<places::INativePlacesEventCallback> unwrapped = l.value.get();
     if (unwrapped != aCallback) {
       continue;
     }
-    if (l.flags == aFlags) {
-      listeners.RemoveElementAt(i);
+    if (l.flags == (aFlags & l.flags)) {
+      listeners->RemoveElementAt(i);
       i--;
     } else {
       l.flags &= ~aFlags;
     }
   }
 }
 
 void
@@ -323,28 +332,31 @@ PlacesObservers::NotifyListeners(const S
     });
 
   auto& listenersToRemove = *JSListeners::GetListenersToRemove();
   if (listenersToRemove.Length() > 0) {
     for (auto& listener : listenersToRemove) {
       RemoveListener(listener.flags, *listener.value);
     }
   }
+  listenersToRemove.Clear();
 
   auto& weakListenersToRemove = *WeakJSListeners::GetListenersToRemove();
   if (weakListenersToRemove.Length() > 0) {
     for (auto& listener : weakListenersToRemove) {
       RemoveListener(listener.flags, *listener.value.get());
     }
   }
+  weakListenersToRemove.Clear();
 
   auto& nativeListenersToRemove = *WeakNativeListeners::GetListenersToRemove();
   if (nativeListenersToRemove.Length() > 0) {
     for (auto& listener : nativeListenersToRemove) {
       RemoveListener(listener.flags, listener.value.get());
     }
   }
+  nativeListenersToRemove.Clear();
 
   gCallingListeners = false;
 }
 
 } // namespace dom
 } // namespace mozilla
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -200,16 +200,18 @@ EXPORTS.mozilla.dom += [
     'MimeType.h',
     'MozQueryInterface.h',
     'NameSpaceConstants.h',
     'Navigator.h',
     'NodeInfo.h',
     'NodeInfoInlines.h',
     'NodeIterator.h',
     'ParentProcessMessageManager.h',
+    'PlacesBookmark.h',
+    'PlacesBookmarkAddition.h',
     'PlacesEvent.h',
     'PlacesObservers.h',
     'PlacesVisit.h',
     'PlacesWeakCallbackWrapper.h',
     'Pose.h',
     'ProcessMessageManager.h',
     'ResponsiveImageSelector.h',
     'SameProcessMessageQueue.h',
--- a/dom/chrome-webidl/PlacesEvent.webidl
+++ b/dom/chrome-webidl/PlacesEvent.webidl
@@ -1,15 +1,20 @@
 enum PlacesEventType {
   "none",
 
   /**
    * data: PlacesVisit. Fired whenever a page is visited.
    */
   "page-visited",
+  /**
+   * data: PlacesBookmarkAddition. Fired whenever a bookmark
+   * (or a bookmark folder/separator) is created.
+   */
+  "bookmark-added",
 };
 
 [ChromeOnly, Exposed=(Window,System)]
 interface PlacesEvent {
   readonly attribute PlacesEventType type;
 };
 
 [ChromeOnly, Exposed=(Window,System)]
@@ -62,8 +67,88 @@ interface PlacesVisit : PlacesEvent {
   readonly attribute unsigned long typedCount;
 
   /**
    * The last known title of the page. Might not be from the current visit,
    * and might be null if it is not known.
    */
   readonly attribute DOMString? lastKnownTitle;
 };
+
+/**
+ * Base class for properties that are common to all bookmark events.
+ */
+[ChromeOnly, Exposed=(Window,System)]
+interface PlacesBookmark : PlacesEvent {
+  /**
+   * The id of the item.
+   */
+  readonly attribute long long id;
+
+  /**
+   * The id of the folder to which the item belongs.
+   */
+  readonly attribute long long parentId;
+
+  /**
+   * The type of the added item (see TYPE_* constants in nsINavBooksService.idl).
+   */
+  readonly attribute unsigned short itemType;
+
+  /**
+   * The URI of the added item if it was TYPE_BOOKMARK, "" otherwise.
+   */
+  readonly attribute DOMString url;
+
+  /**
+   * The unique ID associated with the item.
+   */
+  readonly attribute ByteString guid;
+
+  /**
+   * The unique ID associated with the item's parent.
+   */
+  readonly attribute ByteString parentGuid;
+
+  /**
+   * A change source constant from nsINavBookmarksService::SOURCE_*,
+   * passed to the method that notifies the observer.
+   */
+  readonly attribute unsigned short source;
+
+  /**
+   * True if the item is a tag or a tag folder.
+   * NOTE: this will go away with bug 424160.
+   */
+  readonly attribute boolean isTagging;
+};
+
+dictionary PlacesBookmarkAdditionInit {
+  required long long id;
+  required long long parentId;
+  required unsigned short itemType;
+  required DOMString url;
+  required ByteString guid;
+  required ByteString parentGuid;
+  required unsigned short source;
+  required long index;
+  required DOMString title;
+  required unsigned long long dateAdded;
+  required boolean isTagging;
+};
+
+[ChromeOnly, Exposed=(Window,System), Constructor(PlacesBookmarkAdditionInit initDict)]
+interface PlacesBookmarkAddition : PlacesBookmark {
+  /**
+   * The item's index in the folder.
+   */
+  readonly attribute long index;
+
+  /**
+   * The title of the added item.
+   */
+  readonly attribute DOMString title;
+
+  /**
+   * The time that the item was added, in milliseconds from the epoch.
+   */
+  readonly attribute unsigned long long dateAdded;
+};
--- a/dom/locales/en-US/chrome/dom/dom.properties
+++ b/dom/locales/en-US/chrome/dom/dom.properties
@@ -103,16 +103,18 @@ MediaDecodeAudioDataNoAudio=The buffer p
 MediaElementAudioSourceNodeCrossOrigin=The HTMLMediaElement passed to createMediaElementSource has a cross-origin resource, the node will output silence.
 # LOCALIZATION NOTE: Do not translate MediaStream and createMediaStreamSource.
 MediaStreamAudioSourceNodeCrossOrigin=The MediaStream passed to createMediaStreamSource has a cross-origin resource, the node will output silence.
 # LOCALIZATION NOTE: Do not translate HTMLMediaElement and MediaStream.
 MediaElementAudioCaptureOfMediaStreamError=The captured HTMLMediaElement is playing a MediaStream. Applying volume or mute status is not currently supported.
 MediaLoadExhaustedCandidates=All candidate resources failed to load. Media load paused.
 MediaLoadSourceMissingSrc=<source> element has no “src” attribute. Media resource load failed.
 MediaStreamAudioSourceNodeDifferentRate=Connecting AudioNodes from AudioContexts with different sample-rate is currently not supported.
+# LOCALIZATION NOTE: Do not translate ConvolverNode
+ConvolverNodeAllocationError=Out-of-memory error when instantiating a ConvolverNode: the node will output silence.
 # LOCALIZATION NOTE: %1$S is the Http error code the server returned (e.g. 404, 500, etc), %2$S is the URL of the media resource which failed to load.
 MediaLoadHttpError=HTTP load failed with status %1$S. Load of media resource %2$S failed.
 # LOCALIZATION NOTE: %S is the URL of the media resource which failed to load.
 MediaLoadInvalidURI=Invalid URI. Load of media resource %S failed.
 # LOCALIZATION NOTE: %1$S is the media resource's format/codec type (basically equivalent to the file type, e.g. MP4,AVI,WMV,MOV etc), %2$S is the URL of the media resource which failed to load.
 MediaLoadUnsupportedTypeAttribute=Specified “type” attribute of “%1$S” is not supported. Load of media resource %2$S failed.
 # LOCALIZATION NOTE: %1$S is the "media" attribute value of the <source> element. It is a media query. %2$S is the URL of the media resource which failed to load.
 MediaLoadSourceMediaNotMatched=Specified “media” attribute of “%1$S” does not match the environment. Load of media resource %2$S failed.
--- a/dom/media/webaudio/ConvolverNode.cpp
+++ b/dom/media/webaudio/ConvolverNode.cpp
@@ -23,18 +23,19 @@ NS_INTERFACE_MAP_END_INHERITING(AudioNod
 
 NS_IMPL_ADDREF_INHERITED(ConvolverNode, AudioNode)
 NS_IMPL_RELEASE_INHERITED(ConvolverNode, AudioNode)
 
 class ConvolverNodeEngine final : public AudioNodeEngine
 {
   typedef PlayingRefChangeHandler PlayingRefChanged;
 public:
-  ConvolverNodeEngine(AudioNode* aNode, bool aNormalize)
+  ConvolverNodeEngine(AudioNode* aNode, bool aNormalize, uint64_t aWindowID)
     : AudioNodeEngine(aNode)
+    , mWindowID(aWindowID)
     , mUseBackgroundThreads(!aNode->Context()->IsOffline())
     , mNormalize(aNormalize)
   {
   }
 
   // Indicates how the right output channel is generated.
   enum class RightConvolverMode {
     // A right convolver is always used when there is more than one impulse
@@ -135,18 +136,26 @@ public:
     }
 
     // Assume for now that convolution of channel difference is not required.
     // Direct may change to Difference during processing.
     mRightConvolverMode =
       aBuffer.ChannelCount() == 1 ? RightConvolverMode::Direct
       : RightConvolverMode::Always;
 
+    bool allocationFailure = false;
     mReverb = new WebCore::Reverb(aBuffer, MaxFFTSize, mUseBackgroundThreads,
-                                  mNormalize, mSampleRate);
+                                  mNormalize, mSampleRate, &allocationFailure);
+    if (allocationFailure) {
+      // If the allocation failed, this AudioNodeEngine is going to output
+      // silence. This is signaled to developers in the console.
+      mReverb = nullptr;
+      WebAudioUtils::LogToDeveloperConsole(mWindowID,
+                                           "ConvolverNodeAllocationError");
+    }
   }
 
   void AllocateReverbInput(const AudioBlock& aInput,
                            uint32_t aTotalChannelCount)
   {
     uint32_t inputChannelCount = aInput.ChannelCount();
     MOZ_ASSERT(inputChannelCount <= aTotalChannelCount);
     mReverbInput.AllocateChannels(aTotalChannelCount);
@@ -191,16 +200,17 @@ public:
   {
     return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
   }
 
 private:
   // Keeping mReverbInput across process calls avoids unnecessary reallocation.
   AudioBlock mReverbInput;
   nsAutoPtr<WebCore::Reverb> mReverb;
+  uint64_t mWindowID;
   // Tracks samples of the tail remaining to be output.  INT32_MIN is a
   // special value to indicate that the end of any previous tail has been
   // handled.
   int32_t mRemainingLeftOutput = INT32_MIN;
   // mRemainingRightOutput and mRemainingRightHistory are only used when
   // mRightOutputMode != Always.  There is no special handling required at the
   // end of tail times and so INT32_MIN is not used.
   // mRemainingRightOutput tracks how much longer this node needs to continue
@@ -392,17 +402,18 @@ ConvolverNodeEngine::ProcessBlock(AudioN
 
 ConvolverNode::ConvolverNode(AudioContext* aContext)
   : AudioNode(aContext,
               2,
               ChannelCountMode::Clamped_max,
               ChannelInterpretation::Speakers)
   , mNormalize(true)
 {
-  ConvolverNodeEngine* engine = new ConvolverNodeEngine(this, mNormalize);
+  uint64_t windowID = aContext->GetParentObject()->WindowID();
+  ConvolverNodeEngine* engine = new ConvolverNodeEngine(this, mNormalize, windowID);
   mStream = AudioNodeStream::Create(aContext, engine,
                                     AudioNodeStream::NO_STREAM_FLAGS,
                                     aContext->Graph());
 }
 
 /* static */ already_AddRefed<ConvolverNode>
 ConvolverNode::Create(JSContext* aCx, AudioContext& aAudioContext,
                       const ConvolverOptions& aOptions,
--- a/dom/media/webaudio/blink/Reverb.cpp
+++ b/dom/media/webaudio/blink/Reverb.cpp
@@ -72,30 +72,36 @@ static float calculateNormalizationScale
 
     // True-stereo compensation
     if (numberOfChannels == 4)
         scale *= 0.5f;
 
     return scale;
 }
 
-Reverb::Reverb(const AudioChunk& impulseResponse, size_t maxFFTSize, bool useBackgroundThreads, bool normalize, float sampleRate)
+Reverb::Reverb(const AudioChunk& impulseResponse, size_t maxFFTSize, bool useBackgroundThreads, bool normalize, float sampleRate, bool* aAllocationFailure)
 {
+    MOZ_ASSERT(aAllocationFailure);
     size_t impulseResponseBufferLength = impulseResponse.mDuration;
     float scale = impulseResponse.mVolume;
 
     AutoTArray<const float*,4> irChannels(impulseResponse.ChannelData<float>());
     AutoTArray<float,1024> tempBuf;
 
     if (normalize) {
         scale = calculateNormalizationScale(irChannels, impulseResponseBufferLength, sampleRate);
     }
 
     if (scale != 1.0f) {
-        tempBuf.SetLength(irChannels.Length()*impulseResponseBufferLength);
+        bool rv = tempBuf.SetLength(irChannels.Length()*impulseResponseBufferLength, mozilla::fallible);
+        *aAllocationFailure = !rv;
+        if (*aAllocationFailure) {
+          return;
+        }
+
         for (uint32_t i = 0; i < irChannels.Length(); ++i) {
             float* buf = &tempBuf[i*impulseResponseBufferLength];
             AudioBufferCopyWithScale(irChannels[i], scale, buf,
                                      impulseResponseBufferLength);
             irChannels[i] = buf;
         }
     }
 
--- a/dom/media/webaudio/blink/Reverb.h
+++ b/dom/media/webaudio/blink/Reverb.h
@@ -39,18 +39,22 @@ namespace WebCore {
 
 // Multi-channel convolution reverb with channel matrixing - one or more ReverbConvolver objects are used internally.
 
 class Reverb {
 public:
     enum { MaxFrameSize = 256 };
 
     // renderSliceSize is a rendering hint, so the FFTs can be optimized to not all occur at the same time (very bad when rendering on a real-time thread).
+    // aAllocation failure is to be checked by the caller. If false, internal
+    // memory could not be allocated, and this Reverb instance is not to be
+    // used.
     Reverb(const mozilla::AudioChunk& impulseResponseBuffer, size_t maxFFTSize,
-           bool useBackgroundThreads, bool normalize, float sampleRate);
+           bool useBackgroundThreads, bool normalize, float sampleRate,
+           bool* aAllocationFailure);
 
     void process(const mozilla::AudioBlock* sourceBus,
                  mozilla::AudioBlock* destinationBus);
 
     size_t impulseResponseLength() const { return m_impulseResponseLength; }
 
     size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
 
--- a/dom/media/webaudio/test/mochitest.ini
+++ b/dom/media/webaudio/test/mochitest.ini
@@ -114,16 +114,18 @@ skip-if = toolkit == 'android' # bug 105
 [test_channelSplitterNodeWithVolume.html]
 skip-if = (android_version == '18' && debug) # bug 1158417
 [test_convolverNode.html]
 [test_convolverNode_mono_mono.html]
 [test_convolverNodeChannelCount.html]
 [test_convolverNodeChannelInterpretationChanges.html]
 [test_convolverNodeDelay.html]
 [test_convolverNodeFiniteInfluence.html]
+[test_convolverNodeOOM.html]
+skip-if = toolkit == 'android'
 [test_convolverNodeNormalization.html]
 [test_convolverNodePassThrough.html]
 [test_convolverNodeWithGain.html]
 [test_convolver-upmixing-1-channel-response.html]
 # This is a copy of
 # testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-upmixing-1-channel-response.html,
 # but WPT are not run with ASan or Android builds.
 skip-if = !asan && toolkit != android
new file mode 100644
--- /dev/null
+++ b/dom/media/webaudio/test/test_convolverNodeOOM.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test ConvolverNode with very large buffer that triggers an OOM</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="webaudio.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+  length: 2048,
+  numberOfChannels: 1,
+  skipOfflineContextTests: true,
+  createGraph: function(context) {
+    var source = context.createOscillator();
+    var convolver = context.createConvolver();
+    // Very big buffer that results in an OOM
+    try {
+      var buffer = context.createBuffer(2, 300000000, context.sampleRate)
+      var channel = buffer.getChannelData(0);
+    } catch(e) {
+      // OOM when attempting to create the buffer, this can happen on 32bits
+      // OSes. Simply return here.
+      return convolver;
+    }
+    source.connect(convolver);
+    convolver.buffer = buffer;
+    source.start();
+    return convolver;
+  }
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/helper_bug1473108.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1473108
+-->
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width; initial-scale=1.0">
+  <title>Test for Bug 1473108</title>
+  <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+  <script type="application/javascript" src="apz_test_utils.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+  <style>
+  .a {
+    background: green;
+    height: 64px;
+    width: 32px;
+    display: block;
+  }
+  span::before {
+    content: "";
+    background: red;
+    height: 32px;
+    width: 32px;
+    display: block;
+  }
+  span:active::after {
+    content: "";
+  }
+</style>
+</head>
+
+<body>
+  <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1473108">Mozilla Bug 1473108</a>
+  <a class="a" id="event"><span id="target"></span></a>
+
+  <script type="application/javascript">
+
+  waitUntilApzStable().then(() => {
+    let target = document.getElementById("target");
+    target.addEventListener('click', function(e) {
+      is(e.target, target, `Clicked on at (${e.clientX}, ${e.clientY})`);
+      subtestDone();
+    });
+    synthesizeNativeTap(target, 5, 5);
+  });
+
+</script>
+</body>
+</html>
--- a/gfx/layers/apz/test/mochitest/test_group_touchevents-3.html
+++ b/gfx/layers/apz/test/mochitest/test_group_touchevents-3.html
@@ -18,16 +18,18 @@ var touch_action_prefs = [
 var subtests = [
   // Simple test to exercise touch-action CSS property
   {'file': 'helper_touch_action.html', 'prefs': touch_action_prefs},
   // More complex touch-action tests, with overlapping regions and such
   {'file': 'helper_touch_action_complex.html', 'prefs': touch_action_prefs},
   // Tests that touch-action CSS properties are handled in APZ without waiting
   // on the main-thread, when possible
   {'file': 'helper_touch_action_regions.html', 'prefs': touch_action_prefs},
+  // touch-action tests with :active::after CSS property
+  {'file': 'helper_bug1473108.html'},
   // Add new subtests here. If this starts timing out because it's taking too
   // long, create a test_group_touchevents-4.html file. Refer to 1423011#c57
   // for more details.
 ];
 
 if (isApzEnabled()) {
   ok(window.TouchEvent, "Check if TouchEvent is supported (it should be, the test harness forces it on everywhere)");
   if (getPlatform() == "android") {
--- a/gfx/src/FontPropertyTypes.h
+++ b/gfx/src/FontPropertyTypes.h
@@ -270,16 +270,26 @@ public:
   {
     return FontStretch(kExtraExpanded);
   }
   static FontStretch UltraExpanded()
   {
     return FontStretch(kUltraExpanded);
   }
 
+  // The style system represents percentages in the 0.0..1.0 range, and
+  // FontStretch does it in the 0.0..100.0 range.
+  //
+  // TODO(emilio): We should consider changing this class to deal with the same
+  // range as the style system.
+  static FontStretch FromStyle(float aStylePercentage)
+  {
+    return FontStretch(std::min(aStylePercentage * 100.0f, float(kMax)));
+  }
+
   bool IsNormal() const { return mValue == kNormal; }
   float Percentage() const { return ToFloat(); }
 
   typedef uint16_t InternalType;
 
 private:
   friend class StretchRange;
 
--- a/gfx/thebes/gfxFontConstants.h
+++ b/gfx/thebes/gfxFontConstants.h
@@ -36,22 +36,16 @@
 
 #define NS_FONT_KERNING_AUTO                        0
 #define NS_FONT_KERNING_NONE                        1
 #define NS_FONT_KERNING_NORMAL                      2
 
 #define NS_FONT_SYNTHESIS_WEIGHT                    0x1
 #define NS_FONT_SYNTHESIS_STYLE                     0x2
 
-#define NS_FONT_DISPLAY_AUTO            0
-#define NS_FONT_DISPLAY_BLOCK           1
-#define NS_FONT_DISPLAY_SWAP            2
-#define NS_FONT_DISPLAY_FALLBACK        3
-#define NS_FONT_DISPLAY_OPTIONAL        4
-
 #define NS_FONT_OPTICAL_SIZING_AUTO     0
 #define NS_FONT_OPTICAL_SIZING_NONE     1
 
 #define NS_FONT_VARIANT_ALTERNATES_NORMAL             0
 // alternates - simple enumerated values
 #define NS_FONT_VARIANT_ALTERNATES_HISTORICAL        (1 << 0)
 
 // alternates - values that use functional syntax
--- a/gfx/thebes/gfxUserFontSet.cpp
+++ b/gfx/thebes/gfxUserFontSet.cpp
@@ -107,17 +107,17 @@ gfxUserFontEntry::gfxUserFontEntry(gfxUs
              const nsTArray<gfxFontFaceSrc>& aFontFaceSrcList,
              WeightRange aWeight,
              StretchRange aStretch,
              SlantStyleRange aStyle,
              const nsTArray<gfxFontFeature>& aFeatureSettings,
              const nsTArray<gfxFontVariation>& aVariationSettings,
              uint32_t aLanguageOverride,
              gfxCharacterMap* aUnicodeRanges,
-             uint8_t aFontDisplay,
+             StyleFontDisplay aFontDisplay,
              RangeFlags aRangeFlags)
     : gfxFontEntry(NS_LITERAL_CSTRING("userfont")),
       mUserFontLoadState(STATUS_NOT_LOADED),
       mFontDataLoadingState(NOT_LOADING),
       mUnsupportedFormat(false),
       mFontDisplay(aFontDisplay),
       mLoader(nullptr),
       mFontSet(aFontSet)
@@ -147,17 +147,17 @@ bool
 gfxUserFontEntry::Matches(const nsTArray<gfxFontFaceSrc>& aFontFaceSrcList,
                           WeightRange aWeight,
                           StretchRange aStretch,
                           SlantStyleRange aStyle,
                           const nsTArray<gfxFontFeature>& aFeatureSettings,
                           const nsTArray<gfxFontVariation>& aVariationSettings,
                           uint32_t aLanguageOverride,
                           gfxCharacterMap* aUnicodeRanges,
-                          uint8_t aFontDisplay,
+                          StyleFontDisplay aFontDisplay,
                           RangeFlags aRangeFlags)
 {
     return Weight() == aWeight &&
            Stretch() == aStretch &&
            SlantStyle() == aStyle &&
            mFeatureSettings == aFeatureSettings &&
            mVariationSettings == aVariationSettings &&
            mLanguageOverride == aLanguageOverride &&
@@ -948,17 +948,17 @@ gfxUserFontSet::FindOrCreateUserFontEntr
                                const nsTArray<gfxFontFaceSrc>& aFontFaceSrcList,
                                WeightRange aWeight,
                                StretchRange aStretch,
                                SlantStyleRange aStyle,
                                const nsTArray<gfxFontFeature>& aFeatureSettings,
                                const nsTArray<gfxFontVariation>& aVariationSettings,
                                uint32_t aLanguageOverride,
                                gfxCharacterMap* aUnicodeRanges,
-                               uint8_t aFontDisplay,
+                               StyleFontDisplay aFontDisplay,
                                RangeFlags aRangeFlags)
 {
     RefPtr<gfxUserFontEntry> entry;
 
     // If there's already a userfont entry in the family whose descriptors all match,
     // we can just move it to the end of the list instead of adding a new
     // face that will always "shadow" the old one.
     // Note that we can't do this for platform font entries, even if the
@@ -991,17 +991,17 @@ gfxUserFontSet::FindExistingUserFontEntr
                                const nsTArray<gfxFontFaceSrc>& aFontFaceSrcList,
                                WeightRange aWeight,
                                StretchRange aStretch,
                                SlantStyleRange aStyle,
                                const nsTArray<gfxFontFeature>& aFeatureSettings,
                                const nsTArray<gfxFontVariation>& aVariationSettings,
                                uint32_t aLanguageOverride,
                                gfxCharacterMap* aUnicodeRanges,
-                               uint8_t aFontDisplay,
+                               StyleFontDisplay aFontDisplay,
                                RangeFlags aRangeFlags)
 {
     nsTArray<RefPtr<gfxFontEntry>>& fontList = aFamily->GetFontList();
 
     for (size_t i = 0, count = fontList.Length(); i < count; i++) {
         if (!fontList[i]->mIsUserFontContainer) {
             continue;
         }
@@ -1037,17 +1037,17 @@ gfxUserFontSet::AddUserFontEntry(const n
         aUserFontEntry->Stretch().ToString(stretchString);
         LOG(("userfonts (%p) added to \"%s\" (%p) style: %s weight: %s "
              "stretch: %s display: %d",
              this, aFamilyName.get(), aUserFontEntry,
              (aUserFontEntry->IsItalic() ? "italic" :
               (aUserFontEntry->IsOblique() ? "oblique" : "normal")),
              weightString.get(),
              stretchString.get(),
-             aUserFontEntry->GetFontDisplay()));
+             static_cast<int>(aUserFontEntry->GetFontDisplay())));
     }
 }
 
 void
 gfxUserFontSet::IncrementGeneration(bool aIsRebuild)
 {
     // add one, increment again if zero
     ++sFontSetGeneration;
--- a/gfx/thebes/gfxUserFontSet.h
+++ b/gfx/thebes/gfxUserFontSet.h
@@ -12,16 +12,17 @@
 #include "gfxFontSrcURI.h"
 #include "nsRefPtrHashtable.h"
 #include "nsCOMPtr.h"
 #include "nsIURI.h"
 #include "nsIPrincipal.h"
 #include "nsIScriptError.h"
 #include "nsURIHashKey.h"
 #include "mozilla/FontPropertyTypes.h"
+#include "mozilla/ServoStyleConsts.h"
 #include "mozilla/net/ReferrerPolicy.h"
 #include "gfxFontConstants.h"
 
 namespace mozilla {
 class PostTraversalTask;
 } // namespace mozilla
 class nsFontFaceLoader;
 
@@ -239,32 +240,32 @@ public:
                               const nsTArray<gfxFontFaceSrc>& aFontFaceSrcList,
                               WeightRange aWeight,
                               StretchRange aStretch,
                               SlantStyleRange aStyle,
                               const nsTArray<gfxFontFeature>& aFeatureSettings,
                               const nsTArray<gfxFontVariation>& aVariationSettings,
                               uint32_t aLanguageOverride,
                               gfxCharacterMap* aUnicodeRanges,
-                              uint8_t aFontDisplay,
+                              mozilla::StyleFontDisplay aFontDisplay,
                               RangeFlags aRangeFlags) = 0;
 
     // creates a font face for the specified family, or returns an existing
     // matching entry on the family if there is one
     already_AddRefed<gfxUserFontEntry> FindOrCreateUserFontEntry(
                                const nsACString& aFamilyName,
                                const nsTArray<gfxFontFaceSrc>& aFontFaceSrcList,
                                WeightRange aWeight,
                                StretchRange aStretch,
                                SlantStyleRange aStyle,
                                const nsTArray<gfxFontFeature>& aFeatureSettings,
                                const nsTArray<gfxFontVariation>& aVariationSettings,
                                uint32_t aLanguageOverride,
                                gfxCharacterMap* aUnicodeRanges,
-                               uint8_t aFontDisplay,
+                               mozilla::StyleFontDisplay aFontDisplay,
                                RangeFlags aRangeFlags);
 
     // add in a font face for which we have the gfxUserFontEntry already
     void AddUserFontEntry(const nsCString& aFamilyName,
                           gfxUserFontEntry* aUserFontEntry);
 
     // Whether there is a face with this family name
     bool HasFamily(const nsACString& aFamilyName) const
@@ -510,17 +511,17 @@ protected:
                                    const nsTArray<gfxFontFaceSrc>& aFontFaceSrcList,
                                    WeightRange aWeight,
                                    StretchRange aStretch,
                                    SlantStyleRange aStyle,
                                    const nsTArray<gfxFontFeature>& aFeatureSettings,
                                    const nsTArray<gfxFontVariation>& aVariationSettings,
                                    uint32_t aLanguageOverride,
                                    gfxCharacterMap* aUnicodeRanges,
-                                   uint8_t aFontDisplay,
+                                   mozilla::StyleFontDisplay aFontDisplay,
                                    RangeFlags aRangeFlags);
 
     // creates a new gfxUserFontFamily in mFontFamilies, or returns an existing
     // family if there is one
     gfxUserFontFamily* GetFamily(const nsACString& aFamilyName);
 
     // font families defined by @font-face rules
     nsRefPtrHashtable<nsCStringHashKey, gfxUserFontFamily> mFontFamilies;
@@ -561,31 +562,31 @@ public:
                      const nsTArray<gfxFontFaceSrc>& aFontFaceSrcList,
                      WeightRange aWeight,
                      StretchRange aStretch,
                      SlantStyleRange aStyle,
                      const nsTArray<gfxFontFeature>& aFeatureSettings,
                      const nsTArray<gfxFontVariation>& aVariationSettings,
                      uint32_t aLanguageOverride,
                      gfxCharacterMap* aUnicodeRanges,
-                     uint8_t aFontDisplay,
+                     mozilla::StyleFontDisplay aFontDisplay,
                      RangeFlags aRangeFlags);
 
     virtual ~gfxUserFontEntry();
 
     // Return whether the entry matches the given list of attributes
     bool Matches(const nsTArray<gfxFontFaceSrc>& aFontFaceSrcList,
                  WeightRange aWeight,
                  StretchRange aStretch,
                  SlantStyleRange aStyle,
                  const nsTArray<gfxFontFeature>& aFeatureSettings,
                  const nsTArray<gfxFontVariation>& aVariationSettings,
                  uint32_t aLanguageOverride,
                  gfxCharacterMap* aUnicodeRanges,
-                 uint8_t aFontDisplay,
+                 mozilla::StyleFontDisplay aFontDisplay,
                  RangeFlags aRangeFlags);
 
     gfxFont* CreateFontInstance(const gfxFontStyle* aFontStyle) override;
 
     gfxFontEntry* GetPlatformFontEntry() const { return mPlatformFontEntry; }
 
     // is the font loading or loaded, or did it fail?
     UserFontLoadState LoadState() const { return mUserFontLoadState; }
@@ -612,17 +613,17 @@ public:
         }
         return true;
     }
 
     gfxCharacterMap* GetUnicodeRangeMap() const {
         return mCharacterMap.get();
     }
 
-    uint8_t GetFontDisplay() const { return mFontDisplay; }
+    mozilla::StyleFontDisplay GetFontDisplay() const { return mFontDisplay; }
 
     // load the font - starts the loading of sources which continues until
     // a valid font resource is found or all sources fail
     void Load();
 
     // methods to expose some information to FontFaceSet::UserFontSet
     // since we can't make that class a friend
     void SetLoader(nsFontFaceLoader* aLoader) { mLoader = aLoader; }
@@ -717,18 +718,18 @@ protected:
                              // so keep hiding fallback font
         LOADING_SLOWLY,      // timeout happened and we're not nearly done,
                              // so use the fallback font
         LOADING_TIMED_OUT,   // font load took too long
         LOADING_FAILED       // failed to load any source: use fallback
     };
     FontDataLoadingState     mFontDataLoadingState;
 
-    bool                     mUnsupportedFormat;
-    uint8_t                  mFontDisplay; // timing of userfont fallback
+    bool mUnsupportedFormat;
+    mozilla::StyleFontDisplay mFontDisplay; // timing of userfont fallback
 
     RefPtr<gfxFontEntry>   mPlatformFontEntry;
     nsTArray<gfxFontFaceSrc> mSrcList;
     uint32_t                 mSrcIndex; // index of loading src item
     // This field is managed by the nsFontFaceLoader. In the destructor and Cancel()
     // methods of nsFontFaceLoader this reference is nulled out.
     nsFontFaceLoader* MOZ_NON_OWNING_REF mLoader; // current loader for this entry, if any
     gfxUserFontSet*   MOZ_NON_OWNING_REF mFontSet; // font-set which owns this userfont entry
--- a/js/src/builtin/BigInt.cpp
+++ b/js/src/builtin/BigInt.cpp
@@ -21,22 +21,16 @@
 using namespace js;
 
 static MOZ_ALWAYS_INLINE bool
 IsBigInt(HandleValue v)
 {
     return v.isBigInt() || (v.isObject() && v.toObject().is<BigIntObject>());
 }
 
-static JSObject*
-CreateBigIntPrototype(JSContext* cx, JSProtoKey key)
-{
-    return GlobalObject::createBlankPrototype<PlainObject>(cx, cx->global());
-}
-
 // BigInt proposal section 5.1.3
 static bool
 BigIntConstructor(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     // Step 1.
     if (args.isConstructing()) {
@@ -184,32 +178,39 @@ bool
 BigIntObject::toLocaleString(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     return CallNonGenericMethod<IsBigInt, toLocaleString_impl>(cx, args);
 }
 
 const ClassSpec BigIntObject::classSpec_ = {
     GenericCreateConstructor<BigIntConstructor, 1, gc::AllocKind::FUNCTION>,
-    CreateBigIntPrototype,
+    GenericCreatePrototype<BigIntObject>,
     nullptr,
     nullptr,
     BigIntObject::methods,
     BigIntObject::properties
 };
 
 // The class is named "Object" as a workaround for bug 1277801.
 const Class BigIntObject::class_ = {
     "Object",
     JSCLASS_HAS_CACHED_PROTO(JSProto_BigInt) |
     JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS),
     JS_NULL_CLASS_OPS,
     &BigIntObject::classSpec_
 };
 
+const Class BigIntObject::protoClass_ = {
+    js_Object_str,
+    JSCLASS_HAS_CACHED_PROTO(JSProto_BigInt),
+    JS_NULL_CLASS_OPS,
+    &BigIntObject::classSpec_
+};
+
 const JSPropertySpec BigIntObject::properties[] = {
     // BigInt proposal section 5.3.5
     JS_STRING_SYM_PS(toStringTag, "BigInt", JSPROP_READONLY),
     JS_PS_END
 };
 
 const JSFunctionSpec BigIntObject::methods[] = {
     JS_FN("valueOf", valueOf, 0, 0),
--- a/js/src/builtin/BigInt.h
+++ b/js/src/builtin/BigInt.h
@@ -19,16 +19,17 @@ class GlobalObject;
 class BigIntObject : public NativeObject
 {
     static const unsigned PRIMITIVE_VALUE_SLOT = 0;
     static const unsigned RESERVED_SLOTS = 1;
 
   public:
     static const ClassSpec classSpec_;
     static const Class class_;
+    static const Class protoClass_;
 
     static JSObject* create(JSContext* cx, JS::Handle<JS::BigInt*> bi);
 
     // Methods defined on BigInt.prototype.
     static bool valueOf_impl(JSContext* cx, const CallArgs& args);
     static bool valueOf(JSContext* cx, unsigned argc, JS::Value* vp);
     static bool toString_impl(JSContext* cx, const CallArgs& args);
     static bool toString(JSContext* cx, unsigned argc, JS::Value* vp);
--- a/js/src/builtin/DataViewObject.cpp
+++ b/js/src/builtin/DataViewObject.cpp
@@ -930,23 +930,16 @@ DataViewObject::byteOffsetGetterImpl(JSC
 
 bool
 DataViewObject::byteOffsetGetter(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
     return CallNonGenericMethod<is, byteOffsetGetterImpl>(cx, args);
 }
 
-const Class DataViewObject::protoClass_ = {
-    js_Object_str,
-    JSCLASS_HAS_CACHED_PROTO(JSProto_DataView),
-    JS_NULL_CLASS_OPS,
-    &DataViewObject::classSpec_
-};
-
 JSObject*
 DataViewObject::CreatePrototype(JSContext* cx, JSProtoKey key)
 {
     return GlobalObject::createBlankPrototype(cx, cx->global(), &DataViewObject::protoClass_);
 }
 
 static const ClassOps DataViewObjectClassOps = {
     nullptr, /* addProperty */
@@ -959,32 +952,39 @@ static const ClassOps DataViewObjectClas
     nullptr, /* call */
     nullptr, /* hasInstance */
     nullptr, /* construct */
     ArrayBufferViewObject::trace
 };
 
 const ClassSpec DataViewObject::classSpec_ = {
     GenericCreateConstructor<DataViewObject::construct, 3, gc::AllocKind::FUNCTION>,
-    DataViewObject::CreatePrototype,
+    GenericCreatePrototype<DataViewObject>,
     nullptr,
     nullptr,
     DataViewObject::methods,
     DataViewObject::properties,
 };
 
 const Class DataViewObject::class_ = {
     "DataView",
     JSCLASS_HAS_PRIVATE |
     JSCLASS_HAS_RESERVED_SLOTS(TypedArrayObject::RESERVED_SLOTS) |
     JSCLASS_HAS_CACHED_PROTO(JSProto_DataView),
     &DataViewObjectClassOps,
     &DataViewObject::classSpec_
 };
 
+const Class DataViewObject::protoClass_ = {
+    js_Object_str,
+    JSCLASS_HAS_CACHED_PROTO(JSProto_DataView),
+    JS_NULL_CLASS_OPS,
+    &DataViewObject::classSpec_
+};
+
 const JSFunctionSpec DataViewObject::methods[] = {
     JS_FN("getInt8",    DataViewObject::fun_getInt8,      1,0),
     JS_FN("getUint8",   DataViewObject::fun_getUint8,     1,0),
     JS_FN("getInt16",   DataViewObject::fun_getInt16,     1,0),
     JS_FN("getUint16",  DataViewObject::fun_getUint16,    1,0),
     JS_FN("getInt32",   DataViewObject::fun_getInt32,     1,0),
     JS_FN("getUint32",  DataViewObject::fun_getUint32,    1,0),
     JS_FN("getFloat32", DataViewObject::fun_getFloat32,   1,0),
--- a/js/src/builtin/DataViewObject.h
+++ b/js/src/builtin/DataViewObject.h
@@ -21,17 +21,16 @@ namespace js {
 // In the DataViewObject, the private slot contains a raw pointer into
 // the buffer.  The buffer may be shared memory and the raw pointer
 // should not be exposed without sharedness information accompanying
 // it.
 
 class DataViewObject : public NativeObject
 {
   private:
-    static const Class protoClass_;
     static const ClassSpec classSpec_;
 
     static JSObject* CreatePrototype(JSContext* cx, JSProtoKey key);
 
     static bool is(HandleValue v) {
         return v.isObject() && v.toObject().hasClass(&class_);
     }
 
@@ -55,16 +54,17 @@ class DataViewObject : public NativeObje
     static bool constructWrapped(JSContext* cx, HandleObject bufobj, const CallArgs& args);
 
     static DataViewObject*
     create(JSContext* cx, uint32_t byteOffset, uint32_t byteLength,
            Handle<ArrayBufferObjectMaybeShared*> arrayBuffer, HandleObject proto);
 
   public:
     static const Class class_;
+    static const Class protoClass_;
 
     static Value byteOffsetValue(const DataViewObject* view) {
         Value v = view->getFixedSlot(TypedArrayObject::BYTEOFFSET_SLOT);
         MOZ_ASSERT(v.toInt32() >= 0);
         return v;
     }
 
     static Value byteLengthValue(const DataViewObject* view) {
--- a/js/src/builtin/MapObject.cpp
+++ b/js/src/builtin/MapObject.cpp
@@ -407,39 +407,33 @@ MapIteratorObject::createResultPair(JSCo
     AddTypePropertyId(cx, resultPairObj, JSID_VOID, TypeSet::UnknownType());
 
     return resultPairObj;
 }
 
 
 /*** Map *****************************************************************************************/
 
-static JSObject*
-CreateMapPrototype(JSContext* cx, JSProtoKey key)
-{
-    return GlobalObject::createBlankPrototype(cx, cx->global(), &MapObject::protoClass_);
-}
-
 const ClassOps MapObject::classOps_ = {
     nullptr, // addProperty
     nullptr, // delProperty
     nullptr, // enumerate
     nullptr, // newEnumerate
     nullptr, // resolve
     nullptr, // mayResolve
     finalize,
     nullptr, // call
     nullptr, // hasInstance
     nullptr, // construct
     trace
 };
 
 const ClassSpec MapObject::classSpec_ = {
     GenericCreateConstructor<MapObject::construct, 0, gc::AllocKind::FUNCTION>,
-    CreateMapPrototype,
+    GenericCreatePrototype<MapObject>,
     nullptr,
     MapObject::staticProperties,
     MapObject::methods,
     MapObject::properties,
 };
 
 const Class MapObject::class_ = {
     "Map",
@@ -1269,39 +1263,33 @@ SetIteratorObject::createResult(JSContex
     AddTypePropertyId(cx, resultObj, JSID_VOID, TypeSet::UnknownType());
 
     return resultObj;
 }
 
 
 /*** Set *****************************************************************************************/
 
-static JSObject*
-CreateSetPrototype(JSContext* cx, JSProtoKey key)
-{
-    return GlobalObject::createBlankPrototype(cx, cx->global(), &SetObject::protoClass_);
-}
-
 const ClassOps SetObject::classOps_ = {
     nullptr, // addProperty
     nullptr, // delProperty
     nullptr, // enumerate
     nullptr, // newEnumerate
     nullptr, // resolve
     nullptr, // mayResolve
     finalize,
     nullptr, // call
     nullptr, // hasInstance
     nullptr, // construct
     trace
 };
 
 const ClassSpec SetObject::classSpec_ = {
     GenericCreateConstructor<SetObject::construct, 0, gc::AllocKind::FUNCTION>,
-    CreateSetPrototype,
+    GenericCreatePrototype<SetObject>,
     nullptr,
     SetObject::staticProperties,
     SetObject::methods,
     SetObject::properties,
 };
 
 const Class SetObject::class_ = {
     "Set",
--- a/js/src/builtin/Promise.cpp
+++ b/js/src/builtin/Promise.cpp
@@ -5040,22 +5040,16 @@ OffThreadPromiseRuntimeState::shutdown(J
     numCanceled_ = 0;
 
     // After shutdown, there should be no OffThreadPromiseTask activity in this
     // JSRuntime. Revert to the !initialized() state to catch bugs.
     dispatchToEventLoopCallback_ = nullptr;
     MOZ_ASSERT(!initialized());
 }
 
-static JSObject*
-CreatePromisePrototype(JSContext* cx, JSProtoKey key)
-{
-    return GlobalObject::createBlankPrototype(cx, cx->global(), &PromiseObject::protoClass_);
-}
-
 const JSJitInfo promise_then_info = {
   { (JSJitGetterOp)Promise_then_noRetVal },
   { 0 }, /* unused */
   { 0 }, /* unused */
   JSJitInfo::IgnoresReturnValueNative,
   JSJitInfo::AliasEverything,
   JSVAL_TYPE_UNDEFINED,
 };
@@ -5091,17 +5085,17 @@ static const JSFunctionSpec promise_stat
 
 static const JSPropertySpec promise_static_properties[] = {
     JS_SYM_GET(species, Promise_static_species, 0),
     JS_PS_END
 };
 
 static const ClassSpec PromiseObjectClassSpec = {
     GenericCreateConstructor<PromiseConstructor, 1, gc::AllocKind::FUNCTION>,
-    CreatePromisePrototype,
+    GenericCreatePrototype<PromiseObject>,
     promise_static_methods,
     promise_static_properties,
     promise_methods,
     promise_properties
 };
 
 const Class PromiseObject::class_ = {
     "Promise",
--- a/js/src/builtin/Stream.cpp
+++ b/js/src/builtin/Stream.cpp
@@ -578,17 +578,17 @@ class TeeState : public NativeObject
 const Class TeeState::class_ = {
     "TeeState",
     JSCLASS_HAS_RESERVED_SLOTS(SlotCount)
 };
 
 #define CLASS_SPEC(cls, nCtorArgs, nSlots, specFlags, classFlags, classOps) \
 const ClassSpec cls::classSpec_ = { \
     GenericCreateConstructor<cls::constructor, nCtorArgs, gc::AllocKind::FUNCTION>, \
-    GenericCreatePrototype, \
+    GenericCreatePrototype<cls>, \
     nullptr, \
     nullptr, \
     cls##_methods, \
     cls##_properties, \
     nullptr, \
     specFlags \
 }; \
 \
new file mode 100644
--- /dev/null
+++ b/js/src/jit-test/tests/basic/bug1445854.js
@@ -0,0 +1,4 @@
+load(libdir + "asserts.js");
+if (typeof ReadableStream == "function") {
+    assertThrowsInstanceOf(TypeError, () => ReadableStream.prototype.tee());
+}
--- a/js/src/jsdate.cpp
+++ b/js/src/jsdate.cpp
@@ -3599,23 +3599,16 @@ DateConstructor(JSContext* cx, unsigned 
 
     if (args.length() == 1) {
         return DateOneArgument(cx, args);
     }
 
     return DateMultipleArguments(cx, args);
 }
 
-// ES6 final draft 20.3.4.
-static JSObject*
-CreateDatePrototype(JSContext* cx, JSProtoKey key)
-{
-    return GlobalObject::createBlankPrototype(cx, cx->global(), &DateObject::protoClass_);
-}
-
 static bool
 FinishDateClassInit(JSContext* cx, HandleObject ctor, HandleObject proto)
 {
     /*
      * Date.prototype.toGMTString has the same initial value as
      * Date.prototype.toUTCString.
      */
     RootedValue toUTCStringFun(cx);
@@ -3623,17 +3616,17 @@ FinishDateClassInit(JSContext* cx, Handl
     RootedId toGMTStringId(cx, NameToId(cx->names().toGMTString));
     return NativeGetProperty(cx, proto.as<NativeObject>(), toUTCStringId, &toUTCStringFun) &&
            NativeDefineDataProperty(cx, proto.as<NativeObject>(), toGMTStringId, toUTCStringFun,
                                     0);
 }
 
 static const ClassSpec DateObjectClassSpec = {
     GenericCreateConstructor<DateConstructor, 7, gc::AllocKind::FUNCTION>,
-    CreateDatePrototype,
+    GenericCreatePrototype<DateObject>,
     date_static_methods,
     nullptr,
     date_methods,
     nullptr,
     FinishDateClassInit
 };
 
 const Class DateObject::class_ = {
--- a/js/src/vm/ArrayBufferObject.cpp
+++ b/js/src/vm/ArrayBufferObject.cpp
@@ -256,22 +256,16 @@ js::UnmapBufferMemory(void* base, size_t
  * access.  It can be created explicitly and passed to a TypedArrayObject, or
  * can be created implicitly by constructing a TypedArrayObject with a size.
  */
 
 /*
  * ArrayBufferObject (base)
  */
 
-static JSObject*
-CreateArrayBufferPrototype(JSContext* cx, JSProtoKey key)
-{
-    return GlobalObject::createBlankPrototype(cx, cx->global(), &ArrayBufferObject::protoClass_);
-}
-
 static const ClassOps ArrayBufferObjectClassOps = {
     nullptr,        /* addProperty */
     nullptr,        /* delProperty */
     nullptr,        /* enumerate */
     nullptr,        /* newEnumerate */
     nullptr,        /* resolve */
     nullptr,        /* mayResolve */
     ArrayBufferObject::finalize,
@@ -300,17 +294,17 @@ static const JSFunctionSpec arraybuffer_
 static const JSPropertySpec arraybuffer_proto_properties[] = {
     JS_PSG("byteLength", ArrayBufferObject::byteLengthGetter, 0),
     JS_STRING_SYM_PS(toStringTag, "ArrayBuffer", JSPROP_READONLY),
     JS_PS_END
 };
 
 static const ClassSpec ArrayBufferObjectClassSpec = {
     GenericCreateConstructor<ArrayBufferObject::class_constructor, 1, gc::AllocKind::FUNCTION>,
-    CreateArrayBufferPrototype,
+    GenericCreatePrototype<ArrayBufferObject>,
     arraybuffer_functions,
     arraybuffer_properties,
     arraybuffer_proto_functions,
     arraybuffer_proto_properties
 };
 
 static const ClassExtension ArrayBufferObjectClassExtension = {
     nullptr,    /* weakmapKeyDelegateOp */
--- a/js/src/vm/Debugger.cpp
+++ b/js/src/vm/Debugger.cpp
@@ -11051,17 +11051,17 @@ DebuggerObject::getBoundArguments(JSCont
 Debugger::getObjectAllocationSite(JSObject& obj)
 {
     JSObject* metadata = GetAllocationMetadata(&obj);
     if (!metadata) {
         return nullptr;
     }
 
     MOZ_ASSERT(!metadata->is<WrapperObject>());
-    return SavedFrame::isSavedFrameAndNotProto(*metadata)
+    return metadata->is<SavedFrame>()
         ? &metadata->as<SavedFrame>()
         : nullptr;
 }
 
 /* static */ bool
 DebuggerObject::getAllocationSite(JSContext* cx, HandleDebuggerObject object,
                                   MutableHandleObject result)
 {
--- a/js/src/vm/GlobalObject.h
+++ b/js/src/vm/GlobalObject.h
@@ -904,28 +904,27 @@ JSObject*
 GenericCreateConstructor(JSContext* cx, JSProtoKey key)
 {
     // Note - We duplicate the trick from ClassName() so that we don't need to
     // include vm/JSAtom-inl.h here.
     PropertyName* name = (&cx->names().Null)[key];
     return GlobalObject::createConstructor(cx, ctor, name, length, kind, jitInfo);
 }
 
-inline JSObject*
+template<typename T>
+JSObject*
 GenericCreatePrototype(JSContext* cx, JSProtoKey key)
 {
-    MOZ_ASSERT(key != JSProto_Object);
-    const Class* clasp = ProtoKeyToClass(key);
-    MOZ_ASSERT(clasp);
-    JSProtoKey protoKey = InheritanceProtoKeyForStandardClass(key);
-    if (!GlobalObject::ensureConstructor(cx, cx->global(), protoKey)) {
-        return nullptr;
-    }
-    RootedObject parentProto(cx, &cx->global()->getPrototype(protoKey).toObject());
-    return GlobalObject::createBlankPrototypeInheriting(cx, clasp, parentProto);
+    static_assert(!std::is_same<T, PlainObject>::value,
+                  "creating Object.prototype is very special and isn't handled here");
+    MOZ_ASSERT(&T::class_ == ProtoKeyToClass(key),
+               "type mismatch--probably too much copy/paste in your ClassSpec");
+    MOZ_ASSERT(InheritanceProtoKeyForStandardClass(key) == JSProto_Object,
+               "subclasses (of anything but Object) can't use GenericCreatePrototype");
+    return GlobalObject::createBlankPrototype(cx, cx->global(), &T::protoClass_);
 }
 
 inline JSProtoKey
 StandardProtoKeyOrNull(const JSObject* obj)
 {
     return JSCLASS_CACHED_PROTO_KEY(obj->getClass());
 }
 
--- a/js/src/vm/RegExpObject.cpp
+++ b/js/src/vm/RegExpObject.cpp
@@ -162,39 +162,33 @@ IsMarkingTrace(JSTracer* trc)
 }
 
 void
 RegExpObject::trace(JSTracer* trc)
 {
     TraceNullableEdge(trc, &sharedRef(), "RegExpObject shared");
 }
 
-static JSObject*
-CreateRegExpPrototype(JSContext* cx, JSProtoKey key)
-{
-    return GlobalObject::createBlankPrototype(cx, cx->global(), &RegExpObject::protoClass_);
-}
-
 static const ClassOps RegExpObjectClassOps = {
     nullptr, /* addProperty */
     nullptr, /* delProperty */
     nullptr, /* enumerate */
     nullptr, /* newEnumerate */
     nullptr, /* resolve */
     nullptr, /* mayResolve */
     nullptr, /* finalize */
     nullptr, /* call */
     nullptr, /* hasInstance */
     nullptr, /* construct */
     RegExpObject::trace,
 };
 
 static const ClassSpec RegExpObjectClassSpec = {
     GenericCreateConstructor<js::regexp_construct, 2, gc::AllocKind::FUNCTION>,
-    CreateRegExpPrototype,
+    GenericCreatePrototype<RegExpObject>,
     nullptr,
     js::regexp_static_props,
     js::regexp_methods,
     js::regexp_properties
 };
 
 const Class RegExpObject::class_ = {
     js_RegExp_str,
--- a/js/src/vm/SavedFrame.h
+++ b/js/src/vm/SavedFrame.h
@@ -18,16 +18,17 @@ namespace js {
 class SavedFrame : public NativeObject {
     friend class SavedStacks;
     friend struct ::JSStructuredCloneReader;
 
     static const ClassSpec      classSpec_;
 
   public:
     static const Class          class_;
+    static const Class          protoClass_;
     static const JSPropertySpec protoAccessors[];
     static const JSFunctionSpec protoFunctions[];
     static const JSFunctionSpec staticFunctions[];
 
     // Prototype methods and properties to be exposed to JS.
     static bool construct(JSContext* cx, unsigned argc, Value* vp);
     static bool sourceProperty(JSContext* cx, unsigned argc, Value* vp);
     static bool lineProperty(JSContext* cx, unsigned argc, Value* vp);
@@ -91,27 +92,22 @@ class SavedFrame : public NativeObject {
         RootedSavedFrame frame_;
 
       public:
         RootedRange(JSContext* cx, HandleSavedFrame frame) : frame_(cx, frame) { }
         RootedIterator begin() { return RootedIterator(*this); }
         RootedIterator end() { return RootedIterator(); }
     };
 
-    static bool isSavedFrameAndNotProto(JSObject& obj) {
-        return obj.is<SavedFrame>() &&
-               !obj.as<SavedFrame>().getReservedSlot(JSSLOT_SOURCE).isNull();
-    }
-
-    static bool isSavedFrameOrWrapperAndNotProto(JSObject& obj) {
+    static bool isSavedFrameOrWrapper(JSObject& obj) {
         auto unwrapped = CheckedUnwrap(&obj);
         if (!unwrapped) {
             return false;
         }
-        return isSavedFrameAndNotProto(*unwrapped);
+        return unwrapped->is<SavedFrame>();
     }
 
     struct Lookup;
     struct HashPolicy;
 
     typedef JS::GCHashSet<ReadBarriered<SavedFrame*>,
                           HashPolicy,
                           SystemAllocPolicy> Set;
--- a/js/src/vm/SavedStacks-inl.h
+++ b/js/src/vm/SavedStacks-inl.h
@@ -18,13 +18,13 @@
 // and use the original caller's compartment's principals to determine what
 // level of data to present. Unwrapping and entering the referent's compartment
 // would mess that up. See the module level documentation in
 // `js/src/vm/SavedStacks.h` as well as the comments in `js/src/jsapi.h`.
 inline void
 js::AssertObjectIsSavedFrameOrWrapper(JSContext* cx, HandleObject stack)
 {
     if (stack) {
-        MOZ_RELEASE_ASSERT(js::SavedFrame::isSavedFrameOrWrapperAndNotProto(*stack));
+        MOZ_RELEASE_ASSERT(js::SavedFrame::isSavedFrameOrWrapper(*stack));
     }
 }
 
 #endif // vm_SavedStacksInl_h
--- a/js/src/vm/SavedStacks.cpp
+++ b/js/src/vm/SavedStacks.cpp
@@ -342,20 +342,16 @@ SavedFrame::HashPolicy::match(SavedFrame
 SavedFrame::HashPolicy::rekey(Key& key, const Key& newKey)
 {
     key = newKey;
 }
 
 /* static */ bool
 SavedFrame::finishSavedFrameInit(JSContext* cx, HandleObject ctor, HandleObject proto)
 {
-    // The only object with the SavedFrame::class_ that doesn't have a source
-    // should be the prototype.
-    proto->as<NativeObject>().setReservedSlot(SavedFrame::JSSLOT_SOURCE, NullValue());
-
     return FreezeObject(cx, proto);
 }
 
 static const ClassOps SavedFrameClassOps = {
     nullptr,                    // addProperty
     nullptr,                    // delProperty
     nullptr,                    // enumerate
     nullptr,                    // newEnumerate
@@ -365,17 +361,17 @@ static const ClassOps SavedFrameClassOps
     nullptr,                    // call
     nullptr,                    // hasInstance
     nullptr,                    // construct
     nullptr,                    // trace
 };
 
 const ClassSpec SavedFrame::classSpec_ = {
     GenericCreateConstructor<SavedFrame::construct, 0, gc::AllocKind::FUNCTION>,
-    GenericCreatePrototype,
+    GenericCreatePrototype<SavedFrame>,
     SavedFrame::staticFunctions,
     nullptr,
     SavedFrame::protoFunctions,
     SavedFrame::protoAccessors,
     SavedFrame::finishSavedFrameInit,
     ClassSpec::DontDefineConstructor
 };
 
@@ -385,16 +381,23 @@ const ClassSpec SavedFrame::classSpec_ =
     JSCLASS_HAS_RESERVED_SLOTS(SavedFrame::JSSLOT_COUNT) |
     JSCLASS_HAS_CACHED_PROTO(JSProto_SavedFrame) |
     JSCLASS_IS_ANONYMOUS |
     JSCLASS_FOREGROUND_FINALIZE,
     &SavedFrameClassOps,
     &SavedFrame::classSpec_
 };
 
+const Class SavedFrame::protoClass_ = {
+    js_Object_str,
+    JSCLASS_HAS_CACHED_PROTO(JSProto_SavedFrame),
+    JS_NULL_CLASS_OPS,
+    &SavedFrame::classSpec_
+};
+
 /* static */ const JSFunctionSpec
 SavedFrame::staticFunctions[] = {
     JS_FS_END
 };
 
 /* static */ const JSFunctionSpec
 SavedFrame::protoFunctions[] = {
     JS_FN("constructor", SavedFrame::construct, 0, 0),
@@ -734,25 +737,16 @@ SavedFrame_checkThis(JSContext* cx, Call
     JSObject* thisObject = CheckedUnwrap(&thisValue.toObject());
     if (!thisObject || !thisObject->is<SavedFrame>()) {
         JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
                                   SavedFrame::class_.name, fnName,
                                   thisObject ? thisObject->getClass()->name : "object");
         return false;
     }
 
-    // Check for SavedFrame.prototype, which has the same class as SavedFrame
-    // instances, however doesn't actually represent a captured stack frame. It
-    // is the only object that is<SavedFrame>() but doesn't have a source.
-    if (!SavedFrame::isSavedFrameAndNotProto(*thisObject)) {
-        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_INCOMPATIBLE_PROTO,
-                                  SavedFrame::class_.name, fnName, "prototype object");
-        return false;
-    }
-
     // Now set "frame" to the actual object we were invoked in (which may be a
     // wrapper), not the unwrapped version.  Consumers will need to know what
     // that original object was, and will do principal checks as needed.
     frame.set(&thisValue.toObject());
     return true;
 }
 
 // Get the SavedFrame * from the current this value and handle any errors that
@@ -784,17 +778,17 @@ UnwrapSavedFrame(JSContext* cx, JSPrinci
         return nullptr;
     }
 
     RootedObject savedFrameObj(cx, CheckedUnwrap(obj));
     if (!savedFrameObj) {
         return nullptr;
     }
 
-    MOZ_RELEASE_ASSERT(js::SavedFrame::isSavedFrameAndNotProto(*savedFrameObj));
+    MOZ_RELEASE_ASSERT(savedFrameObj->is<js::SavedFrame>());
     js::RootedSavedFrame frame(cx, &savedFrameObj->as<js::SavedFrame>());
     return GetFirstSubsumedFrame(cx, principals, frame, selfHosted, skippedAsync);
 }
 
 JS_PUBLIC_API(SavedFrameResult)
 GetSavedFrameSource(JSContext* cx, JSPrincipals* principals, HandleObject savedFrame,
                     MutableHandleString sourcep,
                     SavedFrameSelfHosted selfHosted /* = SavedFrameSelfHosted::Include */)
@@ -1133,24 +1127,24 @@ BuildStackString(JSContext* cx, JSPrinci
     stringp.set(str);
     return true;
 }
 
 JS_PUBLIC_API(bool)
 IsMaybeWrappedSavedFrame(JSObject* obj)
 {
     MOZ_ASSERT(obj);
-    return js::SavedFrame::isSavedFrameOrWrapperAndNotProto(*obj);
+    return js::SavedFrame::isSavedFrameOrWrapper(*obj);
 }
 
 JS_PUBLIC_API(bool)
 IsUnwrappedSavedFrame(JSObject* obj)
 {
     MOZ_ASSERT(obj);
-    return js::SavedFrame::isSavedFrameAndNotProto(*obj);
+    return obj->is<js::SavedFrame>();
 }
 
 } /* namespace JS */
 
 namespace js {
 
 /* static */ bool
 SavedFrame::sourceProperty(JSContext* cx, unsigned argc, Value* vp)
@@ -1305,17 +1299,17 @@ SavedStacks::copyAsyncStack(JSContext* c
 
     RootedAtom asyncCauseAtom(cx, AtomizeString(cx, asyncCause));
     if (!asyncCauseAtom) {
         return false;
     }
 
     RootedObject asyncStackObj(cx, CheckedUnwrap(asyncStack));
     MOZ_RELEASE_ASSERT(asyncStackObj);
-    MOZ_RELEASE_ASSERT(js::SavedFrame::isSavedFrameAndNotProto(*asyncStackObj));
+    MOZ_RELEASE_ASSERT(asyncStackObj->is<js::SavedFrame>());
     adoptedStack.set(&asyncStackObj->as<js::SavedFrame>());
 
     if (!adoptAsyncStack(cx, adoptedStack, asyncCauseAtom, maxFrameCount)) {
         return false;
     }
 
     return true;
 }
--- a/js/src/vm/SharedArrayObject.cpp
+++ b/js/src/vm/SharedArrayObject.cpp
@@ -362,23 +362,16 @@ SharedArrayBufferObject::createFromNewRa
         return nullptr;
     }
 
     obj->acceptRawBuffer(buffer, initialSize);
 
     return obj;
 }
 
-static JSObject*
-CreateSharedArrayBufferPrototype(JSContext* cx, JSProtoKey key)
-{
-    return GlobalObject::createBlankPrototype(cx, cx->global(),
-                                              &SharedArrayBufferObject::protoClass_);
-}
-
 static const ClassOps SharedArrayBufferObjectClassOps = {
     nullptr, /* addProperty */
     nullptr, /* delProperty */
     nullptr, /* enumerate */
     nullptr, /* newEnumerate */
     nullptr, /* resolve */
     nullptr, /* mayResolve */
     SharedArrayBufferObject::Finalize,
@@ -405,17 +398,17 @@ static const JSFunctionSpec sharedarray_
 static const JSPropertySpec sharedarray_proto_properties[] = {
     JS_PSG("byteLength", SharedArrayBufferObject::byteLengthGetter, 0),
     JS_STRING_SYM_PS(toStringTag, "SharedArrayBuffer", JSPROP_READONLY),
     JS_PS_END
 };
 
 static const ClassSpec SharedArrayBufferObjectClassSpec = {
     GenericCreateConstructor<SharedArrayBufferObject::class_constructor, 1, gc::AllocKind::FUNCTION>,
-    CreateSharedArrayBufferPrototype,
+    GenericCreatePrototype<SharedArrayBufferObject>,
     sharedarrray_functions,
     sharedarrray_properties,
     sharedarray_proto_functions,
     sharedarray_proto_properties
 };
 
 const Class SharedArrayBufferObject::class_ = {
     "SharedArrayBuffer",
--- a/js/src/vm/StructuredClone.cpp
+++ b/js/src/vm/StructuredClone.cpp
@@ -1844,17 +1844,17 @@ JSStructuredCloneWriter::startWrite(Hand
                 return writeTypedArray(obj);
             }
             if (JS_IsDataViewObject(obj)) {
                 return writeDataView(obj);
             }
             if (wasm::IsSharedWasmMemoryObject(obj)) {
                 return writeSharedWasmMemory(obj);
             }
-            if (SavedFrame::isSavedFrameOrWrapperAndNotProto(*obj)) {
+            if (SavedFrame::isSavedFrameOrWrapper(*obj)) {
                 return traverseSavedFrame(obj);
             }
             break;
           }
         }
 
         if (out.buf.callbacks_ && out.buf.callbacks_->write) {
             return out.buf.callbacks_->write(context(), this, obj, out.buf.closure_);
@@ -2080,17 +2080,17 @@ JSStructuredCloneWriter::write(HandleVal
 
                 counts.back()--;
                 val = otherEntries.popCopy();
                 checkStack();
 
                 if (!startWrite(key) || !startWrite(val)) {
                     return false;
                 }
-            } else if (cls == ESClass::Set || SavedFrame::isSavedFrameOrWrapperAndNotProto(*obj)) {
+            } else if (cls == ESClass::Set || SavedFrame::isSavedFrameOrWrapper(*obj)) {
                 key = otherEntries.popCopy();
                 checkStack();
 
                 if (!startWrite(key)) {
                     return false;
                 }
             } else {
                 id = objectEntries.popCopy();
--- a/js/src/vm/TypedArrayObject.cpp
+++ b/js/src/vm/TypedArrayObject.cpp
@@ -1762,37 +1762,39 @@ TypedArrayObject::staticFunctions[] = {
 };
 
 /* static */ const JSPropertySpec
 TypedArrayObject::staticProperties[] = {
     JS_SELF_HOSTED_SYM_GET(species, "TypedArraySpecies", 0),
     JS_PS_END
 };
 
+static JSObject*
+CreateSharedTypedArrayPrototype(JSContext* cx, JSProtoKey key)
+{
+    return GlobalObject::createBlankPrototype(cx,
+                                              cx->global(),
+                                              &TypedArrayObject::sharedTypedArrayPrototypeClass);
+}
+
 static const ClassSpec
 TypedArrayObjectSharedTypedArrayPrototypeClassSpec = {
     GenericCreateConstructor<TypedArrayConstructor, 0, gc::AllocKind::FUNCTION>,
-    GenericCreatePrototype,
+    CreateSharedTypedArrayPrototype,
     TypedArrayObject::staticFunctions,
     TypedArrayObject::staticProperties,
     TypedArrayObject::protoFunctions,
     TypedArrayObject::protoAccessors,
     nullptr,
     ClassSpec::DontDefineConstructor
 };
 
 /* static */ const Class
 TypedArrayObject::sharedTypedArrayPrototypeClass = {
-    // Actually ({}).toString.call(%TypedArray%.prototype) should throw,
-    // because %TypedArray%.prototype lacks the the typed array internal
-    // slots.  (It's not clear this is desirable -- particularly applied to
-    // the actual typed array prototypes, see below -- but it's what ES6
-    // draft 20140824 requires.)  But this is about as much as we can do
-    // until we implement @@toStringTag.
-    "???",
+    "TypedArrayPrototype",
     JSCLASS_HAS_CACHED_PROTO(JSProto_TypedArray),
     JS_NULL_CLASS_OPS,
     &TypedArrayObjectSharedTypedArrayPrototypeClassSpec
 };
 
 // this default implementation is only valid for integer types
 // less than 32-bits in size.
 template<typename NativeType>
--- a/layout/base/PresShell.cpp
+++ b/layout/base/PresShell.cpp
@@ -783,29 +783,29 @@ nsIPresShell::nsIPresShell()
     , mFontSizeInflationMinTwips(0)
     , mFontSizeInflationLineThreshold(0)
     , mFontSizeInflationForceEnabled(false)
     , mFontSizeInflationDisabledInMasterProcess(false)
     , mFontSizeInflationEnabled(false)
     , mPaintingIsFrozen(false)
     , mIsNeverPainting(false)
     , mInFlush(false)
+    , mCurrentEventFrame(nullptr)
   {}
 
 PresShell::PresShell()
   : mCaretEnabled(false)
 #ifdef DEBUG
   , mInVerifyReflow(false)
   , mCurrentReflowRoot(nullptr)
 #endif
 #ifdef MOZ_REFLOW_PERF
   , mReflowCountMgr(nullptr)
 #endif
   , mMouseLocation(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE)
-  , mCurrentEventFrame(nullptr)
   , mFirstCallbackEventRequest(nullptr)
   , mLastCallbackEventRequest(nullptr)
   , mLastReflowStart(0.0)
   , mLastAnchorScrollPositionY(0)
   , mActiveSuppressDisplayport(0)
   , mAPZFocusSequenceNumber(0)
   , mDocumentLoading(false)
   , mIgnoreFrameDestruction(false)
@@ -2090,16 +2090,32 @@ PresShell::FireResizeEvent()
   nsEventStatus status = nsEventStatus_eIgnore;
 
   if (nsPIDOMWindowOuter* window = mDocument->GetWindow()) {
     EventDispatcher::Dispatch(window, mPresContext, &event, nullptr, &status);
   }
 }
 
 void
+nsIPresShell::NativeAnonymousContentRemoved(nsIContent* aAnonContent)
+{
+  if (aAnonContent == mCurrentEventContent) {
+    mCurrentEventContent = aAnonContent->GetFlattenedTreeParent();
+    mCurrentEventFrame = nullptr;
+  }
+
+  for (unsigned int i = 0; i < mCurrentEventContentStack.Length(); i++) {
+    if (aAnonContent == mCurrentEventContentStack.ElementAt(i)) {
+      mCurrentEventContentStack.ReplaceObjectAt(aAnonContent->GetFlattenedTreeParent(), i);
+      mCurrentEventFrameStack[i] = nullptr;
+    }
+  }
+}
+
+void
 PresShell::SetIgnoreFrameDestruction(bool aIgnore)
 {
   if (mDocument) {
     // We need to tell the ImageLoader to drop all its references to frames
     // because they're about to go away and it won't get notifications of that.
     mDocument->StyleImageLoader()->ClearFrames(mPresContext);
   }
   mIgnoreFrameDestruction = aIgnore;
@@ -6404,28 +6420,28 @@ nsIPresShell::SetCapturingContent(nsICon
     gCaptureInfo.mRetargetToElement = ((aFlags & CAPTURE_RETARGETTOELEMENT) != 0) ||
                                       ((aFlags & CAPTURE_POINTERLOCK) != 0);
     gCaptureInfo.mPreventDrag = (aFlags & CAPTURE_PREVENTDRAG) != 0;
     gCaptureInfo.mPointerLock = (aFlags & CAPTURE_POINTERLOCK) != 0;
   }
 }
 
 nsIContent*
-PresShell::GetCurrentEventContent()
+nsIPresShell::GetCurrentEventContent()
 {
   if (mCurrentEventContent &&
       mCurrentEventContent->GetComposedDoc() != mDocument) {
     mCurrentEventContent = nullptr;
     mCurrentEventFrame = nullptr;
   }
   return mCurrentEventContent;
 }
 
 nsIFrame*
-PresShell::GetCurrentEventFrame()
+nsIPresShell::GetCurrentEventFrame()
 {
   if (MOZ_UNLIKELY(mIsDestroying)) {
     return nullptr;
   }
 
   // GetCurrentEventContent() makes sure the content is still in the
   // same document that this pres shell belongs to. If not, then the
   // frame shouldn't get an event, nor should we even assume its safe
@@ -6435,43 +6451,43 @@ PresShell::GetCurrentEventFrame()
     mCurrentEventFrame = content->GetPrimaryFrame();
     MOZ_ASSERT(!mCurrentEventFrame ||
                mCurrentEventFrame->PresContext()->GetPresShell() == this);
   }
   return mCurrentEventFrame;
 }
 
 already_AddRefed<nsIContent>
-PresShell::GetEventTargetContent(WidgetEvent* aEvent)
+nsIPresShell::GetEventTargetContent(WidgetEvent* aEvent)
 {
   nsCOMPtr<nsIContent> content = GetCurrentEventContent();
   if (!content) {
     nsIFrame* currentEventFrame = GetCurrentEventFrame();
     if (currentEventFrame) {
       currentEventFrame->GetContentForEvent(aEvent, getter_AddRefs(content));
       NS_ASSERTION(!content || content->GetComposedDoc() == mDocument,
                    "handing out content from a different doc");
     }
   }
   return content.forget();
 }
 
 void
-PresShell::PushCurrentEventInfo(nsIFrame* aFrame, nsIContent* aContent)
+nsIPresShell::PushCurrentEventInfo(nsIFrame* aFrame, nsIContent* aContent)
 {
   if (mCurrentEventFrame || mCurrentEventContent) {
     mCurrentEventFrameStack.InsertElementAt(0, mCurrentEventFrame);
     mCurrentEventContentStack.InsertObjectAt(mCurrentEventContent, 0);
   }
   mCurrentEventFrame = aFrame;
   mCurrentEventContent = aContent;
 }
 
 void
-PresShell::PopCurrentEventInfo()
+nsIPresShell::PopCurrentEventInfo()
 {
   mCurrentEventFrame = nullptr;
   mCurrentEventContent = nullptr;
 
   if (0 != mCurrentEventFrameStack.Length()) {
     mCurrentEventFrame = mCurrentEventFrameStack.ElementAt(0);
     mCurrentEventFrameStack.RemoveElementAt(0);
     mCurrentEventContent = mCurrentEventContentStack.ObjectAt(0);
--- a/layout/base/PresShell.h
+++ b/layout/base/PresShell.h
@@ -168,18 +168,16 @@ public:
   nsresult HandleEventWithTarget(WidgetEvent* aEvent,
                                  nsIFrame* aFrame,
                                  nsIContent* aContent,
                                  nsEventStatus* aStatus,
                                  bool aIsHandlingNativeEvent = false,
                                  nsIContent** aTargetContent = nullptr,
                                  nsIContent* aOverrideClickTarget = nullptr)
                                  override;
-  already_AddRefed<nsIContent>
-    GetEventTargetContent(WidgetEvent* aEvent) override;
 
   void NotifyCounterStylesAreDirty() override;
 
   void ReconstructFrames(void) override;
   void Freeze() override;
   void Thaw() override;
   void FireOrClearDelayedEvents(bool aFireEvents) override;
 
@@ -652,22 +650,19 @@ private:
   };
   void ProcessSynthMouseMoveEvent(bool aFromScroll);
 
   void QueryIsActive();
   nsresult UpdateImageLockingState();
 
   bool InZombieDocument(nsIContent *aContent);
   already_AddRefed<nsIPresShell> GetParentPresShellForEventHandling();
-  nsIContent* GetCurrentEventContent();
-  nsIFrame* GetCurrentEventFrame() override;
   MOZ_CAN_RUN_SCRIPT nsresult
   RetargetEventToParent(WidgetGUIEvent* aEvent, nsEventStatus* aEventStatus);
-  void PushCurrentEventInfo(nsIFrame* aFrame, nsIContent* aContent);
-  void PopCurrentEventInfo();
+
   /**
    * @param aIsHandlingNativeEvent      true when the caller (perhaps) handles
    *                                    an event which is caused by native
    *                                    event.  Otherwise, false.
    */
   nsresult HandleEventInternal(WidgetEvent* aEvent,
                                nsEventStatus* aStatus,
                                bool aIsHandlingNativeEvent,
@@ -782,20 +777,16 @@ private:
   RefPtr<StyleSheet> mPrefStyleSheet;
 
   // Set of frames that we should mark with NS_FRAME_HAS_DIRTY_CHILDREN after
   // we finish reflowing mCurrentReflowRoot.
   nsTHashtable<nsPtrHashKey<nsIFrame> > mFramesToDirty;
 
   nsTArray<nsAutoPtr<DelayedEvent> > mDelayedEvents;
 private:
-  nsIFrame* mCurrentEventFrame;
-  nsCOMPtr<nsIContent> mCurrentEventContent;
-  nsTArray<nsIFrame*> mCurrentEventFrameStack;
-  nsCOMArray<nsIContent> mCurrentEventContentStack;
   nsRevocableEventPtr<nsSynthMouseMoveEvent> mSynthMouseMoveEvent;
   nsCOMPtr<nsIContent> mLastAnchorScrolledTo;
   RefPtr<nsCaret> mCaret;
   RefPtr<nsCaret> mOriginalCaret;
   nsCallbackEventRequest* mFirstCallbackEventRequest;
   nsCallbackEventRequest* mLastCallbackEventRequest;
 
   TouchManager mTouchManager;
--- a/layout/base/nsIPresShell.h
+++ b/layout/base/nsIPresShell.h
@@ -37,16 +37,17 @@
 #include "nsChangeHint.h"
 #include "nsRefPtrHashtable.h"
 #include "nsClassHashtable.h"
 #include "nsPresArena.h"
 #include "nsIImageLoadingContent.h"
 #include "nsMargin.h"
 #include "nsFrameState.h"
 #include "nsStubDocumentObserver.h"
+#include "nsCOMArray.h"
 #include "Units.h"
 
 class gfxContext;
 class nsDocShell;
 class nsIDocument;
 class nsIFrame;
 class nsPresContext;
 class nsWindowSizes;
@@ -949,23 +950,22 @@ public:
    * Return whether or not the event is valid to be dispatched
    */
   virtual bool CanDispatchEvent(
       const mozilla::WidgetGUIEvent* aEvent = nullptr) const = 0;
 
   /**
     * Gets the current target event frame from the PresShell
     */
-  virtual nsIFrame* GetCurrentEventFrame() = 0;
+  nsIFrame* GetCurrentEventFrame();
 
   /**
     * Gets the current target event frame from the PresShell
     */
-  virtual already_AddRefed<nsIContent> GetEventTargetContent(
-                                                     mozilla::WidgetEvent* aEvent) = 0;
+  already_AddRefed<nsIContent> GetEventTargetContent(mozilla::WidgetEvent* aEvent);
 
   /**
    * Get and set the history state for the current document
    */
 
   virtual nsresult CaptureHistoryState(nsILayoutHistoryState** aLayoutHistoryState) = 0;
 
   /**
@@ -1621,16 +1621,18 @@ public:
 
   /**
    * Returns whether or not the document has ever handled user input
    */
   virtual bool HasHandledUserInput() const = 0;
 
   virtual void FireResizeEvent() = 0;
 
+  void NativeAnonymousContentRemoved(nsIContent* aAnonContent);
+
 protected:
   /**
    * Refresh observer management.
    */
   void DoObserveStyleFlushes();
   void DoObserveLayoutFlushes();
 
   /**
@@ -1648,16 +1650,20 @@ protected:
 
   void RecordFree(void* aPtr) {
 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
     MOZ_DIAGNOSTIC_ASSERT(mAllocatedPointers.Contains(aPtr));
     mAllocatedPointers.RemoveEntry(aPtr);
 #endif
   }
 
+  void PushCurrentEventInfo(nsIFrame* aFrame, nsIContent* aContent);
+  void PopCurrentEventInfo();
+  nsIContent* GetCurrentEventContent();
+
 public:
   bool AddRefreshObserver(nsARefreshObserver* aObserver,
                           mozilla::FlushType aFlushType);
   bool RemoveRefreshObserver(nsARefreshObserver* aObserver,
                              mozilla::FlushType aFlushType);
 
   bool AddPostRefreshObserver(nsAPostRefreshObserver* aObserver);
   bool RemovePostRefreshObserver(nsAPostRefreshObserver* aObserver);
@@ -1862,13 +1868,18 @@ protected:
   // If a document belongs to an invisible DocShell, this flag must be set
   // to true, so we can avoid any paint calls for widget related to this
   // presshell.
   bool mIsNeverPainting;
 
   // Whether we're currently under a FlushPendingNotifications.
   // This is used to handle flush reentry correctly.
   bool mInFlush;
+
+  nsIFrame* mCurrentEventFrame;
+  nsCOMPtr<nsIContent> mCurrentEventContent;
+  nsTArray<nsIFrame*> mCurrentEventFrameStack;
+  nsCOMArray<nsIContent> mCurrentEventContentStack;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsIPresShell, NS_IPRESSHELL_IID)
 
 #endif /* nsIPresShell_h___ */
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -10120,110 +10120,16 @@ nsLayoutUtils::ComputeSystemFont(nsFont*
       aSystemFont->size =
         std::max(aDefaultVariableFont->size -
                  nsPresContext::CSSPointsToAppUnits(2), 0);
     }
 #endif
   }
 }
 
-static inline void
-AssertValidFontTag(const nsString& aString)
-{
-  // To be valid as a font feature tag, a string MUST be:
-  MOZ_ASSERT(aString.Length() == 4 &&              // (1) exactly 4 chars long
-             NS_IsAscii(aString.BeginReading()) && // (2) entirely ASCII
-             isprint(aString[0]) &&                // (3) all printable chars
-             isprint(aString[1]) &&
-             isprint(aString[2]) &&
-             isprint(aString[3]));
-}
-
-/* static */ void
-nsLayoutUtils::ComputeFontFeatures(const nsCSSValuePairList *aFeaturesList,
-                                   nsTArray<gfxFontFeature>& aFeatureSettings)
-{
-  aFeatureSettings.Clear();
-  for (const nsCSSValuePairList* p = aFeaturesList; p; p = p->mNext) {
-    gfxFontFeature feat;
-
-    MOZ_ASSERT(aFeaturesList->mXValue.GetUnit() == eCSSUnit_String,
-               "unexpected value unit");
-
-    // tag is a 4-byte ASCII sequence
-    nsAutoString tag;
-    p->mXValue.GetStringValue(tag);
-    AssertValidFontTag(tag);
-    if (tag.Length() != 4) {
-      continue;
-    }
-    // parsing validates that these are ASCII chars
-    // tags are always big-endian
-    feat.mTag = (tag[0] << 24) | (tag[1] << 16) | (tag[2] << 8)  | tag[3];
-
-    // value
-    NS_ASSERTION(p->mYValue.GetUnit() == eCSSUnit_Integer,
-                 "should have found an integer unit");
-    feat.mValue = p->mYValue.GetIntValue();
-
-    aFeatureSettings.AppendElement(feat);
-  }
-}
-
-/* static */ void
-nsLayoutUtils::ComputeFontVariations(const nsCSSValuePairList* aVariationsList,
-                                     nsTArray<gfxFontVariation>& aVariationSettings)
-{
-  aVariationSettings.Clear();
-  for (const nsCSSValuePairList* p = aVariationsList; p; p = p->mNext) {
-    gfxFontVariation var;
-
-    MOZ_ASSERT(aVariationsList->mXValue.GetUnit() == eCSSUnit_String,
-               "unexpected value unit");
-
-    // tag is a 4-byte ASCII sequence
-    nsAutoString tag;
-    p->mXValue.GetStringValue(tag);
-    AssertValidFontTag(tag);
-    if (tag.Length() != 4) {
-      continue;
-    }
-    // parsing validates that these are ASCII chars
-    // tags are always big-endian
-    var.mTag = (tag[0] << 24) | (tag[1] << 16) | (tag[2] << 8)  | tag[3];
-
-    // value
-    NS_ASSERTION(p->mYValue.GetUnit() == eCSSUnit_Number,
-                 "should have found a number unit");
-    var.mValue = p->mYValue.GetFloatValue();
-
-    aVariationSettings.AppendElement(var);
-  }
-}
-
-/* static */ uint32_t
-nsLayoutUtils::ParseFontLanguageOverride(const nsAString& aLangTag)
-{
-  if (!aLangTag.Length() || aLangTag.Length() > 4) {
-    return NO_FONT_LANGUAGE_OVERRIDE;
-  }
-  uint32_t index, result = 0;
-  for (index = 0; index < aLangTag.Length(); ++index) {
-    char16_t ch = aLangTag[index];
-    if (!nsCRT::IsAscii(ch)) { // valid tags are pure ASCII
-      return NO_FONT_LANGUAGE_OVERRIDE;
-    }
-    result = (result << 8) + ch;
-  }
-  while (index++ < 4) {
-    result = (result << 8) + 0x20;
-  }
-  return result;
-}
-
 /* static */ bool
 nsLayoutUtils::ShouldHandleMetaViewport(nsIDocument* aDocument)
 {
   uint32_t metaViewportOverride = nsIDocShell::META_VIEWPORT_OVERRIDE_NONE;
   if (aDocument) {
     if (nsIDocShell* docShell = aDocument->GetDocShell()) {
       docShell->GetMetaViewportOverride(&metaViewportOverride);
     }
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -246,16 +246,17 @@ nsReflowStatus::UpdateTruncated(const Re
 }
 
 /* static */ void
 nsIFrame::DestroyAnonymousContent(nsPresContext* aPresContext,
                                   already_AddRefed<nsIContent>&& aContent)
 {
   if (nsCOMPtr<nsIContent> content = aContent) {
     aPresContext->EventStateManager()->NativeAnonymousContentRemoved(content);
+    aPresContext->PresShell()->NativeAnonymousContentRemoved(content);
     content->UnbindFromTree();
   }
 }
 
 // Formerly the nsIFrameDebug interface
 
 std::ostream& operator<<(std::ostream& aStream,
                          const nsReflowStatus& aStatus)
--- a/layout/painting/FrameLayerBuilder.cpp
+++ b/layout/painting/FrameLayerBuilder.cpp
@@ -5514,22 +5514,16 @@ FrameLayerBuilder::ComputeGeometryChange
   }
 
   PaintedDisplayItemLayerUserData* layerData =
     static_cast<PaintedDisplayItemLayerUserData*>(
       aData->mLayer->GetUserData(&gPaintedDisplayItemLayerUserData));
   nsPoint shift = layerData->mAnimatedGeometryRootOrigin -
                   layerData->mLastAnimatedGeometryRootOrigin;
 
-  if (aData->mTransform) {
-    // If this display item is inside a flattened transform, the shift is
-    // already included in the root transform.
-    shift = nsPoint();
-  }
-
   const DisplayItemClip& clip = item->GetClip();
   const int32_t appUnitsPerDevPixel = layerData->mAppUnitsPerDevPixel;
 
   // If the frame is marked as invalidated, and didn't specify a rect to
   // invalidate then we want to invalidate both the old and new bounds,
   // otherwise we only want to invalidate the changed areas. If we do get an
   // invalid rect, then we want to add this on top of the change areas.
   nsRect invalid;
@@ -5581,16 +5575,22 @@ FrameLayerBuilder::ComputeGeometryChange
     }
 #endif
   } else {
     // Let the display item check for geometry changes and decide what needs to
     // be repainted.
     const nsRegion& changedFrameInvalidations =
       aData->GetChangedFrameInvalidations();
 
+    if (aData->mTransform) {
+      // If this display item is inside a flattened transform the offset is
+      // already included in the root transform, so there is no need to shift.
+      shift = nsPoint();
+    }
+
     aData->mGeometry->MoveBy(shift);
 
     nsRegion combined;
     item->ComputeInvalidationRegion(
       mDisplayListBuilder, aData->mGeometry, &combined);
 
     // Only allocate a new geometry object if something actually changed,
     // otherwise the existing one should be fine. We always reallocate for
--- a/layout/style/FontFace.cpp
+++ b/layout/style/FontFace.cpp
@@ -218,33 +218,17 @@ FontFace::InitializeSource(const StringO
   SetStatus(FontFaceLoadStatus::Loading);
   DoLoad();
 }
 
 void
 FontFace::GetFamily(nsString& aResult)
 {
   mFontFaceSet->FlushUserFontSet();
-
-  // Serialize the same way as in nsCSSFontFaceStyleDecl::GetPropertyValue.
-  nsCSSValue value;
-  GetDesc(eCSSFontDesc_Family, value);
-
-  aResult.Truncate();
-
-  if (value.GetUnit() == eCSSUnit_Null) {
-    return;
-  }
-
-  nsDependentString family(value.GetStringBufferValue());
-  if (!family.IsEmpty()) {
-    // The string length can be zero when the author passed an invalid
-    // family name or an invalid descriptor to the JS FontFace constructor.
-    nsStyleUtil::AppendEscapedCSSString(family, aResult);
-  }
+  GetDesc(eCSSFontDesc_Family, aResult);
 }
 
 void
 FontFace::SetFamily(const nsAString& aValue, ErrorResult& aRv)
 {
   mFontFaceSet->FlushUserFontSet();
   SetDescriptor(eCSSFontDesc_Family, aValue, aRv);
 }
@@ -290,20 +274,16 @@ FontFace::SetStretch(const nsAString& aV
   mFontFaceSet->FlushUserFontSet();
   SetDescriptor(eCSSFontDesc_Stretch, aValue, aRv);
 }
 
 void
 FontFace::GetUnicodeRange(nsString& aResult)
 {
   mFontFaceSet->FlushUserFontSet();
-
-  // There is no eCSSProperty_unicode_range for us to pass in to GetDesc
-  // to get a serialized (possibly defaulted) value, but that function
-  // doesn't use the property ID for this descriptor anyway.
   GetDesc(eCSSFontDesc_UnicodeRange, aResult);
 }
 
 void
 FontFace::SetUnicodeRange(const nsAString& aValue, ErrorResult& aRv)
 {
   mFontFaceSet->FlushUserFontSet();
   SetDescriptor(eCSSFontDesc_UnicodeRange, aValue, aRv);
@@ -602,23 +582,16 @@ FontFace::SetDescriptors(const nsAString
     SetStatus(FontFaceLoadStatus::Error);
     return false;
   }
 
   return true;
 }
 
 void
-FontFace::GetDesc(nsCSSFontDesc aDescID, nsCSSValue& aResult) const
-{
-  aResult.Reset();
-  Servo_FontFaceRule_GetDescriptor(GetData(), aDescID, &aResult);
-}
-
-void
 FontFace::GetDesc(nsCSSFontDesc aDescID, nsString& aResult) const
 {
   aResult.Truncate();
   Servo_FontFaceRule_GetDescriptorCssText(GetData(), aDescID, &aResult);
 
   // Fill in a default value for missing descriptors.
   if (aResult.IsEmpty()) {
     if (aDescID == eCSSFontDesc_UnicodeRange) {
@@ -661,29 +634,101 @@ FontFace::SetUserFontEntry(gfxUserFontEn
     FontFaceLoadStatus newStatus =
       LoadStateToStatus(mUserFontEntry->LoadState());
     if (newStatus > mStatus) {
       SetStatus(newStatus);
     }
   }
 }
 
-bool
-FontFace::GetFamilyName(nsCString& aResult)
+Maybe<StyleComputedFontWeightRange>
+FontFace::GetFontWeight() const
+{
+  StyleComputedFontWeightRange range;
+  if (!Servo_FontFaceRule_GetFontWeight(GetData(), &range)) {
+    return Nothing();
+  }
+  return Some(range);
+}
+
+Maybe<StyleComputedFontStretchRange>
+FontFace::GetFontStretch() const
 {
-  nsCSSValue value;
-  GetDesc(eCSSFontDesc_Family, value);
+  StyleComputedFontStretchRange range;
+  if (!Servo_FontFaceRule_GetFontStretch(GetData(), &range)) {
+    return Nothing();
+  }
+  return Some(range);
+}
+
+Maybe<StyleComputedFontStyleDescriptor>
+FontFace::GetFontStyle() const
+{
+  StyleComputedFontStyleDescriptor descriptor;
+  if (!Servo_FontFaceRule_GetFontStyle(GetData(), &descriptor)) {
+    return Nothing();
+  }
+  return Some(descriptor);
+}
+
+Maybe<StyleFontDisplay>
+FontFace::GetFontDisplay() const
+{
+  StyleFontDisplay display;
+  if (!Servo_FontFaceRule_GetFontDisplay(GetData(), &display)) {
+    return Nothing();
+  }
+  return Some(display);
+}
 
-  if (value.GetUnit() == eCSSUnit_String) {
-    nsString familyname;
-    value.GetStringValue(familyname);
-    AppendUTF16toUTF8(familyname, aResult);
+Maybe<StyleFontLanguageOverride>
+FontFace::GetFontLanguageOverride() const
+{
+  StyleFontLanguageOverride langOverride;
+  if (!Servo_FontFaceRule_GetFontLanguageOverride(GetData(), &langOverride)) {
+    return Nothing();
+  }
+  return Some(langOverride);
+}
+
+bool
+FontFace::HasLocalSrc() const
+{
+  AutoTArray<StyleFontFaceSourceListComponent, 8> components;
+  GetSources(components);
+  for (auto& component : components) {
+    if (component.tag == StyleFontFaceSourceListComponent::Tag::Local) {
+      return true;
+    }
   }
+  return false;
+}
 
-  return !aResult.IsEmpty();
+void
+FontFace::GetFontFeatureSettings(nsTArray<gfxFontFeature>& aFeatures) const
+{
+  Servo_FontFaceRule_GetFeatureSettings(GetData(), &aFeatures);
+}
+
+void
+FontFace::GetFontVariationSettings(nsTArray<gfxFontVariation>& aVariations) const
+{
+  Servo_FontFaceRule_GetVariationSettings(GetData(), &aVariations);
+}
+
+void
+FontFace::GetSources(nsTArray<StyleFontFaceSourceListComponent>& aSources) const
+{
+  Servo_FontFaceRule_GetSources(GetData(), &aSources);
+}
+
+nsAtom*
+FontFace::GetFamilyName() const
+{
+  return Servo_FontFaceRule_GetFamilyName(GetData());
 }
 
 void
 FontFace::DisconnectFromRule()
 {
   MOZ_ASSERT(HasRule());
 
   // Make a copy of the descriptors.
@@ -790,29 +835,25 @@ FontFace::EnsurePromise()
 
 gfxCharacterMap*
 FontFace::GetUnicodeRangeAsCharacterMap()
 {
   if (!mUnicodeRangeDirty) {
     return mUnicodeRange;
   }
 
-  nsCSSValue val;
-  GetDesc(eCSSFontDesc_UnicodeRange, val);
+  size_t len;
+  const StyleUnicodeRange* rangesPtr =
+    Servo_FontFaceRule_GetUnicodeRanges(GetData(), &len);
 
-  if (val.GetUnit() == eCSSUnit_Array) {
+  Span<const StyleUnicodeRange> ranges(rangesPtr, len);
+  if (!ranges.IsEmpty()) {
     mUnicodeRange = new gfxCharacterMap();
-    const nsCSSValue::Array& sources = *val.GetArrayValue();
-    MOZ_ASSERT(sources.Count() % 2 == 0,
-               "odd number of entries in a unicode-range: array");
-
-    for (uint32_t i = 0; i < sources.Count(); i += 2) {
-      uint32_t min = sources[i].GetIntValue();
-      uint32_t max = sources[i+1].GetIntValue();
-      mUnicodeRange->SetRange(min, max);
+    for (auto& range : ranges) {
+      mUnicodeRange->SetRange(range.start, range.end);
     }
   } else {
     mUnicodeRange = nullptr;
   }
 
   mUnicodeRangeDirty = false;
   return mUnicodeRange;
 }
--- a/layout/style/FontFace.h
+++ b/layout/style/FontFace.h
@@ -4,16 +4,19 @@
  * 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/. */
 
 #ifndef mozilla_dom_FontFace_h
 #define mozilla_dom_FontFace_h
 
 #include "mozilla/dom/FontFaceBinding.h"
 #include "mozilla/FontPropertyTypes.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Pair.h"
+#include "mozilla/ServoStyleConsts.h"
 #include "gfxUserFontSet.h"
 #include "nsAutoPtr.h"
 #include "nsCSSPropertyID.h"
 #include "nsCSSValue.h"
 #include "nsWrapperCache.h"
 
 class gfxFontFaceBufferSource;
 struct RawServoFontFaceRule;
@@ -50,17 +53,17 @@ public:
           const nsTArray<gfxFontFaceSrc>& aFontFaceSrcList,
           WeightRange aWeight,
           StretchRange aStretch,
           SlantStyleRange aStyle,
           const nsTArray<gfxFontFeature>& aFeatureSettings,
           const nsTArray<gfxFontVariation>& aVariationSettings,
           uint32_t aLanguageOverride,
           gfxCharacterMap* aUnicodeRanges,
-          uint8_t aFontDisplay,
+          StyleFontDisplay aFontDisplay,
           RangeFlags aRangeFlags)
       : gfxUserFontEntry(aFontSet, aFontFaceSrcList, aWeight, aStretch,
                          aStyle, aFeatureSettings, aVariationSettings,
                          aLanguageOverride,
                          aUnicodeRanges, aFontDisplay,
                          aRangeFlags) {}
 
     virtual void SetLoadState(UserFontLoadState aLoadState) override;
@@ -82,17 +85,25 @@ public:
   virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   static already_AddRefed<FontFace>
   CreateForRule(nsISupports* aGlobal, FontFaceSet* aFontFaceSet,
                 RawServoFontFaceRule* aRule);
 
   RawServoFontFaceRule* GetRule() { return mRule; }
 
-  void GetDesc(nsCSSFontDesc aDescID, nsCSSValue& aResult) const;
+  bool HasLocalSrc() const;
+  Maybe<StyleComputedFontWeightRange> GetFontWeight() const;
+  Maybe<StyleComputedFontStretchRange> GetFontStretch() const;
+  Maybe<StyleComputedFontStyleDescriptor> GetFontStyle() const;
+  Maybe<StyleFontDisplay> GetFontDisplay() const;
+  void GetFontFeatureSettings(nsTArray<gfxFontFeature>&) const;
+  void GetFontVariationSettings(nsTArray<gfxFontVariation>&) const;
+  void GetSources(nsTArray<StyleFontFaceSourceListComponent>&) const;
+  Maybe<StyleFontLanguageOverride> GetFontLanguageOverride() const;
 
   gfxUserFontEntry* CreateUserFontEntry();
   gfxUserFontEntry* GetUserFontEntry() const { return mUserFontEntry; }
   void SetUserFontEntry(gfxUserFontEntry* aEntry);
 
   /**
    * Returns whether this object is in the specified FontFaceSet.
    */
@@ -101,19 +112,19 @@ public:
   void AddFontFaceSet(FontFaceSet* aFontFaceSet);
   void RemoveFontFaceSet(FontFaceSet* aFontFaceSet);
 
   FontFaceSet* GetPrimaryFontFaceSet() const { return mFontFaceSet; }
 
   /**
    * Gets the family name of the FontFace as a raw string (such as 'Times', as
    * opposed to GetFamily, which returns a CSS-escaped string, such as
-   * '"Times"').  Returns whether a valid family name was available.
+   * '"Times"').  Returns null if a valid family name was not available.
    */
-  bool GetFamilyName(nsCString& aResult);
+  nsAtom* GetFamilyName() const;
 
   /**
    * Returns whether this object is CSS-connected, i.e. reflecting an
    * @font-face rule.
    */
   bool HasRule() const { return mRule; }
 
   /**
--- a/layout/style/FontFaceSet.cpp
+++ b/layout/style/FontFaceSet.cpp
@@ -206,53 +206,46 @@ void
 FontFaceSet::ParseFontShorthandForMatching(
                             const nsAString& aFont,
                             RefPtr<SharedFontList>& aFamilyList,
                             FontWeight& aWeight,
                             FontStretch& aStretch,
                             FontSlantStyle& aStyle,
                             ErrorResult& aRv)
 {
-  nsCSSValue style;
-  nsCSSValue stretch;
-  nsCSSValue weight;
+  StyleComputedFontStyleDescriptor style;
+  float stretch;
+  float weight;
 
   // FIXME(emilio): This Servo -> nsCSSValue -> Gecko conversion is stupid,
   // Servo understands the font types.
   RefPtr<URLExtraData> url = ServoCSSParser::GetURLExtraData(mDocument);
   if (!ServoCSSParser::ParseFontShorthandForMatching(
         aFont, url, aFamilyList, style, stretch, weight)) {
     aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR);
     return;
   }
 
-  switch (style.GetUnit()) {
-    case eCSSUnit_Normal:
+  switch (style.tag) {
+    case StyleComputedFontStyleDescriptor::Tag::Normal:
       aStyle = FontSlantStyle::Normal();
       break;
-    case eCSSUnit_Enumerated:
-      MOZ_ASSERT(style.GetIntValue() == NS_FONT_STYLE_ITALIC);
+    case StyleComputedFontStyleDescriptor::Tag::Italic:
       aStyle = FontSlantStyle::Italic();
       break;
-    case eCSSUnit_FontSlantStyle:
-      aStyle = style.GetFontSlantStyle();
+    case StyleComputedFontStyleDescriptor::Tag::Oblique:
+      MOZ_ASSERT(style.oblique._0 == style.oblique._1,
+                 "We use ComputedFontStyleDescriptor just for convenience, "
+                 "the two values should always match");
+      aStyle = FontSlantStyle::Oblique(style.oblique._0);
       break;
-    default:
-      MOZ_ASSERT_UNREACHABLE("Unknown unit for font-style");
   }
 
-  if (weight.GetUnit() == eCSSUnit_FontWeight) {
-    aWeight = weight.GetFontWeight();
-  } else {
-    MOZ_ASSERT(weight.GetUnit() == eCSSUnit_Enumerated);
-    aWeight = FontWeight(weight.GetIntValue());
-  }
-
-  MOZ_ASSERT(stretch.GetUnit() == eCSSUnit_FontStretch);
-  aStretch = stretch.GetFontStretch();
+  aWeight = FontWeight(weight);
+  aStretch = FontStretch::FromStyle(stretch);
 }
 
 static bool
 HasAnyCharacterInUnicodeRange(gfxUserFontEntry* aEntry,
                               const nsAString& aInput)
 {
   const char16_t* p = aInput.Data();
   const char16_t* end = p + aInput.Length();
@@ -828,107 +821,94 @@ FontFaceSet::UpdateRules(const nsTArray<
          mUserFontSet.get(),
          (modified ? "modified" : "not modified"),
          (int)(mRuleFaces.Length())));
   }
 
   return modified;
 }
 
-static bool
-HasLocalSrc(const nsCSSValue::Array *aSrcArr)
-{
-  size_t numSrc = aSrcArr->Count();
-  for (size_t i = 0; i < numSrc; i++) {
-    if (aSrcArr->Item(i).GetUnit() == eCSSUnit_Local_Font) {
-      return true;
-    }
-  }
-  return false;
-}
-
 void
 FontFaceSet::IncrementGeneration(bool aIsRebuild)
 {
   MOZ_ASSERT(mUserFontSet);
   mUserFontSet->IncrementGeneration(aIsRebuild);
 }
 
 void
 FontFaceSet::InsertNonRuleFontFace(FontFace* aFontFace,
                                    bool& aFontSetModified)
 {
-  nsAutoCString fontfamily;
-  if (!aFontFace->GetFamilyName(fontfamily)) {
+  nsAtom* fontFamily = aFontFace->GetFamilyName();
+  if (!fontFamily) {
     // If there is no family name, this rule cannot contribute a
     // usable font, so there is no point in processing it further.
     return;
   }
 
+  nsAtomCString family(fontFamily);
+
   // Just create a new font entry if we haven't got one already.
   if (!aFontFace->GetUserFontEntry()) {
     // XXX Should we be checking mUserFontSet->mLocalRulesUsed like
     // InsertRuleFontFace does?
     RefPtr<gfxUserFontEntry> entry =
-      FindOrCreateUserFontEntryFromFontFace(fontfamily, aFontFace,
-                                            SheetType::Doc);
+      FindOrCreateUserFontEntryFromFontFace(family, aFontFace, SheetType::Doc);
     if (!entry) {
       return;
     }
     aFontFace->SetUserFontEntry(entry);
   }
 
   aFontSetModified = true;
-  mUserFontSet->AddUserFontEntry(fontfamily, aFontFace->GetUserFontEntry());
+  mUserFontSet->AddUserFontEntry(family, aFontFace->GetUserFontEntry());
 }
 
 void
 FontFaceSet::InsertRuleFontFace(FontFace* aFontFace, SheetType aSheetType,
                                 nsTArray<FontFaceRecord>& aOldRecords,
                                 bool& aFontSetModified)
 {
-  nsAutoCString fontfamily;
-  if (!aFontFace->GetFamilyName(fontfamily)) {
+  nsAtom* fontFamily = aFontFace->GetFamilyName();
+  if (!fontFamily) {
     // If there is no family name, this rule cannot contribute a
     // usable font, so there is no point in processing it further.
     return;
   }
 
   bool remove = false;
   size_t removeIndex;
 
+  nsAtomCString family(fontFamily);
+
   // This is a rule backed FontFace.  First, we check in aOldRecords; if
   // the FontFace for the rule exists there, just move it to the new record
   // list, and put the entry into the appropriate family.
   for (size_t i = 0; i < aOldRecords.Length(); ++i) {
     FontFaceRecord& rec = aOldRecords[i];
 
     if (rec.mFontFace == aFontFace &&
         rec.mSheetType == aSheetType) {
 
       // if local rules were used, don't use the old font entry
       // for rules containing src local usage
-      if (mUserFontSet->mLocalRulesUsed &&
-          mUserFontSet->mRebuildLocalRules) {
-        nsCSSValue val;
-        aFontFace->GetDesc(eCSSFontDesc_Src, val);
-        nsCSSUnit unit = val.GetUnit();
-        if (unit == eCSSUnit_Array && HasLocalSrc(val.GetArrayValue())) {
+      if (mUserFontSet->mLocalRulesUsed && mUserFontSet->mRebuildLocalRules) {
+        if (aFontFace->HasLocalSrc()) {
           // Remove the old record, but wait to see if we successfully create a
           // new user font entry below.
           remove = true;
           removeIndex = i;
           break;
         }
       }
 
       gfxUserFontEntry* entry = rec.mFontFace->GetUserFontEntry();
       MOZ_ASSERT(entry, "FontFace should have a gfxUserFontEntry by now");
 
-      mUserFontSet->AddUserFontEntry(fontfamily, entry);
+      mUserFontSet->AddUserFontEntry(family, entry);
 
       MOZ_ASSERT(!HasRuleFontFace(rec.mFontFace),
                  "FontFace should not occur in mRuleFaces twice");
 
       mRuleFaces.AppendElement(rec);
       aOldRecords.RemoveElementAt(i);
       // note the set has been modified if an old rule was skipped to find
       // this one - something has been dropped, or ordering changed
@@ -936,17 +916,17 @@ FontFaceSet::InsertRuleFontFace(FontFace
         aFontSetModified = true;
       }
       return;
     }
   }
 
   // this is a new rule:
   RefPtr<gfxUserFontEntry> entry =
-    FindOrCreateUserFontEntryFromFontFace(fontfamily, aFontFace, aSheetType);
+    FindOrCreateUserFontEntryFromFontFace(family, aFontFace, aSheetType);
 
   if (!entry) {
     return;
   }
 
   if (remove) {
     // Although we broke out of the aOldRecords loop above, since we found
     // src local usage, and we're not using the old user font entry, we still
@@ -974,202 +954,121 @@ FontFaceSet::InsertRuleFontFace(FontFace
   // this was a new rule and font entry, so note that the set was modified
   aFontSetModified = true;
 
   // Add the entry to the end of the list.  If an existing userfont entry was
   // returned by FindOrCreateUserFontEntryFromFontFace that was already stored
   // on the family, gfxUserFontFamily::AddFontEntry(), which AddUserFontEntry
   // calls, will automatically remove the earlier occurrence of the same
   // userfont entry.
-  mUserFontSet->AddUserFontEntry(fontfamily, entry);
+  mUserFontSet->AddUserFontEntry(family, entry);
 }
 
 /* static */ already_AddRefed<gfxUserFontEntry>
 FontFaceSet::FindOrCreateUserFontEntryFromFontFace(FontFace* aFontFace)
 {
-  nsAutoCString fontfamily;
-  if (!aFontFace->GetFamilyName(fontfamily)) {
+  nsAtom* fontFamily = aFontFace->GetFamilyName();
+  if (!fontFamily) {
     // If there is no family name, this rule cannot contribute a
     // usable font, so there is no point in processing it further.
     return nullptr;
   }
 
-  return FindOrCreateUserFontEntryFromFontFace(fontfamily, aFontFace,
+  return FindOrCreateUserFontEntryFromFontFace(nsAtomCString(fontFamily), aFontFace,
                                                SheetType::Doc);
 }
 
-static FontWeight
-GetWeightForDescriptor(const nsCSSValue& aVal)
-{
-  switch (aVal.GetUnit()) {
-    case eCSSUnit_FontWeight:
-      return aVal.GetFontWeight();
-    case eCSSUnit_Enumerated:
-      return FontWeight(aVal.GetIntValue());
-    case eCSSUnit_Normal:
-    case eCSSUnit_Null:
-      return FontWeight::Normal();
-    default:
-      MOZ_ASSERT_UNREACHABLE("Unknown font-weight descriptor value");
-      return FontWeight::Normal();
-  }
-}
-
 static WeightRange
-GetWeightRangeForDescriptor(const nsCSSValue& aVal,
+GetWeightRangeForDescriptor(const Maybe<StyleComputedFontWeightRange>& aVal,
                             gfxFontEntry::RangeFlags& aRangeFlags)
 {
-  if (aVal.GetUnit() == eCSSUnit_Null) {
+  if (!aVal) {
     aRangeFlags |= gfxFontEntry::RangeFlags::eAutoWeight;
     return WeightRange(FontWeight::Normal());
   }
-  if (aVal.GetUnit() == eCSSUnit_Pair) {
-    return WeightRange(GetWeightForDescriptor(aVal.GetPairValue().mXValue),
-                       GetWeightForDescriptor(aVal.GetPairValue().mYValue));
-  }
-  return WeightRange(GetWeightForDescriptor(aVal));
-}
-
-static FontSlantStyle
-GetStyleForDescriptor(const nsCSSValue& aVal)
-{
-  switch (aVal.GetUnit()) {
-    case eCSSUnit_Normal:
-    case eCSSUnit_Null:
-      return FontSlantStyle::Normal();
-    case eCSSUnit_Enumerated:
-      MOZ_ASSERT(aVal.GetIntValue() == NS_FONT_STYLE_ITALIC);
-      return FontSlantStyle::Italic();
-    case eCSSUnit_FontSlantStyle:
-      return aVal.GetFontSlantStyle();
-    default:
-      MOZ_ASSERT_UNREACHABLE("Unknown font-style descriptor value");
-      return FontSlantStyle::Normal();
-  }
+  return WeightRange(FontWeight(aVal->_0), FontWeight(aVal->_1));
 }
 
 static SlantStyleRange
-GetStyleRangeForDescriptor(const nsCSSValue& aVal,
+GetStyleRangeForDescriptor(const Maybe<StyleComputedFontStyleDescriptor>& aVal,
                            gfxFontEntry::RangeFlags& aRangeFlags)
 {
-  if (aVal.GetUnit() == eCSSUnit_Null) {
+  if (!aVal) {
     aRangeFlags |= gfxFontEntry::RangeFlags::eAutoSlantStyle;
     return SlantStyleRange(FontSlantStyle::Normal());
   }
-  if (aVal.GetUnit() == eCSSUnit_Pair) {
-    return SlantStyleRange(GetStyleForDescriptor(aVal.GetPairValue().mXValue),
-                           GetStyleForDescriptor(aVal.GetPairValue().mYValue));
+  auto& val = *aVal;
+  switch (val.tag) {
+    case StyleComputedFontStyleDescriptor::Tag::Normal:
+      return SlantStyleRange(FontSlantStyle::Normal());
+    case StyleComputedFontStyleDescriptor::Tag::Italic:
+      return SlantStyleRange(FontSlantStyle::Italic());
+    case StyleComputedFontStyleDescriptor::Tag::Oblique:
+      return SlantStyleRange(FontSlantStyle::Oblique(val.oblique._0),
+                             FontSlantStyle::Oblique(val.oblique._1));
   }
-  return SlantStyleRange(GetStyleForDescriptor(aVal));
-}
-
-static FontStretch
-GetStretchForDescriptor(const nsCSSValue& aVal)
-{
-  switch (aVal.GetUnit()) {
-    case eCSSUnit_Null:
-      return FontStretch::Normal();
-    case eCSSUnit_FontStretch:
-      return aVal.GetFontStretch();
-    default:
-      MOZ_ASSERT_UNREACHABLE("Unknown font-style descriptor value");
-      return FontStretch::Normal();
-  }
+  MOZ_ASSERT_UNREACHABLE("How?");
+  return SlantStyleRange(FontSlantStyle::Normal());
 }
 
 static StretchRange
-GetStretchRangeForDescriptor(const nsCSSValue& aVal,
+GetStretchRangeForDescriptor(const Maybe<StyleComputedFontStretchRange>& aVal,
                              gfxFontEntry::RangeFlags& aRangeFlags)
 {
-  if (aVal.GetUnit() == eCSSUnit_Null) {
+  if (!aVal) {
     aRangeFlags |= gfxFontEntry::RangeFlags::eAutoStretch;
     return StretchRange(FontStretch::Normal());
   }
-  if (aVal.GetUnit() == eCSSUnit_Pair) {
-    return StretchRange(GetStretchForDescriptor(aVal.GetPairValue().mXValue),
-                       GetStretchForDescriptor(aVal.GetPairValue().mYValue));
-  }
-  return StretchRange(GetStretchForDescriptor(aVal));
+  return StretchRange(FontStretch::FromStyle(aVal->_0),
+                      FontStretch::FromStyle(aVal->_1));
 }
 
+// TODO(emilio): Should this take an nsAtom* aFamilyName instead?
+//
+// All callers have one handy.
 /* static */ already_AddRefed<gfxUserFontEntry>
 FontFaceSet::FindOrCreateUserFontEntryFromFontFace(const nsACString& aFamilyName,
                                                    FontFace* aFontFace,
                                                    SheetType aSheetType)
 {
   FontFaceSet* set = aFontFace->GetPrimaryFontFaceSet();
 
-  nsCSSValue val;
-  nsCSSUnit unit;
-
   uint32_t languageOverride = NO_FONT_LANGUAGE_OVERRIDE;
-  uint8_t fontDisplay = NS_FONT_DISPLAY_AUTO;
+  StyleFontDisplay fontDisplay = StyleFontDisplay::Auto;
 
   gfxFontEntry::RangeFlags rangeFlags = gfxFontEntry::RangeFlags::eNoFlags;
 
   // set up weight
-  aFontFace->GetDesc(eCSSFontDesc_Weight, val);
-  WeightRange weight = GetWeightRangeForDescriptor(val, rangeFlags);
+  WeightRange weight =
+    GetWeightRangeForDescriptor(aFontFace->GetFontWeight(), rangeFlags);
 
   // set up stretch
-  aFontFace->GetDesc(eCSSFontDesc_Stretch, val);
-  StretchRange stretch = GetStretchRangeForDescriptor(val, rangeFlags);
+  StretchRange stretch =
+    GetStretchRangeForDescriptor(aFontFace->GetFontStretch(), rangeFlags);
 
   // set up font style
-  aFontFace->GetDesc(eCSSFontDesc_Style, val);
-  SlantStyleRange italicStyle = GetStyleRangeForDescriptor(val, rangeFlags);
+  SlantStyleRange italicStyle =
+    GetStyleRangeForDescriptor(aFontFace->GetFontStyle(), rangeFlags);
 
   // set up font display
-  aFontFace->GetDesc(eCSSFontDesc_Display, val);
-  unit = val.GetUnit();
-  if (unit == eCSSUnit_Enumerated) {
-    fontDisplay = val.GetIntValue();
-  } else {
-    NS_ASSERTION(unit == eCSSUnit_Null,
-                 "@font-face style has unexpected unit");
+  if (Maybe<StyleFontDisplay> display = aFontFace->GetFontDisplay()) {
+    fontDisplay = *display;
   }
 
   // set up font features
   nsTArray<gfxFontFeature> featureSettings;
-  aFontFace->GetDesc(eCSSFontDesc_FontFeatureSettings, val);
-  unit = val.GetUnit();
-  if (unit == eCSSUnit_Normal) {
-    // empty list of features
-  } else if (unit == eCSSUnit_PairList || unit == eCSSUnit_PairListDep) {
-    nsLayoutUtils::ComputeFontFeatures(val.GetPairListValue(), featureSettings);
-  } else {
-    NS_ASSERTION(unit == eCSSUnit_Null,
-                 "@font-face font-feature-settings has unexpected unit");
-  }
+  aFontFace->GetFontFeatureSettings(featureSettings);
 
   // set up font variations
   nsTArray<gfxFontVariation> variationSettings;
-  aFontFace->GetDesc(eCSSFontDesc_FontVariationSettings, val);
-  unit = val.GetUnit();
-  if (unit == eCSSUnit_Normal) {
-    // empty list of variations
-  } else if (unit == eCSSUnit_PairList || unit == eCSSUnit_PairListDep) {
-    nsLayoutUtils::ComputeFontVariations(val.GetPairListValue(), variationSettings);
-  } else {
-    NS_ASSERTION(unit == eCSSUnit_Null,
-                 "@font-face font-variation-settings has unexpected unit");
-  }
+  aFontFace->GetFontVariationSettings(variationSettings);
 
   // set up font language override
-  aFontFace->GetDesc(eCSSFontDesc_FontLanguageOverride, val);
-  unit = val.GetUnit();
-  if (unit == eCSSUnit_Normal) {
-    // empty feature string
-  } else if (unit == eCSSUnit_String) {
-    nsString stringValue;
-    val.GetStringValue(stringValue);
-    languageOverride = nsLayoutUtils::ParseFontLanguageOverride(stringValue);
-  } else {
-    NS_ASSERTION(unit == eCSSUnit_Null,
-                 "@font-face font-language-override has unexpected unit");
+  if (Maybe<StyleFontLanguageOverride> descriptor = aFontFace->GetFontLanguageOverride()) {
+    languageOverride = descriptor->_0;
   }
 
   // set up unicode-range
   gfxCharacterMap* unicodeRanges = aFontFace->GetUnicodeRangeAsCharacterMap();
 
   // set up src array
   nsTArray<gfxFontFaceSrc> srcArray;
 
@@ -1177,70 +1076,63 @@ FontFaceSet::FindOrCreateUserFontEntryFr
     gfxFontFaceSrc* face = srcArray.AppendElement();
     if (!face)
       return nullptr;
 
     face->mSourceType = gfxFontFaceSrc::eSourceType_Buffer;
     face->mBuffer = aFontFace->CreateBufferSource();
     face->mReferrerPolicy = mozilla::net::RP_Unset;
   } else {
-    aFontFace->GetDesc(eCSSFontDesc_Src, val);
-    unit = val.GetUnit();
-    if (unit == eCSSUnit_Array) {
-      // Hold a strong reference because content of val is going away
-      // in the loop below.
-      RefPtr<nsCSSValue::Array> srcArr = val.GetArrayValue();
-      size_t numSrc = srcArr->Count();
-
-      for (size_t i = 0; i < numSrc; i++) {
-        val = srcArr->Item(i);
-        unit = val.GetUnit();
-        gfxFontFaceSrc* face = srcArray.AppendElements(1);
-        if (!face)
-          return nullptr;
-
-        switch (unit) {
-
-        case eCSSUnit_Local_Font: {
-          nsAutoString localName;
-          val.GetStringValue(localName);
-          face->mLocalName.Append(NS_ConvertUTF16toUTF8(localName));
+    AutoTArray<StyleFontFaceSourceListComponent, 8> sourceListComponents;
+    aFontFace->GetSources(sourceListComponents);
+    size_t len = sourceListComponents.Length();
+    for (size_t i = 0; i < len; ++i) {
+      gfxFontFaceSrc* face = srcArray.AppendElement();
+      const auto& component = sourceListComponents[i];
+      switch (component.tag) {
+        case StyleFontFaceSourceListComponent::Tag::Local: {
+          nsAtom* atom = component.local._0;
+          face->mLocalName.Append(nsAtomCString(atom));
           face->mSourceType = gfxFontFaceSrc::eSourceType_Local;
           face->mURI = nullptr;
           face->mFormatFlags = 0;
           face->mReferrerPolicy = mozilla::net::RP_Unset;
           break;
         }
-        case eCSSUnit_URL: {