Backed out 4 changesets (bug 1479037) on dev's request for causing android crashes. a=backout
authorDorel Luca <dluca@mozilla.com>
Wed, 10 Oct 2018 18:52:59 +0300
changeset 498871 8dfeff72def34dea3ee1a59185a73d2840ae77f3
parent 498870 91b4c3687d7563244fbba0f58075779eb89259fb
child 498900 0f1d5395f8013b4dafcd4c849a8cccdcf3c26587
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbackout
bugs1479037
milestone64.0a1
backs out85f8ca59f7476aaeede452f7d0ecc7d478ca0144
630601f0663e034517ed983e7a652b61ab089645
c926c64702351c4a8a3ea441dd553c8027dc45f2
f8a20bd8689532d06434a57551e346a6733dbba0
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Backed out 4 changesets (bug 1479037) on dev's request for causing android crashes. a=backout Backed out changeset 85f8ca59f747 (bug 1479037) Backed out changeset 630601f0663e (bug 1479037) Backed out changeset c926c6470235 (bug 1479037) Backed out changeset f8a20bd86895 (bug 1479037)
accessible/android/AccessibleWrap.cpp
accessible/android/AccessibleWrap.h
accessible/android/DocAccessibleWrap.cpp
accessible/android/DocAccessibleWrap.h
accessible/android/Platform.cpp
accessible/android/ProxyAccessibleWrap.cpp
accessible/android/ProxyAccessibleWrap.h
accessible/android/RootAccessibleWrap.cpp
accessible/android/RootAccessibleWrap.h
accessible/android/SessionAccessibility.cpp
accessible/android/SessionAccessibility.h
accessible/android/moz.build
accessible/generic/Accessible.h
accessible/ipc/ProxyAccessibleBase.h
accessible/jsat/AccessFu.jsm
accessible/jsat/ContentControl.jsm
accessible/jsat/EventManager.jsm
accessible/jsat/OutputGenerator.jsm
accessible/jsat/Presentation.jsm
accessible/jsat/moz.build
accessible/tests/mochitest/jsat/a11y.ini
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/chrome/content/browser.js
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
mobile/android/modules/geckoview/GeckoViewAccessibility.jsm
widget/android/bindings/AccessibilityEvent-classes.txt
widget/android/bindings/moz.build
--- a/accessible/android/AccessibleWrap.cpp
+++ b/accessible/android/AccessibleWrap.cpp
@@ -1,509 +1,23 @@
 /* -*- 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() {}
-
-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
+AccessibleWrap::~AccessibleWrap()
 {
-  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,80 +1,25 @@
 /* -*- 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 "nsCOMPtr.h"
 #include "Accessible.h"
-#include "GeneratedJNIWrappers.h"
-#include "mozilla/a11y/ProxyAccessible.h"
-#include "nsCOMPtr.h"
 
 namespace mozilla {
 namespace a11y {
 
 class AccessibleWrap : public Accessible
 {
-public:
+public: // construction, destruction
   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
deleted file mode 100644
--- a/accessible/android/DocAccessibleWrap.cpp
+++ /dev/null
@@ -1,53 +0,0 @@
-/* -*- 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,35 +6,14 @@
 #ifndef mozilla_a11y_DocAccessibleWrap_h__
 #define mozilla_a11y_DocAccessibleWrap_h__
 
 #include "DocAccessible.h"
 
 namespace mozilla {
 namespace a11y {
 
-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;
-};
+typedef DocAccessible DocAccessibleWrap;
 
 } // namespace a11y
 } // namespace mozilla
 
 #endif
--- a/accessible/android/Platform.cpp
+++ b/accessible/android/Platform.cpp
@@ -1,200 +1,89 @@
 /* -*- 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* aProxy, uint32_t aInterfaces)
+a11y::ProxyCreated(ProxyAccessible*, uint32_t)
 {
-  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::ProxyEvent(ProxyAccessible* aTarget, uint32_t aEventType)
+a11y::ProxyDestroyed(ProxyAccessible*)
 {
-  SessionAccessibility* sessionAcc =
-    SessionAccessibility::GetInstanceFor(aTarget);
-  if (!sessionAcc) {
-    return;
-  }
-
-  switch (aEventType) {
-    case nsIAccessibleEvent::EVENT_FOCUS:
-      sessionAcc->SendFocusEvent(WrapperFor(aTarget));
-      break;
-  }
 }
 
 void
-a11y::ProxyStateChangeEvent(ProxyAccessible* aTarget,
-                            uint64_t aState,
-                            bool aEnabled)
+a11y::ProxyEvent(ProxyAccessible*, uint32_t)
 {
-  SessionAccessibility* sessionAcc =
-    SessionAccessibility::GetInstanceFor(aTarget);
-
-  if (!sessionAcc) {
-    return;
-  }
+}
 
-  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::ProxyStateChangeEvent(ProxyAccessible*, uint64_t, bool)
+{
 }
 
 void
 a11y::ProxyCaretMoveEvent(ProxyAccessible* aTarget, int32_t aOffset)
 {
-  SessionAccessibility* sessionAcc =
-    SessionAccessibility::GetInstanceFor(aTarget);
-
-  if (sessionAcc) {
-    sessionAcc->SendTextSelectionChangedEvent(WrapperFor(aTarget), aOffset);
-  }
 }
 
 void
-a11y::ProxyTextChangeEvent(ProxyAccessible* aTarget,
-                           const nsString& aStr,
-                           int32_t aStart,
-                           uint32_t aLen,
-                           bool aIsInsert,
-                           bool aFromUser)
+a11y::ProxyTextChangeEvent(ProxyAccessible*,
+                           const nsString&,
+                           int32_t,
+                           uint32_t,
+                           bool,
+                           bool)
 {
-  SessionAccessibility* sessionAcc =
-    SessionAccessibility::GetInstanceFor(aTarget);
-
-  if (sessionAcc) {
-    sessionAcc->SendTextChangedEvent(
-      WrapperFor(aTarget), aStr, aStart, aLen, aIsInsert, aFromUser);
-  }
 }
 
 void
-a11y::ProxyShowHideEvent(ProxyAccessible* aTarget,
-                         ProxyAccessible* aParent,
-                         bool aInsert,
-                         bool aFromUser)
+a11y::ProxyShowHideEvent(ProxyAccessible*, ProxyAccessible*, bool, bool)
 {
-  SessionAccessibility* sessionAcc =
-    SessionAccessibility::GetInstanceFor(aTarget);
-  if (sessionAcc) {
-    sessionAcc->SendWindowContentChangedEvent(WrapperFor(aParent));
-  }
 }
 
 void
 a11y::ProxySelectionEvent(ProxyAccessible*, ProxyAccessible*, uint32_t)
 {
 }
 
 void
-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)
+a11y::ProxyVirtualCursorChangeEvent(ProxyAccessible*,
+                                    ProxyAccessible*,
+                                    int32_t,
+                                    int32_t,
+                                    ProxyAccessible*,
+                                    int32_t,
+                                    int32_t,
+                                    int16_t,
+                                    int16_t,
+                                    bool)
 {
-  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* aTarget,
-                          uint32_t aEventType,
-                          uint32_t aScrollX,
-                          uint32_t aScrollY,
-                          uint32_t aMaxScrollX,
-                          uint32_t aMaxScrollY)
+a11y::ProxyScrollingEvent(ProxyAccessible*,
+                          uint32_t,
+                          uint32_t,
+                          uint32_t,
+                          uint32_t,
+                          uint32_t)
 {
-  if (aEventType == nsIAccessibleEvent::EVENT_SCROLLING) {
-    SessionAccessibility* sessionAcc =
-      SessionAccessibility::GetInstanceFor(aTarget);
-
-    if (sessionAcc) {
-      sessionAcc->SendScrollingEvent(
-        WrapperFor(aTarget), aScrollX, aScrollY, aMaxScrollX, aMaxScrollY);
-    }
-  }
 }
deleted file mode 100644
--- a/accessible/android/ProxyAccessibleWrap.cpp
+++ /dev/null
@@ -1,147 +0,0 @@
-/* -*- 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);
-}
deleted file mode 100644
--- a/accessible/android/ProxyAccessibleWrap.h
+++ /dev/null
@@ -1,123 +0,0 @@
-/* -*- 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
deleted file mode 100644
--- a/accessible/android/RootAccessibleWrap.cpp
+++ /dev/null
@@ -1,92 +0,0 @@
-/* -*- 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,32 +6,14 @@
 #ifndef mozilla_a11y_RootAccessibleWrap_h__
 #define mozilla_a11y_RootAccessibleWrap_h__
 
 #include "RootAccessible.h"
 
 namespace mozilla {
 namespace a11y {
 
-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);
-};
+typedef RootAccessible RootAccessibleWrap;
 
 } // namespace a11y
 } // namespace mozilla
 
 #endif
--- a/accessible/android/SessionAccessibility.cpp
+++ b/accessible/android/SessionAccessibility.cpp
@@ -1,22 +1,16 @@
 /* -*- 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 {                                                                         \
@@ -24,30 +18,16 @@
 #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,
@@ -56,264 +36,8 @@ 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,51 +2,21 @@
 /* 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,
@@ -63,59 +33,26 @@ 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();
 
-  // 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)
+  NS_INLINE_DECL_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,20 +6,17 @@
 
 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.
    */
-  virtual void ScrollTo(uint32_t aHow) const;
+  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,24 +163,23 @@ 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), mIsSelection(false)
+    mIsHyperLink(false), mIsHyperText(false)
   {}
 
 protected:
   void SetParent(Derived* aParent);
 
 private:
   uintptr_t mParent;
   static const uintptr_t kNoParent = UINTPTR_MAX;
@@ -200,17 +199,16 @@ 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,16 +52,21 @@ 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);
     }
@@ -82,30 +87,35 @@ 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");
@@ -140,16 +150,48 @@ 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();
         }
@@ -172,18 +214,21 @@ 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.CURSOR_TO_FOCUSED:
-        this.autoMove({ moveToFocused: true });
+      case GECKOVIEW_MESSAGE.VIEW_FOCUSED:
+        this._focused = data.gainFocus;
+        if (this._focused) {
+          this.autoMove({ forcePresent: true, noOpIfOnScreen: 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:
@@ -231,16 +276,20 @@ 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,16 +9,18 @@ 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;
@@ -148,16 +150,19 @@ 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);
@@ -218,28 +223,39 @@ 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;
       }
@@ -378,16 +394,25 @@ 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);
@@ -426,37 +451,48 @@ 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.
+   * Move cursor and/or present its location.
    * 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;
       }
 
