Bug 1479037 - Introduce native event support 4/4. r=jchen,yzen?jamie
authorEitan Isaacson <eitan@monotonous.org>
Thu, 11 Oct 2018 16:22:11 +0000
changeset 499182 d41905041ca7f615f90fce70e1de3c325e490c44
parent 499181 97b5d09ed65af9bfa5eb287f4a5c0f7e5e4ae4a6
child 499183 9b6dac8e39db00a7a5739c05f0b868e2fd88efb5
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)
reviewersjchen
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 - Introduce native event support 4/4. r=jchen,yzen?jamie Depends on D6683 Differential Revision: https://phabricator.services.mozilla.com/D6684
accessible/android/AccessibleWrap.cpp
accessible/android/AccessibleWrap.h
accessible/android/Platform.cpp
accessible/android/ProxyAccessibleWrap.cpp
accessible/android/ProxyAccessibleWrap.h
accessible/android/SessionAccessibility.cpp
accessible/android/SessionAccessibility.h
accessible/generic/Accessible.h
accessible/jsat/AccessFu.jsm
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/AccessibilityTest.kt
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
widget/android/bindings/AccessibilityEvent-classes.txt
widget/android/bindings/moz.build
--- a/accessible/android/AccessibleWrap.cpp
+++ b/accessible/android/AccessibleWrap.cpp
@@ -35,16 +35,128 @@ AccessibleWrap::AccessibleWrap(nsIConten
   }
 }
 
 //-----------------------------------------------------
 // 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);
       }
