Bug 1479037 - Implement native accessible tree 3/4. r=Jamie
authorEitan Isaacson <eitan@monotonous.org>
Thu, 11 Oct 2018 16:21:09 +0000
changeset 499181 97b5d09ed65af9bfa5eb287f4a5c0f7e5e4ae4a6
parent 499180 dab75afa1e1e7e10abb5f7c7931d3fd889778fe8
child 499182 d41905041ca7f615f90fce70e1de3c325e490c44
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)
reviewersJamie
bugs1479037
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1479037 - Implement native accessible tree 3/4. r=Jamie Depends on D6682 Differential Revision: https://phabricator.services.mozilla.com/D6683
accessible/android/AccessibleWrap.cpp
accessible/android/AccessibleWrap.h
accessible/android/ProxyAccessibleWrap.cpp
accessible/android/ProxyAccessibleWrap.h
accessible/android/SessionAccessibility.cpp
accessible/android/SessionAccessibility.h
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/SessionAccessibility.java
mobile/android/modules/geckoview/GeckoViewAccessibility.jsm
--- a/accessible/android/AccessibleWrap.cpp
+++ b/accessible/android/AccessibleWrap.cpp
@@ -1,18 +1,26 @@
 /* -*- 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
@@ -54,8 +62,321 @@ 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);
+  }
+}
+
+mozilla::java::GeckoBundle::LocalRef
+AccessibleWrap::CreateBundle(int32_t aParentID,
+                             role aRole,
+                             uint64_t aState,
+                             const nsString& aName,
+                             const nsString& aTextValue,
+                             const nsString& aDOMNodeID,
+                             const nsIntRect& aBounds,
+                             double aCurVal,
+                             double aMinVal,
+                             double aMaxVal,
+                             double aStep,
+                             nsIPersistentProperties* aAttributes,
+                             const nsTArray<int32_t>& aChildren) const
+{
+  GECKOBUNDLE_START(nodeInfo);
+  GECKOBUNDLE_PUT(nodeInfo, "id", java::sdk::Integer::ValueOf(VirtualViewID()));
+  GECKOBUNDLE_PUT(nodeInfo, "parentId", java::sdk::Integer::ValueOf(aParentID));
+  uint64_t flags = GetFlags(aRole, aState);
+  GECKOBUNDLE_PUT(nodeInfo, "flags", java::sdk::Integer::ValueOf(flags));
+
+  nsAutoString geckoRole;
+  nsAutoString roleDescription;
+  nsAutoString className;
+  GetAndroidRoleAndClass(aRole, geckoRole, roleDescription, className);
+  if (VirtualViewID() == kNoID) {
+    className.AssignLiteral("android.webkit.WebView");
+    roleDescription.AssignLiteral("");
+  }
+  GECKOBUNDLE_PUT(
+    nodeInfo, "roleDescription", jni::StringParam(roleDescription));
+  GECKOBUNDLE_PUT(nodeInfo, "geckoRole", jni::StringParam(geckoRole));
+  GECKOBUNDLE_PUT(nodeInfo, "className", jni::StringParam(className));
+
+  if (!aTextValue.IsEmpty() &&
+      (flags & java::SessionAccessibility::FLAG_EDITABLE)) {
+    GECKOBUNDLE_PUT(nodeInfo, "hint", jni::StringParam(aName));
+    GECKOBUNDLE_PUT(nodeInfo, "text", jni::StringParam(aTextValue));
+  } else {
+    GECKOBUNDLE_PUT(nodeInfo, "text", jni::StringParam(aName));
+  }
+
+  if (!aDOMNodeID.IsEmpty()) {
+    GECKOBUNDLE_PUT(
+      nodeInfo, "viewIdResourceName", jni::StringParam(aDOMNodeID));
+  }
+
+  const int32_t data[4] = {
+    aBounds.x, aBounds.y, aBounds.x + aBounds.width, aBounds.y + aBounds.height
+  };
+  GECKOBUNDLE_PUT(nodeInfo, "bounds", jni::IntArray::New(data, 4));
+
+  if (HasNumericValue()) {
+    GECKOBUNDLE_START(rangeInfo);
+    if (aMaxVal == 1 && aMinVal == 0) {
+      GECKOBUNDLE_PUT(
+        rangeInfo, "type", java::sdk::Integer::ValueOf(2)); // percent
+    } else if (std::round(aStep) != aStep) {
+      GECKOBUNDLE_PUT(
+        rangeInfo, "type", java::sdk::Integer::ValueOf(1)); // float
+    } else {
+      GECKOBUNDLE_PUT(
+        rangeInfo, "type", java::sdk::Integer::ValueOf(0)); // integer
+    }
+
+    if (!IsNaN(aCurVal)) {
+      GECKOBUNDLE_PUT(rangeInfo, "current", java::sdk::Double::New(aCurVal));
+    }
+    if (!IsNaN(aMinVal)) {
+      GECKOBUNDLE_PUT(rangeInfo, "min", java::sdk::Double::New(aMinVal));
+    }
+    if (!IsNaN(aMaxVal)) {
+      GECKOBUNDLE_PUT(rangeInfo, "max", java::sdk::Double::New(aMaxVal));
+    }
+
+    GECKOBUNDLE_FINISH(rangeInfo);
+    GECKOBUNDLE_PUT(nodeInfo, "rangeInfo", rangeInfo);
+  }
+
+  nsString inputType;
+  nsAccUtils::GetAccAttr(aAttributes, nsGkAtoms::textInputType, inputType);
+  if (!inputType.IsEmpty()) {
+    GECKOBUNDLE_PUT(nodeInfo, "inputType", jni::StringParam(inputType));
+  }
+
+  nsString posinset;
+  nsresult rv = aAttributes->GetStringProperty(NS_LITERAL_CSTRING("posinset"), posinset);
+  if (NS_SUCCEEDED(rv)) {
+    int32_t rowIndex;
+    if (sscanf(NS_ConvertUTF16toUTF8(posinset).get(), "%d", &rowIndex) > 0) {
+      GECKOBUNDLE_START(collectionItemInfo);
+      GECKOBUNDLE_PUT(
+        collectionItemInfo, "rowIndex", java::sdk::Integer::ValueOf(rowIndex));
+      GECKOBUNDLE_PUT(
+        collectionItemInfo, "columnIndex", java::sdk::Integer::ValueOf(0));
+      GECKOBUNDLE_PUT(
+        collectionItemInfo, "rowSpan", java::sdk::Integer::ValueOf(1));
+      GECKOBUNDLE_PUT(
+        collectionItemInfo, "columnSpan", java::sdk::Integer::ValueOf(1));
+      GECKOBUNDLE_FINISH(collectionItemInfo);
+
+      GECKOBUNDLE_PUT(nodeInfo, "collectionItemInfo", collectionItemInfo);
+    }
+  }
+
+  nsString colSize;
+  rv = aAttributes->GetStringProperty(NS_LITERAL_CSTRING("child-item-count"),
+                                      colSize);
+  if (NS_SUCCEEDED(rv)) {
+    int32_t rowCount;
+    if (sscanf(NS_ConvertUTF16toUTF8(colSize).get(), "%d", &rowCount) > 0) {
+      GECKOBUNDLE_START(collectionInfo);
+      GECKOBUNDLE_PUT(
+        collectionInfo, "rowCount", java::sdk::Integer::ValueOf(rowCount));
+      GECKOBUNDLE_PUT(
+        collectionInfo, "columnCount", java::sdk::Integer::ValueOf(1));
+
+      nsString unused;
+      rv = aAttributes->GetStringProperty(NS_LITERAL_CSTRING("hierarchical"),
+                                          unused);
+      if (NS_SUCCEEDED(rv)) {
+        GECKOBUNDLE_PUT(
+          collectionInfo, "isHierarchical", java::sdk::Boolean::TRUE());
+      }
+
+      if (IsSelect()) {
+        int32_t selectionMode = (aState & states::MULTISELECTABLE) ? 2 : 1;
+        GECKOBUNDLE_PUT(collectionInfo,
+                        "selectionMode",
+                        java::sdk::Integer::ValueOf(selectionMode));
+      }
+      GECKOBUNDLE_FINISH(collectionInfo);
+
+      GECKOBUNDLE_PUT(nodeInfo, "collectionInfo", collectionInfo);
+    }
+  }
+
+  GECKOBUNDLE_PUT(nodeInfo,
+                  "children",
+                  jni::IntArray::New(aChildren.Elements(), aChildren.Length()));
+  GECKOBUNDLE_FINISH(nodeInfo);
+
+  return nodeInfo;
+}
+
+uint64_t
+AccessibleWrap::GetFlags(role aRole, uint64_t aState)
+{
+  uint64_t flags = 0;
+  if (aState & states::CHECKABLE) {
+    flags |= java::SessionAccessibility::FLAG_CHECKABLE;
+  }
+
+  if (aState & states::CHECKED) {
+    flags |= java::SessionAccessibility::FLAG_CHECKED;
+  }
+
+  if (aState & states::INVALID) {
+    flags |= java::SessionAccessibility::FLAG_CONTENT_INVALID;
+  }
+
+  if (aState & states::EDITABLE) {
+    flags |= java::SessionAccessibility::FLAG_EDITABLE;
+  }
+
+  if (aState & states::SENSITIVE) {
+    flags |= java::SessionAccessibility::FLAG_CLICKABLE;
+  }
+
+  if (aState & states::ENABLED) {
+    flags |= java::SessionAccessibility::FLAG_ENABLED;
+  }
+
+  if (aState & states::FOCUSABLE) {
+    flags |= java::SessionAccessibility::FLAG_FOCUSABLE;
+  }
+
+  if (aState & states::FOCUSED) {
+    flags |= java::SessionAccessibility::FLAG_FOCUSED;
+  }
+
+  if (aState & states::MULTI_LINE) {
+    flags |= java::SessionAccessibility::FLAG_MULTI_LINE;
+  }
+
+  if (aState & states::SELECTABLE) {
+    flags |= java::SessionAccessibility::FLAG_SELECTABLE;
+  }
+
+  if (aState & states::SELECTED) {
+    flags |= java::SessionAccessibility::FLAG_SELECTED;
+  }
+
+  if ((aState & (states::INVISIBLE | states::OFFSCREEN)) == 0) {
+    flags |= java::SessionAccessibility::FLAG_VISIBLE_TO_USER;
+  }
+
+  if (aRole == roles::PASSWORD_TEXT) {
+    flags |= java::SessionAccessibility::FLAG_PASSWORD;
+  }
+
+  return flags;
+}
+
+void
+AccessibleWrap::GetAndroidRoleAndClass(role aRole,
+                                       nsAString& aGeckoRole,
+                                       nsAString& aRoleDescription,
+                                       nsAString& aClassStr)
+{
+  nsresult rv = NS_OK;
+
+  nsCOMPtr<nsIStringBundleService> sbs =
+    do_GetService(NS_STRINGBUNDLE_CONTRACTID, &rv);
+  if (NS_FAILED(rv)) {
+    NS_WARNING("Failed to get string bundle service");
+    return;
+  }
+
+  nsCOMPtr<nsIStringBundle> bundle;
+  rv = sbs->CreateBundle(ROLE_STRINGS_URL, getter_AddRefs(bundle));
+  if (NS_FAILED(rv)) {
+    NS_WARNING("Failed to get string bundle");
+    return;
+  }
+
+#define ROLE(geckoRole,                                                        \
+             stringRole,                                                       \
+             atkRole,                                                          \
+             macRole,                                                          \
+             msaaRole,                                                         \
+             ia2Role,                                                          \
+             androidClass,                                                     \
+             nameRule)                                                         \
+  case roles::geckoRole:                                                       \
+    rv = bundle->GetStringFromName(stringRole, aRoleDescription);              \
+    if (NS_FAILED(rv))                                                         \
+      aRoleDescription.AssignLiteral("");                                      \
+    aGeckoRole.AssignLiteral(stringRole);                                      \
+    aClassStr.AssignLiteral(androidClass);                                     \
+    break;
+
+  switch (aRole) {
+#include "RoleMap.h"
+    default:
+      aRoleDescription.AssignLiteral("");
+      aGeckoRole.AssignLiteral("nothing");
+      aClassStr.AssignLiteral("android.view.View");
+      return;
+  }
+
+#undef ROLE
+}
+
+void
+AccessibleWrap::DOMNodeID(nsString& aDOMNodeID)
+{
+  if (mContent) {
+    nsAtom* id = mContent->GetID();
+    if (id) {
+      id->ToString(aDOMNodeID);
+    }
+  }
+}
+
+mozilla::java::GeckoBundle::LocalRef
+AccessibleWrap::ToBundle()
+{
+  if (IsDefunct()) {
+    return nullptr;
+  }
+
+  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
@@ -2,40 +2,70 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_a11y_AccessibleWrap_h_
 #define mozilla_a11y_AccessibleWrap_h_
 
 #include "Accessible.h"
+#include "GeneratedJNIWrappers.h"
 #include "mozilla/a11y/ProxyAccessible.h"
 #include "nsCOMPtr.h"
 
 namespace mozilla {
 namespace a11y {
 
 class AccessibleWrap : public Accessible
 {
 public:
   AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc);
   virtual ~AccessibleWrap();
 
   virtual void Shutdown() override;
 
-  int32_t VirtualViewID() { return mID; }
+  int32_t VirtualViewID() const { return mID; }
+
+  virtual void SetTextContents(const nsAString& aText);
+
+  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());
 }
 
--- a/accessible/android/ProxyAccessibleWrap.cpp
+++ b/accessible/android/ProxyAccessibleWrap.cpp
@@ -1,15 +1,15 @@
 /* -*- 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 "nsIPersistentProperties2.h"
+#include "nsPersistentProperties.h"
 
 using namespace mozilla::a11y;
 
 ProxyAccessibleWrap::ProxyAccessibleWrap(ProxyAccessible* aProxy)
   : AccessibleWrap(nullptr, nullptr)
 {
   mType = eProxyType;
   mBits.proxy = aProxy;
@@ -43,8 +43,77 @@ ProxyAccessibleWrap::Shutdown()
     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();
+}
+
+// Other
+
+void
+ProxyAccessibleWrap::SetTextContents(const nsAString& aText)
+{
+  Proxy()->ReplaceText(PromiseFlatString(aText));
+}
+
+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);
+}
--- a/accessible/android/ProxyAccessibleWrap.h
+++ b/accessible/android/ProxyAccessibleWrap.h
@@ -9,22 +9,39 @@
 #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;
+
+  // AccessibleWrap
+
+  virtual void SetTextContents(const nsAString& aText) override;
+
+  virtual mozilla::java::GeckoBundle::LocalRef ToBundle() override;
 };
 
 class DocProxyAccessibleWrap : public ProxyAccessibleWrap
 {
 public:
   explicit DocProxyAccessibleWrap(DocAccessibleParent* aProxy)
     : ProxyAccessibleWrap(aProxy)
   {
--- a/accessible/android/SessionAccessibility.cpp
+++ b/accessible/android/SessionAccessibility.cpp
@@ -1,16 +1,20 @@
 /* -*- 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 "HyperTextAccessible.h"
+#include "JavaBuiltins.h"
+#include "RootAccessibleWrap.h"
+#include "nsAccessibilityService.h"
 
 #ifdef DEBUG
 #include <android/log.h>
 #define AALOG(args...)                                                         \
   __android_log_print(ANDROID_LOG_INFO, "GeckoAccessibilityNative", ##args)
 #else
 #define AALOG(args...)                                                         \
   do {                                                                         \
@@ -18,16 +22,30 @@
 #endif
 
 template<>
 const char nsWindow::NativePtr<mozilla::a11y::SessionAccessibility>::sName[] =
   "SessionAccessibility";
 
 using namespace mozilla::a11y;
 
+class Settings final
+  : public mozilla::java::SessionAccessibility::Settings::Natives<Settings>
+{
+public:
+  static void ToggleNativeAccessibility(bool aEnable)
+  {
+    if (aEnable) {
+      GetOrCreateAccService();
+    } else {
+      MaybeShutdownAccService(nsAccessibilityService::ePlatformAPI);
+    }
+  }
+};
+
 void
 SessionAccessibility::SetAttached(bool aAttached,
                                   already_AddRefed<Runnable> aRunnable)
 {
   if (RefPtr<nsThread> uiThread = GetAndroidUiThread()) {
     uiThread->Dispatch(NS_NewRunnableFunction(
       "SessionAccessibility::Attach",
       [aAttached,
@@ -36,8 +54,58 @@ 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());
+  }
+}
--- a/accessible/android/SessionAccessibility.h
+++ b/accessible/android/SessionAccessibility.h
@@ -2,21 +2,51 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_a11y_SessionAccessibility_h_
 #define mozilla_a11y_SessionAccessibility_h_
 
 #include "GeneratedJNINatives.h"
+#include "GeneratedJNIWrappers.h"
+#include "nsAppShell.h"
+#include "nsThreadUtils.h"
 #include "nsWindow.h"
 
+#define GECKOBUNDLE_START(name)                                                \
+  nsTArray<jni::String::LocalRef> _##name##_keys;                              \
+  nsTArray<jni::Object::LocalRef> _##name##_values;
+
+#define GECKOBUNDLE_PUT(name, key, value)                                      \
+  _##name##_keys.AppendElement(jni::StringParam(NS_LITERAL_STRING(key)));      \
+  _##name##_values.AppendElement(value);
+
+#define GECKOBUNDLE_FINISH(name)                                               \
+  MOZ_ASSERT(_##name##_keys.Length() == _##name##_values.Length());            \
+  auto _##name##_jkeys =                                                       \
+    jni::ObjectArray::New<jni::String>(_##name##_keys.Length());               \
+  auto _##name##_jvalues =                                                     \
+    jni::ObjectArray::New<jni::Object>(_##name##_values.Length());             \
+  for (size_t i = 0;                                                           \
+       i < _##name##_keys.Length() && i < _##name##_values.Length();           \
+       i++) {                                                                  \
+    _##name##_jkeys->SetElement(i, _##name##_keys.ElementAt(i));               \
+    _##name##_jvalues->SetElement(i, _##name##_values.ElementAt(i));           \
+  }                                                                            \
+  auto name =                                                                  \
+    mozilla::java::GeckoBundle::New(_##name##_jkeys, _##name##_jvalues);
+
 namespace mozilla {
 namespace a11y {
 
+class AccessibleWrap;
+class ProxyAccessible;
+class RootAccessibleWrap;
+
 class SessionAccessibility final
   : public java::SessionAccessibility::NativeProvider::Natives<SessionAccessibility>
 {
 public:
   typedef java::SessionAccessibility::NativeProvider::Natives<SessionAccessibility> Base;
 
   SessionAccessibility(
     nsWindow::NativePtr<SessionAccessibility>* aPtr,
@@ -33,26 +63,32 @@ public:
     SetAttached(false, std::move(aDisposer));
   }
 
   const java::SessionAccessibility::NativeProvider::Ref& GetJavaAccessibility()
   {
     return mSessionAccessibility;
   }
 
+  static void Init();
+
   // Native implementations
   using Base::AttachNative;
   using Base::DisposeNative;
+  jni::Object::LocalRef GetNodeInfo(int32_t aID);
+  void SetText(int32_t aID, jni::String::Param aText);
+  void StartNativeAccessibility();
 
-  NS_INLINE_DECL_REFCOUNTING(SessionAccessibility)
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SessionAccessibility)
 
 private:
   ~SessionAccessibility() {}
 
   void SetAttached(bool aAttached, already_AddRefed<Runnable> aRunnable);
+  RootAccessibleWrap* GetRoot();
 
   nsWindow::WindowPtr<SessionAccessibility> mWindow; // Parent only
   java::SessionAccessibility::NativeProvider::GlobalRef mSessionAccessibility;
 };
 
 } // namespace a11y
 } // namespace mozilla
 
--- a/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("enabled"));
+                mDynamicToolbar.setAccessibilityEnabled(message.getBoolean("touchEnabled"));
                 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.enabled) {
+        if (aData.touchEnabled) {
           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,16 +1,15 @@
 /* -*- 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
 
@@ -581,28 +580,30 @@ class AccessibilityTest : BaseSessionTes
             @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
-    @Ignore @Test fun autoFill() {
+    @Test fun autoFill() {
         // Wait for the accessibility nodes to populate.
         mainSession.loadTestPath(FORMS_HTML_PATH)
-        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) {
-            }
-        })
+//        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) {
+//            }
+//        })
+        // A quick but not reliable way to test the a11y tree. The next patch will have events
+        // to work with..
+        sessionRule.waitForPageStop()
+
 
         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")
 
@@ -662,17 +663,16 @@ 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)
     @Ignore @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)) 1 else 0) + (if (info.childCount > 0)
                 (0 until info.childCount).sumBy {
                     countAutoFillNodes(cond, info.getChildId(it))
@@ -712,24 +712,147 @@ class AccessibilityTest : BaseSessionTes
         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()
+        // A quick but not reliable way to test the a11y tree. The next patch will have events
+        // to work with..
+        sessionRule.waitForPageStop()
+
+        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()
+        // A quick but not reliable way to test the a11y tree. The next patch will have events
+        // to work with..
+        sessionRule.waitForPageStop()
+
+        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()
+        // A quick but not reliable way to test the a11y tree. The next patch will have events
+        // to work with..
+        sessionRule.waitForPageStop()
+
+        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/SessionAccessibility.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
@@ -1,21 +1,20 @@
 /* -*- 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.mozglue.JNIObject;
 
 import android.content.Context;
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.os.Build;
 import android.os.Bundle;
@@ -23,38 +22,61 @@ import android.text.InputType;
 import android.util.Log;
 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 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 (Build.VERSION.SDK_INT < 17 || mView.getDisplay() != null) {
-                // When running junit tests we don't have a display
-                mView.onInitializeAccessibilityNodeInfo(node);
+            if (mAttached) {
+                populateNodeFromBundle(node, nativeProvider.getNodeInfo(virtualDescendantId));
+            } else {
+                if (Build.VERSION.SDK_INT < 17 || mView.getDisplay() != null) {
+                    // When running junit tests we don't have a display
+                    mView.onInitializeAccessibilityNodeInfo(node);
+                }
+                node.setClassName("android.webkit.WebView");
             }
-            node.setClassName("android.webkit.WebView");
             return node;
         }
 
         @Override
         public boolean performAction(final int virtualViewId, int action, Bundle arguments) {
             final GeckoBundle data;
             switch (action) {
             case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
@@ -126,34 +148,230 @@ 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);
         }
-    };
+
+        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();
-
     private boolean mAttached = false;
 
     /* package */ SessionAccessibility(final GeckoSession session) {
         mSession = session;
-
         Settings.updateAccessibilitySettings();
     }
 
     /**
       * Get the View instance that delegates accessibility to this session.
       *
       * @return View instance.
       */
@@ -249,23 +467,35 @@ 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(1);
-            ret.putBoolean("enabled", isTouchExplorationEnabled());
+            final GeckoBundle ret = new GeckoBundle(2);
+            ret.putBoolean("touchEnabled", isTouchExplorationEnabled());
+            ret.putBoolean("enabled", isEnabled());
             // "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());
+            }
         }
+
+        @WrapForJNI(dispatchTo = "gecko")
+        private static native void toggleNativeAccessibility(boolean enable);
     }
 
     public boolean onMotionEvent(final MotionEvent event) {
         if (!Settings.isTouchExplorationEnabled()) {
             return false;
         }
 
         if (event.getSource() != InputDevice.SOURCE_TOUCHSCREEN) {
@@ -291,10 +521,16 @@ public class SessionAccessibility {
             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);
     }
 }
--- 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.enabled) {
+      if (aData.touchEnabled) {
         AccessFu.enable();
       } else {
         AccessFu.disable();
       }
     }, "GeckoView:AccessibilitySettings");
   }
 }