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 489092 d41905041ca7f615f90fce70e1de3c325e490c44
parent 489091 97b5d09ed65af9bfa5eb287f4a5c0f7e5e4ae4a6
child 489093 9b6dac8e39db00a7a5739c05f0b868e2fd88efb5
push id246
push userfmarier@mozilla.com
push dateSat, 13 Oct 2018 00:15:40 +0000
reviewersjchen
bugs1479037
milestone64.0a1
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'