Merge mozilla-central to inbound. a=merge CLOSED TREE
authorGurzau Raul <rgurzau@mozilla.com>
Wed, 10 Oct 2018 01:04:25 +0300
changeset 488658 38890d8fa2542082eafe6e16dfc26b87d42ade94
parent 488596 4d07362984d4e00ef10251fa54e652426d87bfcf (current diff)
parent 488657 6f8701d1be0ccf42a8e22bfce6f40056a4f58a1b (diff)
child 488659 241f876d557f679b81c2aa6d73f0ec598cc49545
push id246
push userfmarier@mozilla.com
push dateSat, 13 Oct 2018 00:15:40 +0000
reviewersmerge
milestone64.0a1
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