@@ -468,24 +504,29 @@ 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);
       }
 
-      this.sendToChild(vc, {
+      let sentToChild = 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,73 +5,125 @@
 "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" &&
@@ -99,47 +151,358 @@ 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:
       {
-        // XXX: Port to Android
+        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));
+          }
+        }
         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:
       {
-        // XXX: Port to Android
+        this._handleShow(aEvent);
         break;
       }
       case Events.HIDE:
       {
-        // XXX: Port to Android
+        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);
+        }
+        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:
       {
-        // XXX: Port to Android
-        break;
+        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 });
       }
     }
   },
 
-  QueryInterface: ChromeUtils.generateQI([Ci.nsISupportsWeakReference, Ci.nsIObserver])
+  _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])
 };
 
 const AccessibilityEventObserver = {
 
   /**
    * A WeakMap containing [content, EventManager] pairs.
    */
   eventManagers: new WeakMap(),
new file mode 100644
--- /dev/null
+++ b/accessible/jsat/OutputGenerator.jsm
@@ -0,0 +1,824 @@
+/* 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;
+    }
+};
new file mode 100644
--- /dev/null
+++ b/accessible/jsat/Presentation.jsm
@@ -0,0 +1,332 @@
+/* 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,13 +4,15 @@
 # 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,33 +5,24 @@ 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 = true
+skip-if = (verify && !debug && (os == 'linux'))
 [test_text_editing.html]
-skip-if = true
+skip-if = (verify && !debug && (os == 'linux'))
 [test_text_navigation_focus.html]
-skip-if = true
+skip-if = (verify && !debug && (os == 'linux'))
 [test_text_navigation.html]
-skip-if = true
 [test_traversal.html]
 [test_traversal_helper.html]
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -1742,17 +1742,17 @@ public class BrowserApp extends GeckoApp
                         !IntentUtils.getIsInAutomationFromEnvironment(new SafeIntent(getIntent()))) {
                     // TODO: Better scheduling of DLC actions (Bug 1257492)
                     DlcSyncService.enqueueServiceWork(this);
                 }
 
                 break;
 
             case "GeckoView:AccessibilityEnabled":
-                mDynamicToolbar.setAccessibilityEnabled(message.getBoolean("touchEnabled"));
+                mDynamicToolbar.setAccessibilityEnabled(message.getBoolean("enabled"));
                 break;
 
             case "Menu:Open":
                 if (mBrowserToolbar.isEditing()) {
                     mBrowserToolbar.cancelEdit();
                 }
                 openOptionsMenu();
                 break;
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -481,17 +481,17 @@ var BrowserApp = {
       // Make sure the "Open in App" context menu item appears at the bottom of the list
       this.initContextMenu();
       ExternalApps.init();
     }, NativeWindow, "contextmenus");
 
     if (AppConstants.ACCESSIBILITY) {
       InitLater(() => GlobalEventDispatcher.dispatch("GeckoView:AccessibilityReady"));
       GlobalEventDispatcher.registerListener((aEvent, aData, aCallback) => {
-        if (aData.touchEnabled) {
+        if (aData.enabled) {
           AccessFu.enable();
         } else {
           AccessFu.disable();
         }
       }, "GeckoView:AccessibilitySettings");
     }
 
     InitLater(() => {
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
@@ -1,23 +1,23 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
  * Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 package org.mozilla.geckoview.test
 
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
+import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.ReuseSession
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDisplay
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.WithDevToolsAPI
 
 import android.graphics.Rect
 
 import android.os.Build
 import android.os.Bundle
-import android.os.SystemClock
 
 import android.support.test.filters.MediumTest
 import android.support.test.InstrumentationRegistry
 import android.support.test.runner.AndroidJUnit4
 import android.text.InputType
 import android.util.SparseLongArray
 
 import android.view.accessibility.AccessibilityNodeInfo
@@ -92,17 +92,16 @@ class AccessibilityTest : BaseSessionTes
         fun onClicked(event: AccessibilityEvent) { }
         fun onFocused(event: AccessibilityEvent) { }
         fun onSelected(event: AccessibilityEvent) { }
         fun onScrolled(event: AccessibilityEvent) { }
         fun onTextSelectionChanged(event: AccessibilityEvent) { }
         fun onTextChanged(event: AccessibilityEvent) { }
         fun onTextTraversal(event: AccessibilityEvent) { }
         fun onWinContentChanged(event: AccessibilityEvent) { }
-        fun onWinStateChanged(event: AccessibilityEvent) { }
     }
 
     @Before fun setup() {
         // We initialize a view with a parent and grandparent so that the
         // accessibility events propagate up at least to the parent.
         view = FrameLayout(InstrumentationRegistry.getTargetContext())
         FrameLayout(InstrumentationRegistry.getTargetContext()).addView(view)
         FrameLayout(InstrumentationRegistry.getTargetContext()).addView(view.parent as View)
@@ -122,46 +121,35 @@ class AccessibilityTest : BaseSessionTes
                     AccessibilityEvent.TYPE_VIEW_CLICKED -> newDelegate.onClicked(event)
                     AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED -> newDelegate.onAccessibilityFocused(event)
                     AccessibilityEvent.TYPE_VIEW_SELECTED -> newDelegate.onSelected(event)
                     AccessibilityEvent.TYPE_VIEW_SCROLLED -> newDelegate.onScrolled(event)
                     AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED -> newDelegate.onTextSelectionChanged(event)
                     AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED -> newDelegate.onTextChanged(event)
                     AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY -> newDelegate.onTextTraversal(event)
                     AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> newDelegate.onWinContentChanged(event)
-                    AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> newDelegate.onWinStateChanged(event)
                     else -> {}
                 }
                 return false
             }
         }) },
         { (view.parent as View).setAccessibilityDelegate(null) },
         object : EventDelegate { })
     }
 
     @After fun teardown() {
         sessionRule.session.accessibility.view = null
         nodeInfos.forEach { node -> node.recycle() }
     }
 
-    private fun waitForInitialFocus(moveToFirstChild: Boolean = false) {
-        // XXX: Sometimes we get the window state change of the initial
-        // about:blank page loading. Need to figure out how to ignore that.
+    private fun waitForInitialFocus() {
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1)
             override fun onFocused(event: AccessibilityEvent) { }
-
-            @AssertCalled
-            override fun onWinStateChanged(event: AccessibilityEvent) { }
         })
-
-        if (moveToFirstChild) {
-            provider.performAction(View.NO_ID,
-                AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
-        }
     }
 
     @Test fun testRootNode() {
         assertThat("provider is not null", provider, notNullValue())
         val node = createNodeInfo(AccessibilityNodeProvider.HOST_VIEW_ID)
         assertThat("Root node should have WebView class name",
             node.className.toString(), equalTo("android.webkit.WebView"))
     }
@@ -173,53 +161,52 @@ class AccessibilityTest : BaseSessionTes
             @AssertCalled(count = 1)
             override fun onFocused(event: AccessibilityEvent) { }
         })
     }
 
     @Test fun testAccessibilityFocus() {
         var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
         sessionRule.session.loadTestPath(INPUTS_PATH)
-        waitForInitialFocus(true)
+        waitForInitialFocus()
+
+        provider.performAction(AccessibilityNodeProvider.HOST_VIEW_ID,
+            AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null)
 
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1)
             override fun onAccessibilityFocused(event: AccessibilityEvent) {
                 nodeId = getSourceId(event)
                 val node = createNodeInfo(nodeId)
-                assertThat("Label accessibility focused", node.className.toString(),
-                        equalTo("android.view.View"))
                 assertThat("Text node should not be focusable", node.isFocusable, equalTo(false))
             }
         })
 
         provider.performAction(nodeId,
             AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
 
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1)
             override fun onAccessibilityFocused(event: AccessibilityEvent) {
                 nodeId = getSourceId(event)
                 val node = createNodeInfo(nodeId)
-                assertThat("Editbox accessibility focused", node.className.toString(),
-                        equalTo("android.widget.EditText"))
                 assertThat("Entry node should be focusable", node.isFocusable, equalTo(true))
             }
         })
     }
 
     @Test fun testTextEntryNode() {
         sessionRule.session.loadString("<input aria-label='Name' value='Tobias'>", "text/html")
         waitForInitialFocus()
 
         mainSession.evaluateJS("$('input').focus()")
 
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1)
-            override fun onFocused(event: AccessibilityEvent) {
+            override fun onAccessibilityFocused(event: AccessibilityEvent) {
                 val nodeId = getSourceId(event)
                 val node = createNodeInfo(nodeId)
                 assertThat("Focused EditBox", node.className.toString(),
                         equalTo("android.widget.EditText"))
                 if (Build.VERSION.SDK_INT >= 19) {
                     assertThat("Hint has field name",
                             node.extras.getString("AccessibilityNodeInfo.hint"),
                             equalTo("Name"))
@@ -292,17 +279,17 @@ class AccessibilityTest : BaseSessionTes
         var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID;
         sessionRule.session.loadString("<input value='hello cruel world' id='input'>", "text/html")
         waitForInitialFocus()
 
         mainSession.evaluateJS("$('input').focus()")
 
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1)
-            override fun onFocused(event: AccessibilityEvent) {
+            override fun onAccessibilityFocused(event: AccessibilityEvent) {
                 nodeId = getSourceId(event)
                 val node = createNodeInfo(nodeId)
                 assertThat("Focused EditBox", node.className.toString(),
                         equalTo("android.widget.EditText"))
             }
 
             @AssertCalled(count = 1)
             override fun onTextSelectionChanged(event: AccessibilityEvent) {
@@ -337,17 +324,20 @@ class AccessibilityTest : BaseSessionTes
                 assertThat("text should be pasted", event.text[0].toString(), equalTo("hello cruel cruel cruel"))
             }
         })
     }
 
     @Test fun testMoveByCharacter() {
         var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
         sessionRule.session.loadTestPath(LOREM_IPSUM_HTML_PATH)
-        waitForInitialFocus(true)
+        waitForInitialFocus()
+
+        provider.performAction(AccessibilityNodeProvider.HOST_VIEW_ID,
+                AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null)
 
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1)
             override fun onAccessibilityFocused(event: AccessibilityEvent) {
                 nodeId = getSourceId(event)
                 val node = createNodeInfo(nodeId)
                 assertThat("Accessibility focus on first paragraph", node.text as String, startsWith("Lorem ipsum"))
             }
@@ -367,17 +357,20 @@ class AccessibilityTest : BaseSessionTes
                 AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
                 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER))
         waitUntilTextTraversed(0, 1) // "L"
     }
 
     @Test fun testMoveByWord() {
         var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
         sessionRule.session.loadTestPath(LOREM_IPSUM_HTML_PATH)
-        waitForInitialFocus(true)
+        waitForInitialFocus()
+
+        provider.performAction(AccessibilityNodeProvider.HOST_VIEW_ID,
+                AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null)
 
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1)
             override fun onAccessibilityFocused(event: AccessibilityEvent) {
                 nodeId = getSourceId(event)
                 val node = createNodeInfo(nodeId)
                 assertThat("Accessibility focus on first paragraph", node.text as String, startsWith("Lorem ipsum"))
             }
@@ -397,17 +390,17 @@ class AccessibilityTest : BaseSessionTes
                 AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
                 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD))
         waitUntilTextTraversed(0, 5) // "Lorem"
     }
 
     @Test fun testMoveByLine() {
         var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
         sessionRule.session.loadTestPath(LOREM_IPSUM_HTML_PATH)
-        waitForInitialFocus(true)
+        waitForInitialFocus()
 
         provider.performAction(AccessibilityNodeProvider.HOST_VIEW_ID,
                 AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null)
 
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1)
             override fun onAccessibilityFocused(event: AccessibilityEvent) {
                 nodeId = getSourceId(event)
@@ -429,57 +422,58 @@ class AccessibilityTest : BaseSessionTes
         provider.performAction(nodeId,
                 AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
                 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE))
         waitUntilTextTraversed(0, 18) // "Lorem ipsum dolor "
     }
 
     @Test fun testCheckbox() {
         var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID;
-        sessionRule.session.loadString("<label><input type='checkbox'>many option</label>", "text/html")
-        waitForInitialFocus(true)
+        sessionRule.session.loadString("<label><input id='checkbox' type='checkbox'>many option</label>", "text/html")
+        waitForInitialFocus()
 
+        mainSession.evaluateJS("$('#checkbox').focus()")
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1)
             override fun onAccessibilityFocused(event: AccessibilityEvent) {
                 nodeId = getSourceId(event)
                 var node = createNodeInfo(nodeId)
                 assertThat("Checkbox node is checkable", node.isCheckable, equalTo(true))
                 assertThat("Checkbox node is clickable", node.isClickable, equalTo(true))
                 assertThat("Checkbox node is focusable", node.isFocusable, equalTo(true))
                 assertThat("Checkbox node is not checked", node.isChecked, equalTo(false))
-                assertThat("Checkbox node has correct role", node.text.toString(), equalTo("many option"))
+                assertThat("Checkbox node has correct role", node.text.toString(), equalTo("many option check button"))
             }
         })
 
         provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CLICK, null)
         waitUntilClick(true)
 
         provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CLICK, null)
         waitUntilClick(false)
     }
 
     @Test fun testSelectable() {
         var nodeId = View.NO_ID
         sessionRule.session.loadString(
                 """<ul style="list-style-type: none;" role="listbox">
                         <li id="li" role="option" onclick="this.setAttribute('aria-selected',
                             this.getAttribute('aria-selected') == 'true' ? 'false' : 'true')">1</li>
-                        <li role="option" aria-selected="false">2</li>
                 </ul>""","text/html")
-        waitForInitialFocus(true)
+        waitForInitialFocus()
 
+        provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null)
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1)
             override fun onAccessibilityFocused(event: AccessibilityEvent) {
                 nodeId = getSourceId(event)
                 var node = createNodeInfo(nodeId)
                 assertThat("Selectable node is clickable", node.isClickable, equalTo(true))
                 assertThat("Selectable node is not selected", node.isSelected, equalTo(false))
-                assertThat("Selectable node has correct text", node.text.toString(), equalTo("1"))
+                assertThat("Selectable node has correct role", node.text.toString(), equalTo("1 option list box"))
             }
         })
 
         provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CLICK, null)
         waitUntilSelect(true)
 
         provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CLICK, null)
         waitUntilSelect(false)
@@ -503,104 +497,111 @@ class AccessibilityTest : BaseSessionTes
         sessionRule.session.loadString(
                 """<body style="margin: 0;">
                         <div style="height: 100vh;"></div>
                         <button>Hello</button>
                         <p style="margin: 0;">Lorem ipsum dolor sit amet, consectetur adipiscing elit,
                             sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
                 </body>""",
                 "text/html")
+        sessionRule.waitForPageStop()
 
         sessionRule.waitUntilCalled(object : EventDelegate {
-            @AssertCalled
-            override fun onWinStateChanged(event: AccessibilityEvent) { }
-
             @AssertCalled(count = 1)
             override fun onFocused(event: AccessibilityEvent) {
                 nodeId = getSourceId(event)
                 var node = createNodeInfo(nodeId)
                 var nodeBounds = Rect()
                 node.getBoundsInParent(nodeBounds)
                 assertThat("Default root node bounds are correct", nodeBounds, equalTo(screenRect))
             }
         })
 
-        provider.performAction(View.NO_ID, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
+        provider.performAction(View.NO_ID, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null)
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1, order = [1])
             override fun onAccessibilityFocused(event: AccessibilityEvent) {
                 nodeId = getSourceId(event)
                 assertThat("Focused node is onscreen", screenContainsNode(nodeId), equalTo(true))
             }
 
             @AssertCalled(count = 1, order = [2])
             override fun onScrolled(event: AccessibilityEvent) {
                 assertThat("View is scrolled for focused node to be onscreen", event.scrollY, greaterThan(0))
                 assertThat("View is not scrolled to the end", event.scrollY, lessThan(event.maxScrollY))
             }
 
             @AssertCalled(count = 1, order = [3])
             override fun onWinContentChanged(event: AccessibilityEvent) {
+                nodeId = getSourceId(event)
                 assertThat("Focused node is onscreen", screenContainsNode(nodeId), equalTo(true))
             }
         })
 
-        SystemClock.sleep(100);
         provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_SCROLL_FORWARD, null)
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1, order = [1])
             override fun onScrolled(event: AccessibilityEvent) {
-                assertThat("View is scrolled to the end", event.scrollY.toDouble(), closeTo(event.maxScrollY.toDouble(), 1.0))
+                assertThat("View is scrolled to the end", event.scrollY, equalTo(event.maxScrollY))
             }
 
             @AssertCalled(count = 1, order = [2])
             override fun onWinContentChanged(event: AccessibilityEvent) {
+                nodeId = getSourceId(event)
                 assertThat("Focused node is still onscreen", screenContainsNode(nodeId), equalTo(true))
             }
         })
 
-        SystemClock.sleep(100)
         provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD, null)
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1, order = [1])
             override fun onScrolled(event: AccessibilityEvent) {
                 assertThat("View is scrolled to the beginning", event.scrollY, equalTo(0))
             }
 
             @AssertCalled(count = 1, order = [2])
             override fun onWinContentChanged(event: AccessibilityEvent) {
+                nodeId = getSourceId(event)
                 assertThat("Focused node is offscreen", screenContainsNode(nodeId), equalTo(false))
             }
         })
 
-        SystemClock.sleep(100)
         provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, null)
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1, order = [1])
             override fun onAccessibilityFocused(event: AccessibilityEvent) {
                 nodeId = getSourceId(event)
                 assertThat("Focused node is onscreen", screenContainsNode(nodeId), equalTo(true))
             }
 
             @AssertCalled(count = 1, order = [2])
             override fun onScrolled(event: AccessibilityEvent) {
-                assertThat("View is scrolled to the end", event.scrollY.toDouble(), closeTo(event.maxScrollY.toDouble(), 1.0))
+                assertThat("View is scrolled to the end", event.scrollY, equalTo(event.maxScrollY))
             }
 
             @AssertCalled(count = 1, order = [3])
             override fun onWinContentChanged(event: AccessibilityEvent) {
+                nodeId = getSourceId(event)
                 assertThat("Focused node is onscreen", screenContainsNode(nodeId), equalTo(true))
             }
         })
     }
 
+    @ReuseSession(false) // XXX automation crash fix (bug 1485107)
+    @WithDevToolsAPI
     @Test fun autoFill() {
         // Wait for the accessibility nodes to populate.
         mainSession.loadTestPath(FORMS_HTML_PATH)
-        waitForInitialFocus()
+        sessionRule.waitUntilCalled(object : EventDelegate {
+            // For the root document and the iframe document, each has a form group and
+            // a group for inputs outside of forms, so the total count is 4.
+            @AssertCalled(count = 4)
+            override fun onWinContentChanged(event: AccessibilityEvent) {
+            }
+        })
 
         val autoFills = mapOf(
                 "#user1" to "bar", "#pass1" to "baz", "#user2" to "bar", "#pass2" to "baz") +
                 if (Build.VERSION.SDK_INT >= 19) mapOf(
                         "#email1" to "a@b.c", "#number1" to "24", "#tel1" to "42")
                 else mapOf(
                         "#email1" to "bar", "#number1" to "", "#tel1" to "bar")
 
@@ -660,180 +661,74 @@ class AccessibilityTest : BaseSessionTes
         autoFillChild(View.NO_ID, createNodeInfo(View.NO_ID))
 
         // Wait on the promises and check for correct values.
         for ((actual, expected) in promises.map { it.value.asJSList<String>() }) {
             assertThat("Auto-filled value must match", actual, equalTo(expected))
         }
     }
 
+    @ReuseSession(false) // XXX automation crash fix (bug 1485107)
     @Test fun autoFill_navigation() {
         fun countAutoFillNodes(cond: (AccessibilityNodeInfo) -> Boolean =
                                        { it.className == "android.widget.EditText" },
                                id: Int = View.NO_ID): Int {
             val info = createNodeInfo(id)
-            return (if (cond(info) && info.className != "android.webkit.WebView" ) 1 else 0) + (if (info.childCount > 0)
+            return (if (cond(info)) 1 else 0) + (if (info.childCount > 0)
                 (0 until info.childCount).sumBy {
                     countAutoFillNodes(cond, info.getChildId(it))
                 } else 0)
         }
 
         // Wait for the accessibility nodes to populate.
         mainSession.loadTestPath(FORMS_HTML_PATH)
-        waitForInitialFocus()
-
+        sessionRule.waitUntilCalled(object : EventDelegate {
+            @AssertCalled(count = 4)
+            override fun onWinContentChanged(event: AccessibilityEvent) {
+            }
+        })
         assertThat("Initial auto-fill count should match",
                    countAutoFillNodes(), equalTo(14))
         assertThat("Password auto-fill count should match",
                    countAutoFillNodes({ it.isPassword }), equalTo(4))
 
         // Now wait for the nodes to clear.
         mainSession.loadTestPath(HELLO_HTML_PATH)
-        waitForInitialFocus()
+        mainSession.waitForPageStop()
         assertThat("Should not have auto-fill fields",
                    countAutoFillNodes(), equalTo(0))
 
         // Now wait for the nodes to reappear.
         mainSession.goBack()
-        waitForInitialFocus()
+        sessionRule.waitUntilCalled(object : EventDelegate {
+            @AssertCalled(count = 4)
+            override fun onWinContentChanged(event: AccessibilityEvent) {
+            }
+        })
         assertThat("Should have auto-fill fields again",
                    countAutoFillNodes(), equalTo(14))
         assertThat("Should not have focused field",
                    countAutoFillNodes({ it.isFocused }), equalTo(0))
 
         mainSession.evaluateJS("$('#pass1').focus()")
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled
             override fun onFocused(event: AccessibilityEvent) {
             }
         })
         assertThat("Should have one focused field",
                    countAutoFillNodes({ it.isFocused }), equalTo(1))
+        // The focused field, its siblings, and its parent should be visible.
+        assertThat("Should have six visible nodes",
+                   countAutoFillNodes({ node -> node.isVisibleToUser &&
+                           !(Rect().also({ node.getBoundsInScreen(it) }).isEmpty) }),
+                   equalTo(6))
 
         mainSession.evaluateJS("$('#pass1').blur()")
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled
             override fun onFocused(event: AccessibilityEvent) {
             }
         })
         assertThat("Should not have focused field",
                    countAutoFillNodes({ it.isFocused }), equalTo(0))
     }
-
-    @Test fun testTree() {
-        sessionRule.session.loadString(
-                "<label for='name'>Name:</label><input id='name' type='text' value='Julie'><button>Submit</button>",
-                "text/html")
-        waitForInitialFocus()
-
-        val rootNode = createNodeInfo(View.NO_ID)
-        assertThat("Document has 3 children", rootNode.childCount, equalTo(3))
-
-        val labelNode = createNodeInfo(rootNode.getChildId(0))
-        assertThat("First node is a label", labelNode.className.toString(), equalTo("android.view.View"))
-        assertThat("Label has text", labelNode.text.toString(), equalTo("Name:"))
-
-        val entryNode = createNodeInfo(rootNode.getChildId(1))
-        assertThat("Second node is an entry", entryNode.className.toString(), equalTo("android.widget.EditText"))
-        assertThat("Entry value is text", entryNode.text.toString(), equalTo("Julie"))
-        if (Build.VERSION.SDK_INT >= 19) {
-            assertThat("Entry hint is label",
-                    entryNode.extras.getString("AccessibilityNodeInfo.hint"),
-                    equalTo("Name:"))
-            assertThat("Entry input type is correct", entryNode.inputType,
-                    equalTo(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT))
-        }
-
-
-        val buttonNode = createNodeInfo(rootNode.getChildId(2))
-        assertThat("Last node is a button", buttonNode.className.toString(), equalTo("android.widget.Button"))
-        assertThat("Button has a single text leaf", buttonNode.childCount, equalTo(1))
-        assertThat("Button has correct text", buttonNode.text.toString(), equalTo("Submit"))
-
-        val textLeaf = createNodeInfo(buttonNode.getChildId(0))
-        assertThat("First node is a label", textLeaf.className.toString(), equalTo("android.view.View"))
-        assertThat("Text leaf has correct text", textLeaf.text.toString(), equalTo("Submit"))
-    }
-
-    @Test fun testCollection() {
-        sessionRule.session.loadString(
-                """<ul>
-                  |  <li>One</li>
-                  |  <li>Two</li>
-                  |</ul>
-                  |<ul>
-                  |  <li>1<ul><li>1.1</li><li>1.2</li></ul></li>
-                  |</ul>
-                """.trimMargin(),
-                "text/html")
-        waitForInitialFocus()
-
-        val rootNode = createNodeInfo(View.NO_ID)
-        assertThat("Document has 2 children", rootNode.childCount, equalTo(2))
-
-        val firstList = createNodeInfo(rootNode.getChildId(0))
-        assertThat("First list has 2 children", firstList.childCount, equalTo(2))
-        assertThat("List is a ListView", firstList.className.toString(), equalTo("android.widget.ListView"))
-        if (Build.VERSION.SDK_INT >= 19) {
-            assertThat("First list should have collectionInfo", firstList.collectionInfo, notNullValue())
-            assertThat("First list has 2 rowCount", firstList.collectionInfo.rowCount, equalTo(2))
-            assertThat("First list should not be hierarchical", firstList.collectionInfo.isHierarchical, equalTo(false))
-        }
-
-        val firstListFirstItem = createNodeInfo(firstList.getChildId(0))
-        if (Build.VERSION.SDK_INT >= 19) {
-            assertThat("Item has collectionItemInfo", firstListFirstItem.collectionItemInfo, notNullValue())
-            assertThat("Item has collectionItemInfo", firstListFirstItem.collectionItemInfo.rowIndex, equalTo(1))
-        }
-
-        val secondList = createNodeInfo(rootNode.getChildId(1))
-        assertThat("Second list has 1 child", secondList.childCount, equalTo(1))
-        if (Build.VERSION.SDK_INT >= 19) {
-            assertThat("Second list should have collectionInfo", secondList.collectionInfo, notNullValue())
-            assertThat("Second list has 2 rowCount", secondList.collectionInfo.rowCount, equalTo(1))
-            assertThat("Second list should be hierarchical", secondList.collectionInfo.isHierarchical, equalTo(true))
-        }
-    }
-
-    @Test fun testRange() {
-        sessionRule.session.loadString(
-                """<input type="range" aria-label="Rating" min="1" max="10" value="4">
-                  |<input type="range" aria-label="Stars" min="1" max="5" step="0.5" value="4.5">
-                  |<input type="range" aria-label="Percent" min="0" max="1" step="0.01" value="0.83">
-                """.trimMargin(),
-                "text/html")
-        waitForInitialFocus()
-
-        val rootNode = createNodeInfo(View.NO_ID)
-        assertThat("Document has 3 children", rootNode.childCount, equalTo(3))
-
-        val firstRange = createNodeInfo(rootNode.getChildId(0))
-        assertThat("Range has right label", firstRange.text.toString(), equalTo("Rating"))
-        assertThat("Range is SeekBar", firstRange.className.toString(), equalTo("android.widget.SeekBar"))
-        if (Build.VERSION.SDK_INT >= 19) {
-            assertThat("'Rating' has rangeInfo", firstRange.rangeInfo, notNullValue())
-            assertThat("'Rating' has correct value", firstRange.rangeInfo.current, equalTo(4f))
-            assertThat("'Rating' has correct max", firstRange.rangeInfo.max, equalTo(10f))
-            assertThat("'Rating' has correct min", firstRange.rangeInfo.min, equalTo(1f))
-            assertThat("'Rating' has correct range type", firstRange.rangeInfo.type, equalTo(AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_INT))
-        }
-
-        val secondRange = createNodeInfo(rootNode.getChildId(1))
-        assertThat("Range has right label", secondRange.text.toString(), equalTo("Stars"))
-        if (Build.VERSION.SDK_INT >= 19) {
-            assertThat("'Rating' has rangeInfo", secondRange.rangeInfo, notNullValue())
-            assertThat("'Rating' has correct value", secondRange.rangeInfo.current, equalTo(4.5f))
-            assertThat("'Rating' has correct max", secondRange.rangeInfo.max, equalTo(5f))
-            assertThat("'Rating' has correct min", secondRange.rangeInfo.min, equalTo(1f))
-            assertThat("'Rating' has correct range type", secondRange.rangeInfo.type, equalTo(AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_FLOAT))
-        }
-
-        val thirdRange = createNodeInfo(rootNode.getChildId(2))
-        assertThat("Range has right label", thirdRange.text.toString(), equalTo("Percent"))
-        if (Build.VERSION.SDK_INT >= 19) {
-            assertThat("'Rating' has rangeInfo", thirdRange.rangeInfo, notNullValue())
-            assertThat("'Rating' has correct value", thirdRange.rangeInfo.current, equalTo(0.83f))
-            assertThat("'Rating' has correct max", thirdRange.rangeInfo.max, equalTo(1f))
-            assertThat("'Rating' has correct min", thirdRange.rangeInfo.min, equalTo(0f))
-            assertThat("'Rating' has correct range type", thirdRange.rangeInfo.type, equalTo(AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_PERCENT))
-        }
-    }
 }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoSession.java
@@ -1079,16 +1079,19 @@ public class GeckoSession extends LayerS
         onWindowChanged(WINDOW_CLOSE, /* inProgress */ false);
     }
 
     private void onWindowChanged(int change, boolean inProgress) {
         if ((change == WINDOW_OPEN || change == WINDOW_TRANSFER_IN) && !inProgress) {
             mTextInput.onWindowChanged(mWindow);
         }
         if ((change == WINDOW_CLOSE || change == WINDOW_TRANSFER_OUT) && !inProgress) {
+            if (mAccessibility != null) {
+                mAccessibility.clearAutoFill();
+            }
             mTextInput.clearAutoFill();
         }
     }
 
     /**
      * Get the SessionTextInput instance for this session. May be called on any thread.
      *
      * @return SessionTextInput instance.
@@ -1408,16 +1411,20 @@ public class GeckoSession extends LayerS
      *                false if the session should lose focus.
      *
      * @see #setActive
      */
     public void setFocused(boolean focused) {
         final GeckoBundle msg = new GeckoBundle(1);
         msg.putBoolean("focused", focused);
         mEventDispatcher.dispatch("GeckoView:SetFocused", msg);
+
+        if (focused && mAccessibility != null) {
+            mAccessibility.onWindowFocus();
+        }
     }
 
     /**
      * Class representing a saved session state.
      */
     public static class SessionState implements Parcelable {
         private String mState;
 
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
@@ -1,126 +1,182 @@
 /* -*- Mode: Java; 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/. */
 
 package org.mozilla.geckoview;
 
-import org.mozilla.gecko.GeckoThread;
 import org.mozilla.gecko.annotation.WrapForJNI;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.PrefsHelper;
+import org.mozilla.gecko.util.BundleEventListener;
+import org.mozilla.gecko.util.EventCallback;
 import org.mozilla.gecko.util.GeckoBundle;
-import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.mozglue.JNIObject;
 
 import android.content.Context;
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.os.Build;
 import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
 import android.text.InputType;
+import android.util.Log;
+import android.util.SparseArray;
 import android.view.InputDevice;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewParent;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
-import android.view.accessibility.AccessibilityNodeInfo.RangeInfo;
-import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo;
-import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo;
 import android.view.accessibility.AccessibilityNodeProvider;
 
 public class SessionAccessibility {
     private static final String LOGTAG = "GeckoAccessibility";
+    private static final boolean DEBUG = false;
+
+    // This is a special ID we use for nodes that are event sources.
+    // We expose it as a fragment and not an actual child of the View node.
+    private static final int VIRTUAL_CONTENT_ID = -2;
 
     // This is the number BrailleBack uses to start indexing routing keys.
     private static final int BRAILLE_CLICK_BASE_INDEX = -275000000;
+
+    private static final int ACTION_SET_TEXT = 0x200000;
     private static final String ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE =
             "ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE";
 
-    @WrapForJNI static final int FLAG_ACCESSIBILITY_FOCUSED = 0;
-    @WrapForJNI static final int FLAG_CHECKABLE = 1 << 1;
-    @WrapForJNI static final int FLAG_CHECKED = 1 << 2;
-    @WrapForJNI static final int FLAG_CLICKABLE = 1 << 3;
-    @WrapForJNI static final int FLAG_CONTENT_INVALID = 1 << 4;
-    @WrapForJNI static final int FLAG_CONTEXT_CLICKABLE = 1 << 5;
-    @WrapForJNI static final int FLAG_EDITABLE = 1 << 6;
-    @WrapForJNI static final int FLAG_ENABLED = 1 << 7;
-    @WrapForJNI static final int FLAG_FOCUSABLE = 1 << 8;
-    @WrapForJNI static final int FLAG_FOCUSED = 1 << 9;
-    @WrapForJNI static final int FLAG_LONG_CLICKABLE = 1 << 10;
-    @WrapForJNI static final int FLAG_MULTI_LINE = 1 << 11;
-    @WrapForJNI static final int FLAG_PASSWORD = 1 << 12;
-    @WrapForJNI static final int FLAG_SCROLLABLE = 1 << 13;
-    @WrapForJNI static final int FLAG_SELECTED = 1 << 14;
-    @WrapForJNI static final int FLAG_VISIBLE_TO_USER = 1 << 15;
-    @WrapForJNI static final int FLAG_SELECTABLE = 1 << 16;
-
-
     /* package */ final class NodeProvider extends AccessibilityNodeProvider {
         @Override
         public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualDescendantId) {
-            AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(mView, virtualDescendantId);
-            if (mAttached) {
-                populateNodeFromBundle(node, nativeProvider.getNodeInfo(virtualDescendantId));
-            } else {
+            AccessibilityNodeInfo info = getAutoFillNode(virtualDescendantId);
+            if (info != null) {
+                // Try auto-fill nodes first.
+                return info;
+            }
+
+            info = (virtualDescendantId == VIRTUAL_CONTENT_ID && mVirtualContentNode != null)
+                   ? AccessibilityNodeInfo.obtain(mVirtualContentNode)
+                   : AccessibilityNodeInfo.obtain(mView, virtualDescendantId);
+
+            switch (virtualDescendantId) {
+            case View.NO_ID:
+                // This is the parent View node.
+                // We intentionally don't add VIRTUAL_CONTENT_ID
+                // as a child. It is a source for events,
+                // but not a member of the tree you
+                // can get to by traversing down.
                 if (Build.VERSION.SDK_INT < 17 || mView.getDisplay() != null) {
                     // When running junit tests we don't have a display
-                    mView.onInitializeAccessibilityNodeInfo(node);
+                    mView.onInitializeAccessibilityNodeInfo(info);
+                }
+                info.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
+                info.setClassName("android.webkit.WebView"); // TODO: WTF
+
+                if (Build.VERSION.SDK_INT >= 19) {
+                    Bundle bundle = info.getExtras();
+                    bundle.putCharSequence(
+                        "ACTION_ARGUMENT_HTML_ELEMENT_STRING_VALUES",
+                        "ARTICLE,BUTTON,CHECKBOX,COMBOBOX,CONTROL," +
+                        "FOCUSABLE,FRAME,GRAPHIC,H1,H2,H3,H4,H5,H6," +
+                        "HEADING,LANDMARK,LINK,LIST,LIST_ITEM,MAIN," +
+                        "MEDIA,RADIO,SECTION,TABLE,TEXT_FIELD," +
+                        "UNVISITED_LINK,VISITED_LINK");
                 }
-                node.setClassName("android.webkit.WebView");
+                info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
+
+                if (mAutoFillRoots != null) {
+                    // Add auto-fill nodes.
+                    if (DEBUG) {
+                        Log.d(LOGTAG, "Adding roots " + mAutoFillRoots);
+                    }
+                    for (int i = 0; i < mAutoFillRoots.size(); i++) {
+                        info.addChild(mView, mAutoFillRoots.keyAt(i));
+                    }
+                }
+                break;
+            default:
+                info.setParent(mView);
+                info.setSource(mView, virtualDescendantId);
+                info.setVisibleToUser(mView.isShown());
+                info.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
+                info.setEnabled(true);
+                info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
+                info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
+                info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+                info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+                info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
+                info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
+                info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER |
+                                              AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD |
+                                              AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE);
+                break;
             }
-
-            node.setAccessibilityFocused(mAccessibilityFocusedNode == virtualDescendantId);
-            return node;
+            return info;
         }
 
         @Override
-        public boolean performAction(final int virtualViewId, int action, Bundle arguments) {
-            final GeckoBundle data;
+        public boolean performAction(int virtualViewId, int action, Bundle arguments) {
+            if (virtualViewId == View.NO_ID) {
+                return performRootAction(action, arguments);
+            }
+            if (action == AccessibilityNodeInfo.ACTION_SET_TEXT) {
+                final String value = arguments.getString(Build.VERSION.SDK_INT >= 21
+                        ? AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE
+                        : ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE);
+                return performAutoFill(virtualViewId, value);
+            }
+            return performContentAction(action, arguments);
+        }
 
+        private boolean performRootAction(int action, Bundle arguments) {
             switch (action) {
             case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
-                    if (virtualViewId == View.NO_ID) {
-                        sendEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, View.NO_ID, null, null);
-                    } else {
-                        final GeckoBundle nodeInfo = nativeProvider.getNodeInfo(virtualViewId);
-                        final int flags = nodeInfo.getInt("flags");
-                        if ((flags & FLAG_FOCUSED) != 0) {
-                            mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityCursorToFocused", null);
-                        } else {
-                            sendEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, virtualViewId, null, nodeInfo);
-                        }
-                    }
+            case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
+                final GeckoBundle data = new GeckoBundle(1);
+                data.putBoolean("gainFocus", action == AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+                mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityViewFocused", data);
+                return true;
+            }
+
+            return mView.performAccessibilityAction(action, arguments);
+        }
+
+        @SuppressWarnings("fallthrough")
+        private boolean performContentAction(int action, Bundle arguments) {
+            final GeckoBundle data;
+            switch (action) {
+            case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
+                final AccessibilityEvent event = obtainEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, VIRTUAL_CONTENT_ID);
+                ((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
                 return true;
             case AccessibilityNodeInfo.ACTION_CLICK:
                 mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityActivate", null);
-                GeckoBundle nodeInfo = nativeProvider.getNodeInfo(virtualViewId);
-                final int flags = nodeInfo.getInt("flags");
-                if ((flags & (FLAG_SELECTABLE | FLAG_CHECKABLE)) == 0) {
-                    sendEvent(AccessibilityEvent.TYPE_VIEW_CLICKED, virtualViewId, null, nodeInfo);
-                }
                 return true;
             case AccessibilityNodeInfo.ACTION_LONG_CLICK:
                 mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityLongPress", null);
                 return true;
             case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
                 mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityScrollForward", null);
                 return true;
             case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
                 mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityScrollBackward", null);
                 return true;
             case AccessibilityNodeInfo.ACTION_SELECT:
                 mSession.getEventDispatcher().dispatch("GeckoView:AccessibilitySelect", null);
                 return true;
             case AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT:
+                if (mLastItem) {
+                    return false;
+                }
+                // fall-through
             case AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT:
                 if (arguments != null) {
                     data = new GeckoBundle(1);
                     data.putString("rule", arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING));
                 } else {
                     data = null;
                 }
                 mSession.getEventDispatcher().dispatch(action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT ?
@@ -163,243 +219,65 @@ public class SessionAccessibility {
                 return true;
             case AccessibilityNodeInfo.ACTION_CUT:
             case AccessibilityNodeInfo.ACTION_COPY:
             case AccessibilityNodeInfo.ACTION_PASTE:
                 data = new GeckoBundle(1);
                 data.putInt("action", action);
                 mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityClipboard", data);
                 return true;
-            case AccessibilityNodeInfo.ACTION_SET_TEXT:
-                final String value = arguments.getString(Build.VERSION.SDK_INT >= 21
-                        ? AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE
-                        : ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE);
-                if (mAttached) {
-                    nativeProvider.setText(virtualViewId, value);
-                }
-                return true;
             }
 
             return mView.performAccessibilityAction(action, arguments);
         }
-
-        @Override
-        public AccessibilityNodeInfo findFocus(int focus) {
-          if (focus == AccessibilityNodeInfo.FOCUS_ACCESSIBILITY &&
-              mAccessibilityFocusedNode != 0) {
-            return createAccessibilityNodeInfo(mAccessibilityFocusedNode);
-          }
-
-          return super.findFocus(focus);
-        }
-
-        private void populateNodeFromBundle(final AccessibilityNodeInfo node, final GeckoBundle nodeInfo) {
-            if (mView == null || nodeInfo == null) {
-                return;
-            }
-
-            boolean isRoot = nodeInfo.getInt("id") == View.NO_ID;
-            if (isRoot) {
-                if (Build.VERSION.SDK_INT < 17 || mView.getDisplay() != null) {
-                    // When running junit tests we don't have a display
-                    mView.onInitializeAccessibilityNodeInfo(node);
-                }
-                node.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
-                node.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
-            } else {
-                node.setParent(mView, nodeInfo.getInt("parentId", View.NO_ID));
-            }
-
-            final int flags = nodeInfo.getInt("flags");
-
-            // The basics
-            node.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
-            node.setClassName(nodeInfo.getString("className", "android.view.View"));
-            node.setText(nodeInfo.getString("text", ""));
-
-            // Add actions
-            node.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
-            node.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
-            node.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
-            node.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
-            node.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
-            node.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
-            node.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER |
-                    AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD |
-                    AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE |
-                    AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH);
-            if ((flags & FLAG_CLICKABLE) != 0) {
-                node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
-            }
-
-
-            // Set boolean properties
-            node.setAccessibilityFocused((flags & FLAG_ACCESSIBILITY_FOCUSED) != 0);
-            node.setCheckable((flags & FLAG_CHECKABLE) != 0);
-            node.setChecked((flags & FLAG_CHECKED) != 0);
-            node.setClickable((flags & FLAG_CLICKABLE) != 0);
-            node.setEnabled((flags & FLAG_ENABLED) != 0);
-            node.setFocusable((flags & FLAG_FOCUSABLE) != 0);
-            node.setFocused((flags & FLAG_FOCUSED) != 0);
-            node.setLongClickable((flags & FLAG_LONG_CLICKABLE) != 0);
-            node.setPassword((flags & FLAG_PASSWORD) != 0);
-            node.setScrollable((flags & FLAG_SCROLLABLE) != 0);
-            node.setSelected((flags & FLAG_SELECTED) != 0);
-            node.setVisibleToUser((flags & FLAG_VISIBLE_TO_USER) != 0);
-            // Other boolean properties to consider later:
-            // setHeading, setImportantForAccessibility, setScreenReaderFocusable, setShowingHintText, setDismissable
-
-            // Bounds
-            int[] b = nodeInfo.getIntArray("bounds");
-            if (b != null) {
-                final Rect screenBounds = new Rect(b[0], b[1], b[2], b[3]);
-                node.setBoundsInScreen(screenBounds);
-
-                final Matrix matrix = new Matrix();
-                mSession.getClientToScreenMatrix(matrix);
-                final float[] origin = new float[2];
-                matrix.mapPoints(origin);
-                final Rect parentBounds = new Rect(b[0] - (int)origin[0], b[1] - (int)origin[1], b[2], b[3]);
-                node.setBoundsInParent(parentBounds);
-            }
-
-            // Children
-            int[] children = nodeInfo.getIntArray("children");
-            if (children != null) {
-                for (int childId : children) {
-                    node.addChild(mView, childId);
-                }
-            }
-
-            // SDK 18 and above
-            if (Build.VERSION.SDK_INT >= 18) {
-                if ((flags & FLAG_EDITABLE) != 0) {
-                    node.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION);
-                    node.addAction(AccessibilityNodeInfo.ACTION_CUT);
-                    node.addAction(AccessibilityNodeInfo.ACTION_COPY);
-                    node.addAction(AccessibilityNodeInfo.ACTION_PASTE);
-                    node.setEditable(true);
-                }
-            }
-
-            // SDK 19 and above
-            if (Build.VERSION.SDK_INT >= 19) {
-                node.setMultiLine((flags & FLAG_MULTI_LINE) != 0);
-                node.setContentInvalid((flags & FLAG_CONTENT_INVALID) != 0);
-
-                // Set bundle keys like role and hint
-                Bundle bundle = node.getExtras();
-                if (nodeInfo.containsKey("hint")) {
-                    final String hint =  nodeInfo.getString("hint");
-                    bundle.putCharSequence("AccessibilityNodeInfo.hint", hint);
-                    if (Build.VERSION.SDK_INT >= 26) {
-                        node.setHintText(hint);
-                    }
-                }
-                if (nodeInfo.containsKey("geckoRole")) {
-                    bundle.putCharSequence("AccessibilityNodeInfo.geckoRole", nodeInfo.getString("geckoRole"));
-                }
-                if (nodeInfo.containsKey("roleDescription")) {
-                    bundle.putCharSequence("AccessibilityNodeInfo.roleDescription", nodeInfo.getString("roleDescription"));
-                }
-                if (isRoot) {
-                    // Argument values for ACTION_NEXT_HTML_ELEMENT/ACTION_PREVIOUS_HTML_ELEMENT.
-                    // This is mostly here to let TalkBack know we are a legit "WebView".
-                    bundle.putCharSequence(
-                            "ACTION_ARGUMENT_HTML_ELEMENT_STRING_VALUES",
-                            "ARTICLE,BUTTON,CHECKBOX,COMBOBOX,CONTROL," +
-                                    "FOCUSABLE,FRAME,GRAPHIC,H1,H2,H3,H4,H5,H6," +
-                                    "HEADING,LANDMARK,LINK,LIST,LIST_ITEM,MAIN," +
-                                    "MEDIA,RADIO,SECTION,TABLE,TEXT_FIELD," +
-                                    "UNVISITED_LINK,VISITED_LINK");
-                }
-
-
-                // Set RangeInfo
-                GeckoBundle rangeBundle = nodeInfo.getBundle("rangeInfo");
-                if (rangeBundle != null) {
-                    final RangeInfo rangeInfo = RangeInfo.obtain(
-                            rangeBundle.getInt("type"),
-                            (float)rangeBundle.getDouble("min", Float.NEGATIVE_INFINITY),
-                            (float)rangeBundle.getDouble("max", Float.POSITIVE_INFINITY),
-                            (float)rangeBundle.getDouble("current", 0));
-                    node.setRangeInfo(rangeInfo);
-                }
-
-                // Set CollectionItemInfo
-                GeckoBundle collectionItemBundle = nodeInfo.getBundle("collectionItemInfo");
-                if (collectionItemBundle != null) {
-                    final CollectionItemInfo collectionItemInfo = CollectionItemInfo.obtain(
-                            collectionItemBundle.getInt("rowIndex"),
-                            collectionItemBundle.getInt("rowSpan"),
-                            collectionItemBundle.getInt("columnIndex"),
-                            collectionItemBundle.getInt("columnSpan"), false);
-                    node.setCollectionItemInfo(collectionItemInfo);
-                }
-
-                // Set CollectionInfo
-                GeckoBundle collectionBundle = nodeInfo.getBundle("collectionInfo");
-                if (collectionBundle != null) {
-                    final CollectionInfo collectionInfo = CollectionInfo.obtain(
-                            collectionBundle.getInt("rowCount"),
-                            collectionBundle.getInt("columnCount"),
-                            collectionBundle.getBoolean("isHierarchical", false),
-                            collectionBundle.getInt("selectionMode", 0));
-                    node.setCollectionInfo(collectionInfo);
-                }
-
-                // Set inputType
-                switch (nodeInfo.getString("inputType", "").toLowerCase()) {
-                    case "email":
-                        node.setInputType(InputType.TYPE_CLASS_TEXT |
-                                InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS);
-                        break;
-                    case "number":
-                        node.setInputType(InputType.TYPE_CLASS_NUMBER);
-                        break;
-                    case "password":
-                        node.setInputType(InputType.TYPE_CLASS_TEXT |
-                                InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD);
-                        break;
-                    case "tel":
-                        node.setInputType(InputType.TYPE_CLASS_PHONE);
-                        break;
-                    case "text":
-                        node.setInputType(InputType.TYPE_CLASS_TEXT |
-                                InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
-                        break;
-                    case "url":
-                        node.setInputType(InputType.TYPE_CLASS_TEXT |
-                                InputType.TYPE_TEXT_VARIATION_URI);
-                        break;
-                    default:
-                        break;
-                }
-            }
-
-            // SDK 23 and above
-            if (Build.VERSION.SDK_INT >= 23) {
-                node.setContextClickable((flags & FLAG_CONTEXT_CLICKABLE) != 0);
-            }
-        }
-    }
+    };
 
     // Gecko session we are proxying
     /* package */  final GeckoSession mSession;
     // This is the view that delegates accessibility to us. We also sends event through it.
     private View mView;
     // The native portion of the node provider.
     /* package */ final NativeProvider nativeProvider = new NativeProvider();
+    // Have we reached the last item in content?
+    private boolean mLastItem;
+    // Used to store the JSON message and populate the event later in the code path.
+    private AccessibilityNodeInfo mVirtualContentNode;
+    // Auto-fill nodes.
+    private SparseArray<GeckoBundle> mAutoFillNodes;
+    private SparseArray<EventCallback> mAutoFillRoots;
+    private int mAutoFillFocusedId = View.NO_ID;
+    private int mAutoFillFocusedRoot = View.NO_ID;
+
     private boolean mAttached = false;
-    // The current node with accessibility focus
-    private int mAccessibilityFocusedNode = 0;
 
     /* package */ SessionAccessibility(final GeckoSession session) {
         mSession = session;
+
         Settings.updateAccessibilitySettings();
+
+        session.getEventDispatcher().registerUiThreadListener(new BundleEventListener() {
+                @Override
+                public void handleMessage(final String event, final GeckoBundle message,
+                                          final EventCallback callback) {
+                    if ("GeckoView:AccessibilityEvent".equals(event)) {
+                        sendAccessibilityEvent(message);
+                    } else if ("GeckoView:AddAutoFill".equals(event)) {
+                        addAutoFill(message, callback);
+                    } else if ("GeckoView:ClearAutoFill".equals(event)) {
+                        clearAutoFill();
+                    } else if ("GeckoView:OnAutoFillFocus".equals(event)) {
+                        onAutoFillFocus(message);
+                    }
+                }
+            },
+            "GeckoView:AccessibilityEvent",
+            "GeckoView:AddAutoFill",
+            "GeckoView:ClearAutoFill",
+            "GeckoView:OnAutoFillFocus",
+            null);
     }
 
     /**
       * Get the View instance that delegates accessibility to this session.
       *
       * @return View instance.
       */
     public View getView() {
@@ -412,16 +290,17 @@ public class SessionAccessibility {
       * @param view View instance.
       */
     public void setView(final View view) {
         if (mView != null) {
             mView.setAccessibilityDelegate(null);
         }
 
         mView = view;
+        mLastItem = false;
 
         if (mView == null) {
             return;
         }
 
         mView.setAccessibilityDelegate(new View.AccessibilityDelegate() {
             private NodeProvider mProvider;
 
@@ -477,18 +356,16 @@ public class SessionAccessibility {
                         sForceEnabled = value < 0;
                         dispatch();
                     }
                 }
             };
             PrefsHelper.addObserver(new String[]{ FORCE_ACCESSIBILITY_PREF }, prefHandler);
         }
 
-        public static boolean isPlatformEnabled() { return sEnabled; }
-
         public static boolean isEnabled() {
             return sEnabled || sForceEnabled;
         }
 
         public static boolean isTouchExplorationEnabled() {
             return sTouchExplorationEnabled || sForceEnabled;
         }
 
@@ -496,35 +373,181 @@ public class SessionAccessibility {
             final AccessibilityManager accessibilityManager = (AccessibilityManager)
                     GeckoAppShell.getApplicationContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
             sEnabled = accessibilityManager.isEnabled();
             sTouchExplorationEnabled = sEnabled && accessibilityManager.isTouchExplorationEnabled();
             dispatch();
         }
 
         /* package */ static void dispatch() {
-            final GeckoBundle ret = new GeckoBundle(2);
-            ret.putBoolean("touchEnabled", isTouchExplorationEnabled());
-            ret.putBoolean("enabled", isEnabled());
+            final GeckoBundle ret = new GeckoBundle(1);
+            ret.putBoolean("enabled", isTouchExplorationEnabled());
             // "GeckoView:AccessibilitySettings" is dispatched to the Gecko thread.
             EventDispatcher.getInstance().dispatch("GeckoView:AccessibilitySettings", ret);
             // "GeckoView:AccessibilityEnabled" is dispatched to the UI thread.
             EventDispatcher.getInstance().dispatch("GeckoView:AccessibilityEnabled", ret);
+        }
+    }
 
-            if (GeckoThread.isStateAtLeast(GeckoThread.State.PROFILE_READY)) {
-                toggleNativeAccessibility(isEnabled());
-            } else {
-                GeckoThread.queueNativeCallUntil(
-                        GeckoThread.State.PROFILE_READY,
-                        Settings.class, "toggleNativeAccessibility", isEnabled());
+    private AccessibilityEvent obtainEvent(final int eventType, final int sourceId) {
+        AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+        event.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
+        event.setSource(mView, sourceId);
+
+        return event;
+    }
+
+    private static void populateEventFromJSON(AccessibilityEvent event, final GeckoBundle message) {
+        final String[] textArray = message.getStringArray("text");
+        if (textArray != null) {
+            for (int i = 0; i < textArray.length; i++)
+                event.getText().add(textArray[i] != null ? textArray[i] : "");
+        }
+
+        if (message.containsKey("className"))
+            event.setClassName(message.getString("className"));
+        event.setContentDescription(message.getString("description", ""));
+        event.setEnabled(message.getBoolean("enabled", true));
+        event.setChecked(message.getBoolean("checked"));
+        event.setPassword(message.getBoolean("password"));
+        event.setAddedCount(message.getInt("addedCount", -1));
+        event.setRemovedCount(message.getInt("removedCount", -1));
+        event.setFromIndex(message.getInt("fromIndex", -1));
+        event.setItemCount(message.getInt("itemCount", -1));
+        event.setCurrentItemIndex(message.getInt("currentItemIndex", -1));
+        event.setBeforeText(message.getString("beforeText", ""));
+        event.setToIndex(message.getInt("toIndex", -1));
+        event.setScrollable(message.getBoolean("scrollable"));
+        event.setScrollX(message.getInt("scrollX", -1));
+        event.setScrollY(message.getInt("scrollY", -1));
+        event.setMaxScrollX(message.getInt("maxScrollX", -1));
+        event.setMaxScrollY(message.getInt("maxScrollY", -1));
+    }
+
+    private void populateNodeInfoFromJSON(AccessibilityNodeInfo node, final GeckoBundle message) {
+        node.setEnabled(message.getBoolean("enabled", true));
+        node.setCheckable(message.getBoolean("checkable"));
+        node.setChecked(message.getBoolean("checked"));
+        node.setPassword(message.getBoolean("password"));
+        node.setFocusable(message.getBoolean("focusable"));
+        node.setFocused(message.getBoolean("focused"));
+        node.setSelected(message.getBoolean("selected"));
+
+        node.setClassName(message.getString("className", "android.view.View"));
+
+        final String[] textArray = message.getStringArray("text");
+        StringBuilder sb = new StringBuilder();
+        if (textArray != null && textArray.length > 0) {
+            sb.append(textArray[0] != null ? textArray[0] : "");
+            for (int i = 1; i < textArray.length; i++) {
+                sb.append(' ').append(textArray[i] != null ? textArray[i] : "");
+            }
+            node.setText(sb.toString());
+        }
+        node.setContentDescription(message.getString("description", ""));
+
+        if (Build.VERSION.SDK_INT >= 18 && message.getBoolean("editable")) {
+            node.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION);
+            node.addAction(AccessibilityNodeInfo.ACTION_CUT);
+            node.addAction(AccessibilityNodeInfo.ACTION_COPY);
+            node.addAction(AccessibilityNodeInfo.ACTION_PASTE);
+            node.setEditable(true);
+        }
+
+        if (message.getBoolean("clickable")) {
+            node.setClickable(true);
+            node.addAction(AccessibilityNodeInfo.ACTION_CLICK);
+        }
+
+        if (Build.VERSION.SDK_INT >= 19 && message.containsKey("hint")) {
+            Bundle bundle = node.getExtras();
+            bundle.putCharSequence("AccessibilityNodeInfo.hint", message.getString("hint"));
+        }
+    }
+
+    private void updateBounds(final AccessibilityNodeInfo node, final GeckoBundle message) {
+        final GeckoBundle bounds = message.getBundle("bounds");
+        if (bounds == null) {
+            return;
+        }
+
+        Rect screenBounds = new Rect(bounds.getInt("left"), bounds.getInt("top"),
+                                     bounds.getInt("right"), bounds.getInt("bottom"));
+        node.setBoundsInScreen(screenBounds);
+
+        final Matrix matrix = new Matrix();
+        final float[] origin = new float[2];
+        mSession.getClientToScreenMatrix(matrix);
+        matrix.mapPoints(origin);
+
+        screenBounds.offset((int) -origin[0], (int) -origin[1]);
+        node.setBoundsInParent(screenBounds);
+    }
+
+    private void updateState(final AccessibilityNodeInfo node, final GeckoBundle message) {
+        if (message.containsKey("checked")) {
+            node.setChecked(message.getBoolean("checked"));
+        }
+        if (message.containsKey("selected")) {
+            node.setSelected(message.getBoolean("selected"));
+        }
+    }
+
+    private void sendAccessibilityEvent(final GeckoBundle message) {
+        if (mView == null || !Settings.isTouchExplorationEnabled())
+            return;
+
+        final int eventType = message.getInt("eventType", -1);
+        if (eventType < 0) {
+            Log.e(LOGTAG, "No accessibility event type provided");
+            return;
+        }
+
+        int eventSource = VIRTUAL_CONTENT_ID;
+
+        if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
+            final String exitView = message.getString("exitView", "");
+
+            mLastItem = exitView.equals("moveNext");
+            if (mLastItem) {
+                return;
+            }
+
+            if (exitView.equals("movePrevious")) {
+                eventSource = View.NO_ID;
             }
         }
 
-        @WrapForJNI(dispatchTo = "gecko")
-        private static native void toggleNativeAccessibility(boolean enable);
+        if (eventSource != View.NO_ID &&
+                (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED ||
+                 eventType == AccessibilityEvent.TYPE_VIEW_FOCUSED ||
+                 eventType == AccessibilityEvent.TYPE_VIEW_HOVER_ENTER)) {
+            // In Jelly Bean we populate an AccessibilityNodeInfo with the minimal amount of data to have
+            // it work with TalkBack.
+            if (mVirtualContentNode != null) {
+                mVirtualContentNode.recycle();
+            }
+            mVirtualContentNode = AccessibilityNodeInfo.obtain(mView, eventSource);
+            populateNodeInfoFromJSON(mVirtualContentNode, message);
+        }
+
+        if (mVirtualContentNode != null) {
+            // Bounds for the virtual content can be updated from any event.
+            updateBounds(mVirtualContentNode, message);
+
+            // State for the virtual content can be updated when view is clicked/selected.
+            if (eventType == AccessibilityEvent.TYPE_VIEW_CLICKED ||
+                eventType == AccessibilityEvent.TYPE_VIEW_SELECTED) {
+                updateState(mVirtualContentNode, message);
+            }
+        }
+
+        final AccessibilityEvent accessibilityEvent = obtainEvent(eventType, eventSource);
+        populateEventFromJSON(accessibilityEvent, message);
+        ((ViewParent) mView).requestSendAccessibilityEvent(mView, accessibilityEvent);
     }
 
     public boolean onMotionEvent(final MotionEvent event) {
         if (!Settings.isTouchExplorationEnabled()) {
             return false;
         }
 
         if (event.getSource() != InputDevice.SOURCE_TOUCHSCREEN) {
@@ -539,86 +562,260 @@ public class SessionAccessibility {
         }
 
         final GeckoBundle data = new GeckoBundle(2);
         data.putDoubleArray("coordinates", new double[] {event.getRawX(), event.getRawY()});
         mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityExploreByTouch", data);
         return true;
     }
 
-    /* package */ void sendEvent(final int eventType, final int sourceId, final GeckoBundle eventData, final GeckoBundle sourceInfo) {
-        ThreadUtils.assertOnUiThread();
-        if (mView == null) {
+    /* package */ AccessibilityNodeInfo getAutoFillNode(final int id) {
+        if (mView == null || mAutoFillRoots == null) {
+            return null;
+        }
+
+        final GeckoBundle bundle = mAutoFillNodes.get(id);
+        if (bundle == null) {
+            return null;
+        }
+
+        if (DEBUG) {
+            Log.d(LOGTAG, "getAutoFillNode(" + id + ')');
+        }
+
+        final AccessibilityNodeInfo node = AccessibilityNodeInfo.obtain(mView, id);
+        node.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
+        node.setParent(mView, bundle.getInt("parent", View.NO_ID));
+        node.setEnabled(true);
+
+        if (mAutoFillFocusedRoot != View.NO_ID &&
+                mAutoFillFocusedRoot == bundle.getInt("root", View.NO_ID)) {
+            // Some auto-fill clients require a dummy rect for the focused View.
+            final Rect rect = new Rect();
+            mSession.getSurfaceBounds(rect);
+            node.setVisibleToUser(!rect.isEmpty());
+            node.setBoundsInParent(rect);
+
+            final int[] offset = new int[2];
+            mView.getLocationOnScreen(offset);
+            rect.offset(offset[0], offset[1]);
+            node.setBoundsInScreen(rect);
+        }
+
+        final GeckoBundle[] children = bundle.getBundleArray("children");
+        if (children != null) {
+            for (final GeckoBundle child : children) {
+                final int childId = child.getInt("id");
+                node.addChild(mView, childId);
+                mAutoFillNodes.append(childId, child);
+            }
+        }
+
+        String tag = bundle.getString("tag", "");
+        final String type = bundle.getString("type", "text");
+        final GeckoBundle attrs = bundle.getBundle("attributes");
+
+        if ("INPUT".equals(tag) && !bundle.getBoolean("editable", false)) {
+            tag = ""; // Don't process non-editable inputs (e.g. type="button").
+        }
+        switch (tag) {
+            case "INPUT":
+            case "TEXTAREA": {
+                final boolean disabled = bundle.getBoolean("disabled");
+                node.setClassName("android.widget.EditText");
+                node.setEnabled(!disabled);
+                node.setFocusable(!disabled);
+                node.setFocused(id == mAutoFillFocusedId);
+
+                if ("password".equals(type)) {
+                    node.setPassword(true);
+                }
+                if (Build.VERSION.SDK_INT >= 18) {
+                    node.setEditable(!disabled);
+                }
+                if (Build.VERSION.SDK_INT >= 19) {
+                    node.setMultiLine("TEXTAREA".equals(tag));
+                }
+                if (Build.VERSION.SDK_INT >= 21) {
+                    try {
+                        node.setMaxTextLength(Integer.parseInt(
+                                String.valueOf(attrs.get("maxlength"))));
+                    } catch (final NumberFormatException ignore) {
+                    }
+                }
+
+                if (!disabled) {
+                    if (Build.VERSION.SDK_INT >= 21) {
+                        node.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT);
+                    } else {
+                        node.addAction(ACTION_SET_TEXT);
+                    }
+                }
+                break;
+            }
+            default:
+                if (children != null) {
+                    node.setClassName("android.view.ViewGroup");
+                } else {
+                    node.setClassName("android.view.View");
+                }
+                break;
+        }
+
+        if (Build.VERSION.SDK_INT >= 19 && "INPUT".equals(tag)) {
+            switch (type) {
+                case "email":
+                    node.setInputType(InputType.TYPE_CLASS_TEXT |
+                                      InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS);
+                    break;
+                case "number":
+                    node.setInputType(InputType.TYPE_CLASS_NUMBER);
+                    break;
+                case "password":
+                    node.setInputType(InputType.TYPE_CLASS_TEXT |
+                                      InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD);
+                    break;
+                case "tel":
+                    node.setInputType(InputType.TYPE_CLASS_PHONE);
+                    break;
+                case "text":
+                    node.setInputType(InputType.TYPE_CLASS_TEXT |
+                                      InputType.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
+                    break;
+                case "url":
+                    node.setInputType(InputType.TYPE_CLASS_TEXT |
+                                      InputType.TYPE_TEXT_VARIATION_URI);
+                    break;
+            }
+        }
+        return node;
+    }
+
+    /* package */ boolean performAutoFill(final int id, final String value) {
+        if (mAutoFillRoots == null) {
+            return false;
+        }
+
+        int rootId = id;
+        for (int currentId = id; currentId != View.NO_ID;) {
+            final GeckoBundle bundle = mAutoFillNodes.get(currentId);
+            if (bundle == null) {
+                return false;
+            }
+            rootId = currentId;
+            currentId = bundle.getInt("parent", View.NO_ID);
+        }
+
+        if (DEBUG) {
+            Log.d(LOGTAG, "performAutoFill(" + id + ", " + value + ')');
+        }
+
+        final EventCallback callback = mAutoFillRoots.get(rootId);
+        if (callback == null) {
+            return false;
+        }
+
+        final GeckoBundle response = new GeckoBundle(1);
+        response.putString(String.valueOf(id), value);
+        callback.sendSuccess(response);
+        return true;
+    }
+
+    private void fireWindowChangedEvent(final int id) {
+        if (Settings.isEnabled() && mView instanceof ViewParent) {
+            final AccessibilityEvent event = obtainEvent(
+                    AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED, id);
+            if (Build.VERSION.SDK_INT >= 19) {
+                event.setContentChangeTypes(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
+            }
+            ((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
+        }
+    }
+
+    /* package */ void addAutoFill(final GeckoBundle message, final EventCallback callback) {
+        if (!Settings.isEnabled()) {
             return;
         }
 
-        if (!Settings.isPlatformEnabled() && (Build.VERSION.SDK_INT < 17 || mView.getDisplay() != null)) {
-            // Accessibility could be activated in Gecko via xpcom, for example when using a11y
-            // devtools. Here we assure that either Android a11y is *really* enabled, or no
-            // display is attached and we must be in a junit test.
+        if (mAutoFillRoots == null) {
+            mAutoFillRoots = new SparseArray<>();
+            mAutoFillNodes = new SparseArray<>();
+        }
+
+        final int id = message.getInt("id");
+        if (DEBUG) {
+            Log.d(LOGTAG, "addAutoFill(" + id + ')');
+        }
+
+        mAutoFillRoots.append(id, callback);
+        mAutoFillNodes.append(id, message);
+        fireWindowChangedEvent(id);
+    }
+
+    /* package */ void clearAutoFill() {
+        if (mAutoFillRoots != null) {
+            if (DEBUG) {
+                Log.d(LOGTAG, "clearAutoFill()");
+            }
+            mAutoFillRoots = null;
+            mAutoFillNodes = null;
+            mAutoFillFocusedId = View.NO_ID;
+            mAutoFillFocusedRoot = View.NO_ID;
+            fireWindowChangedEvent(View.NO_ID);
+        }
+    }
+
+    /* package */ void onAutoFillFocus(final GeckoBundle message) {
+        if (!Settings.isEnabled() || !(mView instanceof ViewParent) || mAutoFillNodes == null) {
             return;
         }
-        if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
-            mAccessibilityFocusedNode = sourceId;
-        }
 
-        final AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
-        event.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
-        event.setSource(mView, sourceId);
-        event.setEnabled(true);
-
-        if (sourceInfo != null) {
-            final int flags = sourceInfo.getInt("flags");
-            event.setClassName(sourceInfo.getString("className", "android.view.View"));
-            event.setChecked((flags & FLAG_CHECKED) != 0);
-            event.getText().add(sourceInfo.getString("text", ""));
+        final int id;
+        final int root;
+        if (message != null) {
+            id = message.getInt("id");
+            root = message.getInt("root");
+        } else {
+            id = root = View.NO_ID;
         }
 
-        if (eventData != null) {
-            if (eventData.containsKey("text")) {
-                event.getText().add(eventData.getString("text"));
+        if (DEBUG) {
+            Log.d(LOGTAG, "onAutoFillFocus(" + id + ')');
+        }
+        if (mAutoFillFocusedId == id) {
+            return;
+        }
+        mAutoFillFocusedId = id;
+        mAutoFillFocusedRoot = root;
+
+        // We already send "TYPE_VIEW_FOCUSED" in touch exploration mode,
+        // so in that case don't send it here.
+        if (!Settings.isTouchExplorationEnabled()) {
+            AccessibilityEvent event = obtainEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED, id);
+            ((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
+        }
+    }
+
+    /* package */ void onWindowFocus() {
+        // Auto-fill clients expect a state change event on focus.
+        if (Settings.isEnabled() && mView instanceof ViewParent) {
+            if (DEBUG) {
+                Log.d(LOGTAG, "onWindowFocus()");
             }
-            event.setContentDescription(eventData.getString("description", ""));
-            event.setAddedCount(eventData.getInt("addedCount", -1));
-            event.setRemovedCount(eventData.getInt("removedCount", -1));
-            event.setFromIndex(eventData.getInt("fromIndex", -1));
-            event.setItemCount(eventData.getInt("itemCount", -1));
-            event.setCurrentItemIndex(eventData.getInt("currentItemIndex", -1));
-            event.setBeforeText(eventData.getString("beforeText", ""));
-            event.setToIndex(eventData.getInt("toIndex", -1));
-            event.setScrollX(eventData.getInt("scrollX", -1));
-            event.setScrollY(eventData.getInt("scrollY", -1));
-            event.setMaxScrollX(eventData.getInt("maxScrollX", -1));
-            event.setMaxScrollY(eventData.getInt("maxScrollY", -1));
+            final AccessibilityEvent event = obtainEvent(
+                    AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, View.NO_ID);
+            ((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
         }
-
-        ((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
     }
 
     /* package */ final class NativeProvider extends JNIObject {
         @WrapForJNI(calledFrom = "ui")
         private void setAttached(final boolean attached) {
             mAttached = attached;
         }
 
         @Override // JNIObject
         protected void disposeNative() {
             // Disposal happens in native code.
             throw new UnsupportedOperationException();
         }
-
-        @WrapForJNI(dispatchTo = "current")
-        public native GeckoBundle getNodeInfo(int id);
-
-        @WrapForJNI(dispatchTo = "gecko")
-        public native void setText(int id, String text);
-
-        @WrapForJNI(calledFrom = "gecko", stubName = "SendEvent")
-        private void sendEventNative(final int eventType, final int sourceId, final GeckoBundle eventData, final GeckoBundle sourceInfo) {
-            ThreadUtils.postToUiThread(new Runnable() {
-                @Override
-                public void run() {
-                    sendEvent(eventType, sourceId, eventData, sourceInfo);
-                }
-            });
-        }
     }
 }
--- a/mobile/android/modules/geckoview/GeckoViewAccessibility.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewAccessibility.jsm
@@ -12,16 +12,16 @@ ChromeUtils.import("resource://gre/modul
 XPCOMUtils.defineLazyModuleGetters(this, {
   EventDispatcher: "resource://gre/modules/Messaging.jsm",
   AccessFu: "resource://gre/modules/accessibility/AccessFu.jsm"
 });
 
 class GeckoViewAccessibility extends GeckoViewModule {
   onInit() {
     EventDispatcher.instance.registerListener((aEvent, aData, aCallback) => {
-      if (aData.touchEnabled) {
+      if (aData.enabled) {
         AccessFu.enable();
       } else {
         AccessFu.disable();
       }
     }, "GeckoView:AccessibilitySettings");
   }
 }
deleted file mode 100644
--- a/widget/android/bindings/AccessibilityEvent-classes.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-# We only use constants from KeyEvent
-[android.view.accessibility.AccessibilityEvent = skip:true]
-<field> = skip:false
--- a/widget/android/bindings/moz.build
+++ b/widget/android/bindings/moz.build
@@ -5,17 +5,16 @@
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 with Files("**"):
     BUG_COMPONENT = ("Firefox for Android", "Graphics, Panning and Zooming")
 
 # List of stems to generate .cpp and .h files for.  To add a stem, add it to
 # this list and ensure that $(stem)-classes.txt exists in this directory.
 generated = [
-    'AccessibilityEvent',
     'AndroidBuild',
     'AndroidRect',
     'JavaBuiltins',
     'KeyEvent',
     'MediaCodec',
     'MotionEvent',
     'SurfaceTexture',
     'ViewConfiguration'