@@ -70,16 +182,34 @@ AccessibleWrap::ReleaseID(int32_t 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,
--- a/accessible/android/AccessibleWrap.h
+++ b/accessible/android/AccessibleWrap.h
@@ -15,22 +15,27 @@ namespace mozilla {
 namespace a11y {
 
 class AccessibleWrap : public Accessible
 {
 public:
   AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc);
   virtual ~AccessibleWrap();
 
+  virtual nsresult HandleAccEvent(AccEvent* aEvent) override;
   virtual void Shutdown() override;
 
   int32_t VirtualViewID() const { return mID; }
 
   virtual void SetTextContents(const nsAString& aText);
 
+  virtual void GetTextContents(nsAString& aText);
+
+  virtual bool GetSelectionBounds(int32_t* aStartOffset, int32_t* aEndOffset);
+
   virtual mozilla::java::GeckoBundle::LocalRef ToBundle();
 
   static const int32_t kNoID = -1;
 
 protected:
   mozilla::java::GeckoBundle::LocalRef CreateBundle(
     int32_t aParentID,
     role aRole,
--- a/accessible/android/Platform.cpp
+++ b/accessible/android/Platform.cpp
@@ -1,17 +1,20 @@
 /* -*- 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()
 {
 }
@@ -49,65 +52,149 @@ a11y::ProxyDestroyed(ProxyAccessible* aP
   }
 
   wrapper->Shutdown();
   aProxy->SetWrapper(0);
   wrapper->Release();
 }
 
 void
-a11y::ProxyEvent(ProxyAccessible*, uint32_t)
+a11y::ProxyEvent(ProxyAccessible* aTarget, uint32_t aEventType)
 {
+  SessionAccessibility* sessionAcc =
+    SessionAccessibility::GetInstanceFor(aTarget);
+  if (!sessionAcc) {
+    return;
+  }
+
+  switch (aEventType) {
+    case nsIAccessibleEvent::EVENT_FOCUS:
+      sessionAcc->SendFocusEvent(WrapperFor(aTarget));
+      break;
+  }
 }
 
 void
-a11y::ProxyStateChangeEvent(ProxyAccessible*, uint64_t, bool)
+a11y::ProxyStateChangeEvent(ProxyAccessible* aTarget,
+                            uint64_t aState,
+                            bool aEnabled)
 {
+  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::ProxyCaretMoveEvent(ProxyAccessible* aTarget, int32_t aOffset)
 {
+  SessionAccessibility* sessionAcc =
+    SessionAccessibility::GetInstanceFor(aTarget);
+
+  if (sessionAcc) {
+    sessionAcc->SendTextSelectionChangedEvent(WrapperFor(aTarget), aOffset);
+  }
 }
 
 void
-a11y::ProxyTextChangeEvent(ProxyAccessible*,
-                           const nsString&,
-                           int32_t,
-                           uint32_t,
-                           bool,
-                           bool)
+a11y::ProxyTextChangeEvent(ProxyAccessible* aTarget,
+                           const nsString& aStr,
+                           int32_t aStart,
+                           uint32_t aLen,
+                           bool aIsInsert,
+                           bool aFromUser)
 {
+  SessionAccessibility* sessionAcc =
+    SessionAccessibility::GetInstanceFor(aTarget);
+
+  if (sessionAcc) {
+    sessionAcc->SendTextChangedEvent(
+      WrapperFor(aTarget), aStr, aStart, aLen, aIsInsert, aFromUser);
+  }
 }
 
 void
-a11y::ProxyShowHideEvent(ProxyAccessible*, ProxyAccessible*, bool, bool)
+a11y::ProxyShowHideEvent(ProxyAccessible* aTarget,
+                         ProxyAccessible* aParent,
+                         bool aInsert,
+                         bool aFromUser)
 {
+  SessionAccessibility* sessionAcc =
+    SessionAccessibility::GetInstanceFor(aTarget);
+  if (sessionAcc) {
+    sessionAcc->SendWindowContentChangedEvent(WrapperFor(aParent));
+  }
 }
 
 void
 a11y::ProxySelectionEvent(ProxyAccessible*, ProxyAccessible*, uint32_t)
 {
 }
 
 void
-a11y::ProxyVirtualCursorChangeEvent(ProxyAccessible*,
-                                    ProxyAccessible*,
-                                    int32_t,
-                                    int32_t,
-                                    ProxyAccessible*,
-                                    int32_t,
-                                    int32_t,
-                                    int16_t,
-                                    int16_t,
-                                    bool)
+a11y::ProxyVirtualCursorChangeEvent(ProxyAccessible* aTarget,
+                                    ProxyAccessible* aOldPosition,
+                                    int32_t aOldStartOffset,
+                                    int32_t aOldEndOffset,
+                                    ProxyAccessible* aNewPosition,
+                                    int32_t aNewStartOffset,
+                                    int32_t aNewEndOffset,
+                                    int16_t aReason,
+                                    int16_t aBoundaryType,
+                                    bool aFromUser)
 {
+  if (!aNewPosition) {
+    return;
+  }
+
+  SessionAccessibility* sessionAcc =
+    SessionAccessibility::GetInstanceFor(aTarget);
+
+  if (!sessionAcc) {
+    return;
+  }
+
+  if (aOldPosition != aNewPosition) {
+    if (aReason == nsIAccessiblePivot::REASON_POINT) {
+      sessionAcc->SendHoverEnterEvent(WrapperFor(aNewPosition));
+    } else {
+      sessionAcc->SendAccessibilityFocusedEvent(WrapperFor(aNewPosition));
+    }
+  }
+
+  if (aBoundaryType != nsIAccessiblePivot::NO_BOUNDARY) {
+    sessionAcc->SendTextTraversedEvent(
+      WrapperFor(aNewPosition), aNewStartOffset, aNewEndOffset);
+  }
 }
 
 void
-a11y::ProxyScrollingEvent(ProxyAccessible*,
-                          uint32_t,
-                          uint32_t,
-                          uint32_t,
-                          uint32_t,
-                          uint32_t)
+a11y::ProxyScrollingEvent(ProxyAccessible* aTarget,
+                          uint32_t aEventType,
+                          uint32_t aScrollX,
+                          uint32_t aScrollY,
+                          uint32_t aMaxScrollX,
+                          uint32_t aMaxScrollY)
 {
+  if (aEventType == nsIAccessibleEvent::EVENT_SCROLLING) {
+    SessionAccessibility* sessionAcc =
+      SessionAccessibility::GetInstanceFor(aTarget);
+
+    if (sessionAcc) {
+      sessionAcc->SendScrollingEvent(
+        WrapperFor(aTarget), aScrollX, aScrollY, aMaxScrollX, aMaxScrollY);
+    }
+  }
 }
--- a/accessible/android/ProxyAccessibleWrap.cpp
+++ b/accessible/android/ProxyAccessibleWrap.cpp
@@ -61,24 +61,52 @@ ProxyAccessibleWrap::Attributes()
   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;
   }
 
--- a/accessible/android/ProxyAccessibleWrap.h
+++ b/accessible/android/ProxyAccessibleWrap.h
@@ -27,20 +27,28 @@ 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)
--- a/accessible/android/SessionAccessibility.cpp
+++ b/accessible/android/SessionAccessibility.cpp
@@ -1,20 +1,22 @@
 /* -*- Mode: c++; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "SessionAccessibility.h"
 #include "AndroidUiThread.h"
 #include "nsThreadUtils.h"
+#include "AccessibilityEvent.h"
 #include "HyperTextAccessible.h"
 #include "JavaBuiltins.h"
 #include "RootAccessibleWrap.h"
 #include "nsAccessibilityService.h"
+#include "nsViewManager.h"
 
 #ifdef DEBUG
 #include <android/log.h>
 #define AALOG(args...)                                                         \
   __android_log_print(ANDROID_LOG_INFO, "GeckoAccessibilityNative", ##args)
 #else
 #define AALOG(args...)                                                         \
   do {                                                                         \
@@ -104,8 +106,214 @@ SessionAccessibility::SetText(int32_t aI
     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
@@ -64,24 +64,51 @@ public:
   }
 
   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)
 
 private:
   ~SessionAccessibility() {}
 
   void SetAttached(bool aAttached, already_AddRefed<Runnable> aRunnable);
   RootAccessibleWrap* GetRoot();
 
--- a/accessible/generic/Accessible.h
+++ b/accessible/generic/Accessible.h
@@ -550,17 +550,17 @@ public:
   /**
    * Focus the accessible.
    */
   virtual void TakeFocus() const;
 
   /**
    * Scroll the accessible into view.
    */
-  void ScrollTo(uint32_t aHow) const;
+  virtual void ScrollTo(uint32_t aHow) const;
 
   /**
    * Scroll the accessible to the given point.
    */
   void ScrollToPoint(uint32_t aCoordinateType, int32_t aX, int32_t aY);
 
   /**
    * Get a pointer to accessibility interface for this node, which is specific
--- a/accessible/jsat/AccessFu.jsm
+++ b/accessible/jsat/AccessFu.jsm
@@ -14,16 +14,17 @@ 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",
@@ -171,16 +172,19 @@ 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 });
+        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:
         this.Input.setSelection(data);
--- 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
@@ -7,16 +7,17 @@ package org.mozilla.geckoview.test
 import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
 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
@@ -28,17 +29,16 @@ import android.view.ViewGroup
 import android.widget.EditText
 
 import android.widget.FrameLayout
 
 import org.hamcrest.Matchers.*
 import org.junit.Test
 import org.junit.Before
 import org.junit.After
-import org.junit.Ignore
 import org.junit.runner.RunWith
 
 const val DISPLAY_WIDTH = 480
 const val DISPLAY_HEIGHT = 640
 
 @RunWith(AndroidJUnit4::class)
 @MediumTest
 @WithDisplay(width = DISPLAY_WIDTH, height = DISPLAY_HEIGHT)
@@ -92,16 +92,17 @@ 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)
@@ -121,92 +122,104 @@ 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() {
+    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.
         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"))
     }
 
-    @Ignore @Test fun testPageLoad() {
+    @Test fun testPageLoad() {
         sessionRule.session.loadTestPath(INPUTS_PATH)
 
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled(count = 1)
             override fun onFocused(event: AccessibilityEvent) { }
         })
     }
 
-    @Ignore @Test fun testAccessibilityFocus() {
+    @Test fun testAccessibilityFocus() {
         var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
         sessionRule.session.loadTestPath(INPUTS_PATH)
-        waitForInitialFocus()
-
-        provider.performAction(AccessibilityNodeProvider.HOST_VIEW_ID,
-            AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null)
+        waitForInitialFocus(true)
 
         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))
             }
         })
     }
 
-    @Ignore @Test fun testTextEntryNode() {
+    @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 onAccessibilityFocused(event: AccessibilityEvent) {
+            override fun onFocused(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"))
@@ -270,26 +283,26 @@ class AccessibilityTest : BaseSessionTes
 
     private fun moveByGranularityArguments(granularity: Int, extendSelection: Boolean = false): Bundle {
         val arguments = Bundle(2)
         arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT, granularity)
         arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, extendSelection)
         return arguments
     }
 
-    @Ignore @Test fun testClipboard() {
+    @Test fun testClipboard() {
         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 onAccessibilityFocused(event: AccessibilityEvent) {
+            override fun onFocused(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) {
@@ -321,23 +334,20 @@ class AccessibilityTest : BaseSessionTes
         sessionRule.waitUntilCalled(object : EventDelegate {
             @AssertCalled
             override fun onTextChanged(event: AccessibilityEvent) {
                 assertThat("text should be pasted", event.text[0].toString(), equalTo("hello cruel cruel cruel"))
             }
         })
     }
 
-    @Ignore @Test fun testMoveByCharacter() {
+    @Test fun testMoveByCharacter() {
         var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
         sessionRule.session.loadTestPath(LOREM_IPSUM_HTML_PATH)
-        waitForInitialFocus()
-
-        provider.performAction(AccessibilityNodeProvider.HOST_VIEW_ID,
-                AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null)
+        waitForInitialFocus(true)
 
         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"))
             }
@@ -354,23 +364,20 @@ class AccessibilityTest : BaseSessionTes
         waitUntilTextTraversed(1, 2) // "o"
 
         provider.performAction(nodeId,
                 AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
                 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER))
         waitUntilTextTraversed(0, 1) // "L"
     }
 
-    @Ignore @Test fun testMoveByWord() {
+    @Test fun testMoveByWord() {
         var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
         sessionRule.session.loadTestPath(LOREM_IPSUM_HTML_PATH)
-        waitForInitialFocus()
-
-        provider.performAction(AccessibilityNodeProvider.HOST_VIEW_ID,
-                AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null)
+        waitForInitialFocus(true)
 
         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"))
             }
@@ -387,20 +394,20 @@ class AccessibilityTest : BaseSessionTes
         waitUntilTextTraversed(6, 11) // "ipsum"
 
         provider.performAction(nodeId,
                 AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
                 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD))
         waitUntilTextTraversed(0, 5) // "Lorem"
     }
 
-    @Ignore @Test fun testMoveByLine() {
+    @Test fun testMoveByLine() {
         var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID
         sessionRule.session.loadTestPath(LOREM_IPSUM_HTML_PATH)
-        waitForInitialFocus()
+        waitForInitialFocus(true)
 
         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)
@@ -420,60 +427,59 @@ class AccessibilityTest : BaseSessionTes
         waitUntilTextTraversed(18, 28) // "sit amet, "
 
         provider.performAction(nodeId,
                 AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY,
                 moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE))
         waitUntilTextTraversed(0, 18) // "Lorem ipsum dolor "
     }
 
-    @Ignore @Test fun testCheckbox() {
+    @Test fun testCheckbox() {
         var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID;
-        sessionRule.session.loadString("<label><input id='checkbox' type='checkbox'>many option</label>", "text/html")
-        waitForInitialFocus()
+        sessionRule.session.loadString("<label><input type='checkbox'>many option</label>", "text/html")
+        waitForInitialFocus(true)
 
-        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 check button"))
+                assertThat("Checkbox node has correct role", node.text.toString(), equalTo("many option"))
             }
         })
 
         provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CLICK, null)
         waitUntilClick(true)
 
         provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CLICK, null)
         waitUntilClick(false)
     }
 
-    @Ignore @Test fun testSelectable() {
+    @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()
+        waitForInitialFocus(true)
 
-        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 role", node.text.toString(), equalTo("1 option list box"))
+                assertThat("Selectable node has correct text", node.text.toString(), equalTo("1"))
             }
         })
 
         provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CLICK, null)
         waitUntilSelect(true)
 
         provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CLICK, null)
         waitUntilSelect(false)
@@ -487,123 +493,114 @@ class AccessibilityTest : BaseSessionTes
 
     private fun screenContainsNode(nodeId: Int): Boolean {
         var node = createNodeInfo(nodeId)
         var nodeBounds = Rect()
         node.getBoundsInScreen(nodeBounds)
         return screenRect.contains(nodeBounds)
     }
 
-    @Ignore @Test fun testScroll() {
+    @Test fun testScroll() {
         var nodeId = View.NO_ID
         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_ACCESSIBILITY_FOCUS, null)
+        provider.performAction(View.NO_ID, 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 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, equalTo(event.maxScrollY))
+                assertThat("View is scrolled to the end", event.scrollY.toDouble(), closeTo(event.maxScrollY.toDouble(), 1.0))
             }
 
             @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, equalTo(event.maxScrollY))
+                assertThat("View is scrolled to the end", event.scrollY.toDouble(), closeTo(event.maxScrollY.toDouble(), 1.0))
             }
 
             @AssertCalled(count = 1, order = [3])
             override fun onWinContentChanged(event: AccessibilityEvent) {
-                nodeId = getSourceId(event)
                 assertThat("Focused node is onscreen", screenContainsNode(nodeId), equalTo(true))
             }
         })
     }
 
     @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) {
-//            }
-//        })
-        // A quick but not reliable way to test the a11y tree. The next patch will have events
-        // to work with..
-        sessionRule.waitForPageStop()
-
+        waitForInitialFocus()
 
         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")
 
@@ -663,52 +660,45 @@ 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))
         }
     }
 
-    @Ignore @Test fun autoFill_navigation() {
+    @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)
+            return (if (cond(info) && info.className != "android.webkit.WebView" ) 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)
-        sessionRule.waitUntilCalled(object : EventDelegate {
-            @AssertCalled(count = 4)
-            override fun onWinContentChanged(event: AccessibilityEvent) {
-            }
-        })
+        waitForInitialFocus()
+
         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)
-        mainSession.waitForPageStop()
+        waitForInitialFocus()
         assertThat("Should not have auto-fill fields",
                    countAutoFillNodes(), equalTo(0))
 
         // Now wait for the nodes to reappear.
         mainSession.goBack()
-        sessionRule.waitUntilCalled(object : EventDelegate {
-            @AssertCalled(count = 4)
-            override fun onWinContentChanged(event: AccessibilityEvent) {
-            }
-        })
+        waitForInitialFocus()
         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
@@ -727,20 +717,17 @@ class AccessibilityTest : BaseSessionTes
         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()
+        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:"))
 
@@ -772,20 +759,17 @@ class AccessibilityTest : BaseSessionTes
                   |  <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()
+        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) {
@@ -811,20 +795,17 @@ class AccessibilityTest : BaseSessionTes
 
     @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()
+        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) {
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
@@ -6,25 +6,25 @@
 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.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.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;
@@ -67,31 +67,46 @@ public class SessionAccessibility {
                 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.setAccessibilityFocused(mAccessibilityFocusedNode == virtualDescendantId);
             return node;
         }
 
         @Override
         public boolean performAction(final int virtualViewId, int action, Bundle arguments) {
             final GeckoBundle data;
+
             switch (action) {
             case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
-                final AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
-                event.setPackageName(GeckoAppShell.getApplicationContext().getPackageName());
-                event.setSource(mView, virtualViewId);
-                ((ViewParent) mView).requestSendAccessibilityEvent(mView, event);
+                    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 != null ? nodeInfo.getInt("flags") : 0;
+                        if ((flags & FLAG_FOCUSED) != 0) {
+                            mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityCursorToFocused", null);
+                        } else {
+                            sendEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, virtualViewId, null, nodeInfo);
+                        }
+                    }
                 return true;
             case AccessibilityNodeInfo.ACTION_CLICK:
                 mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityActivate", null);
+                GeckoBundle nodeInfo = nativeProvider.getNodeInfo(virtualViewId);
+                final int flags = nodeInfo != null ? nodeInfo.getInt("flags") : 0;
+                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:
@@ -161,16 +176,26 @@ public class SessionAccessibility {
                     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) {
@@ -359,16 +384,18 @@ public class SessionAccessibility {
 
     // 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;
+    // The current node with accessibility focus
+    private int mAccessibilityFocusedNode = 0;
 
     /* package */ SessionAccessibility(final GeckoSession session) {
         mSession = session;
         Settings.updateAccessibilitySettings();
     }
 
     /**
       * Get the View instance that delegates accessibility to this session.
@@ -450,16 +477,18 @@ 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;
         }
 
@@ -510,16 +539,65 @@ 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) {
+            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.
+            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", ""));
+        }
+
+        if (eventData != null) {
+            if (eventData.containsKey("text")) {
+                event.getText().add(eventData.getString("text"));
+            }
+            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));
+        }
+
+        ((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() {
@@ -527,10 +605,20 @@ public class SessionAccessibility {
             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);
+                }
+            });
+        }
     }
 }
new file mode 100644
--- /dev/null
+++ b/widget/android/bindings/AccessibilityEvent-classes.txt
@@ -0,0 +1,3 @@
+# 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,16 +5,17 @@
 # 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'