diff --git a/accessible/android/AccessibleWrap.cpp b/accessible/android/AccessibleWrap.cpp
index c06e6fb5e857..6a7ade21514a 100644
--- a/accessible/android/AccessibleWrap.cpp
+++ b/accessible/android/AccessibleWrap.cpp
@@ -40,6 +40,118 @@ AccessibleWrap::AccessibleWrap(nsIContent* aContent, DocAccessible* aDoc)
//-----------------------------------------------------
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(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(vcEvent->NewAccessible());
+ auto oldPosition = static_cast(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(event->Parent());
+ sessionAcc->SendWindowContentChangedEvent(parent);
+ break;
+ }
+ default:
+ break;
+ }
+
+ return NS_OK;
+}
+
void
AccessibleWrap::Shutdown()
{
@@ -75,6 +187,24 @@ AccessibleWrap::SetTextContents(const nsAString& 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,
diff --git a/accessible/android/AccessibleWrap.h b/accessible/android/AccessibleWrap.h
index 75d03386a37f..a7c37e321b6d 100644
--- a/accessible/android/AccessibleWrap.h
+++ b/accessible/android/AccessibleWrap.h
@@ -20,12 +20,17 @@ 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;
diff --git a/accessible/android/Platform.cpp b/accessible/android/Platform.cpp
index ef2299adcf05..1da5941d056e 100644
--- a/accessible/android/Platform.cpp
+++ b/accessible/android/Platform.cpp
@@ -6,7 +6,10 @@
#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;
@@ -54,33 +57,85 @@ a11y::ProxyDestroyed(ProxyAccessible* aProxy)
}
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
@@ -89,25 +144,57 @@ 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);
+ }
+ }
}
diff --git a/accessible/android/ProxyAccessibleWrap.cpp b/accessible/android/ProxyAccessibleWrap.cpp
index 5091c628fb98..b7391c95d766 100644
--- a/accessible/android/ProxyAccessibleWrap.cpp
+++ b/accessible/android/ProxyAccessibleWrap.cpp
@@ -66,6 +66,18 @@ ProxyAccessibleWrap::Attributes()
return attributes.forget();
}
+uint32_t
+ProxyAccessibleWrap::ChildCount() const
+{
+ return Proxy()->ChildrenCount();
+}
+
+void
+ProxyAccessibleWrap::ScrollTo(uint32_t aHow) const
+{
+ Proxy()->ScrollTo(aHow);
+}
+
// Other
void
@@ -74,6 +86,22 @@ 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()
{
diff --git a/accessible/android/ProxyAccessibleWrap.h b/accessible/android/ProxyAccessibleWrap.h
index 55d2493ed9f4..47509d5b48dc 100644
--- a/accessible/android/ProxyAccessibleWrap.h
+++ b/accessible/android/ProxyAccessibleWrap.h
@@ -32,10 +32,18 @@ public:
virtual already_AddRefed 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;
};
diff --git a/accessible/android/SessionAccessibility.cpp b/accessible/android/SessionAccessibility.cpp
index b82a59e1026e..4e84dd6ecae9 100644
--- a/accessible/android/SessionAccessibility.cpp
+++ b/accessible/android/SessionAccessibility.cpp
@@ -6,10 +6,12 @@
#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
@@ -109,3 +111,209 @@ SessionAccessibility::SetText(int32_t aID, jni::String::Param aText)
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 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(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());
+}
diff --git a/accessible/android/SessionAccessibility.h b/accessible/android/SessionAccessibility.h
index 0a23fc5dd20f..5742321c96fe 100644
--- a/accessible/android/SessionAccessibility.h
+++ b/accessible/android/SessionAccessibility.h
@@ -69,6 +69,8 @@ public:
}
static void Init();
+ static SessionAccessibility* GetInstanceFor(ProxyAccessible* aAccessible);
+ static SessionAccessibility* GetInstanceFor(Accessible* aAccessible);
// Native implementations
using Base::AttachNative;
@@ -77,6 +79,31 @@ public:
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:
diff --git a/accessible/generic/Accessible.h b/accessible/generic/Accessible.h
index 721bfe9367c6..3eb127ae45c8 100644
--- a/accessible/generic/Accessible.h
+++ b/accessible/generic/Accessible.h
@@ -555,7 +555,7 @@ public:
/**
* 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.
diff --git a/accessible/jsat/AccessFu.jsm b/accessible/jsat/AccessFu.jsm
index c63b2998c630..8fd3b4b4ac7b 100644
--- a/accessible/jsat/AccessFu.jsm
+++ b/accessible/jsat/AccessFu.jsm
@@ -19,6 +19,7 @@ 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",
@@ -176,6 +177,9 @@ var AccessFu = {
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;
diff --git 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
index dc37321972e2..1e002c1d30b7 100644
--- 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
@@ -12,6 +12,7 @@ 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
@@ -33,7 +34,6 @@ 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
@@ -97,6 +97,7 @@ class AccessibilityTest : BaseSessionTest() {
fun onTextChanged(event: AccessibilityEvent) { }
fun onTextTraversal(event: AccessibilityEvent) { }
fun onWinContentChanged(event: AccessibilityEvent) { }
+ fun onWinStateChanged(event: AccessibilityEvent) { }
}
@Before fun setup() {
@@ -126,6 +127,7 @@ class AccessibilityTest : BaseSessionTest() {
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
@@ -140,11 +142,21 @@ class AccessibilityTest : BaseSessionTest() {
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() {
@@ -154,7 +166,7 @@ class AccessibilityTest : BaseSessionTest() {
node.className.toString(), equalTo("android.webkit.WebView"))
}
- @Ignore @Test fun testPageLoad() {
+ @Test fun testPageLoad() {
sessionRule.session.loadTestPath(INPUTS_PATH)
sessionRule.waitUntilCalled(object : EventDelegate {
@@ -163,19 +175,18 @@ class AccessibilityTest : BaseSessionTest() {
})
}
- @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))
}
})
@@ -188,12 +199,14 @@ class AccessibilityTest : BaseSessionTest() {
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("", "text/html")
waitForInitialFocus()
@@ -201,7 +214,7 @@ class AccessibilityTest : BaseSessionTest() {
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(),
@@ -275,7 +288,7 @@ class AccessibilityTest : BaseSessionTest() {
return arguments
}
- @Ignore @Test fun testClipboard() {
+ @Test fun testClipboard() {
var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID;
sessionRule.session.loadString("", "text/html")
waitForInitialFocus()
@@ -284,7 +297,7 @@ class AccessibilityTest : BaseSessionTest() {
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(),
@@ -326,13 +339,10 @@ class AccessibilityTest : BaseSessionTest() {
})
}
- @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)
@@ -359,13 +369,10 @@ class AccessibilityTest : BaseSessionTest() {
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)
@@ -392,10 +399,10 @@ class AccessibilityTest : BaseSessionTest() {
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)
@@ -425,12 +432,11 @@ class AccessibilityTest : BaseSessionTest() {
waitUntilTextTraversed(0, 18) // "Lorem ipsum dolor "
}
- @Ignore @Test fun testCheckbox() {
+ @Test fun testCheckbox() {
var nodeId = AccessibilityNodeProvider.HOST_VIEW_ID;
- sessionRule.session.loadString("", "text/html")
- waitForInitialFocus()
+ sessionRule.session.loadString("", "text/html")
+ waitForInitialFocus(true)
- mainSession.evaluateJS("$('#checkbox').focus()")
sessionRule.waitUntilCalled(object : EventDelegate {
@AssertCalled(count = 1)
override fun onAccessibilityFocused(event: AccessibilityEvent) {
@@ -440,7 +446,7 @@ class AccessibilityTest : BaseSessionTest() {
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"))
}
})
@@ -451,16 +457,16 @@ class AccessibilityTest : BaseSessionTest() {
waitUntilClick(false)
}
- @Ignore @Test fun testSelectable() {
+ @Test fun testSelectable() {
var nodeId = View.NO_ID
sessionRule.session.loadString(
"""""","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) {
@@ -468,7 +474,7 @@ class AccessibilityTest : BaseSessionTest() {
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"))
}
})
@@ -492,7 +498,7 @@ class AccessibilityTest : BaseSessionTest() {
return screenRect.contains(nodeBounds)
}
- @Ignore @Test fun testScroll() {
+ @Test fun testScroll() {
var nodeId = View.NO_ID
sessionRule.session.loadString(
"""
@@ -502,9 +508,11 @@ class AccessibilityTest : BaseSessionTest() {
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
""",
"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)
@@ -515,7 +523,7 @@ class AccessibilityTest : BaseSessionTest() {
}
})
- 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) {
@@ -531,25 +539,25 @@ class AccessibilityTest : BaseSessionTest() {
@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])
@@ -559,11 +567,11 @@ class AccessibilityTest : BaseSessionTest() {
@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])
@@ -574,12 +582,11 @@ class AccessibilityTest : BaseSessionTest() {
@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))
}
})
@@ -588,17 +595,7 @@ class AccessibilityTest : BaseSessionTest() {
@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") +
@@ -668,12 +665,12 @@ class AccessibilityTest : BaseSessionTest() {
}
}
- @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)
@@ -681,11 +678,8 @@ class AccessibilityTest : BaseSessionTest() {
// 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",
@@ -693,17 +687,13 @@ class AccessibilityTest : BaseSessionTest() {
// 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",
@@ -732,10 +722,7 @@ class AccessibilityTest : BaseSessionTest() {
sessionRule.session.loadString(
"",
"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))
@@ -777,10 +764,7 @@ class AccessibilityTest : BaseSessionTest() {
|
""".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))
@@ -816,10 +800,7 @@ class AccessibilityTest : BaseSessionTest() {
|
""".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))
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
index ef63af24d84c..9e52d7ccd5ee 100644
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/SessionAccessibility.java
@@ -11,6 +11,7 @@ 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;
@@ -19,7 +20,6 @@ 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;
@@ -72,21 +72,36 @@ public class SessionAccessibility {
}
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.getInt("flags");
+ 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.getInt("flags");
+ if ((flags & (FLAG_SELECTABLE | FLAG_CHECKABLE)) == 0) {
+ sendEvent(AccessibilityEvent.TYPE_VIEW_CLICKED, virtualViewId, null, nodeInfo);
+ }
return true;
case AccessibilityNodeInfo.ACTION_LONG_CLICK:
mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityLongPress", null);
@@ -166,6 +181,16 @@ public class SessionAccessibility {
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;
@@ -364,6 +389,8 @@ public class SessionAccessibility {
// 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;
@@ -455,6 +482,8 @@ public class SessionAccessibility {
PrefsHelper.addObserver(new String[]{ FORCE_ACCESSIBILITY_PREF }, prefHandler);
}
+ public static boolean isPlatformEnabled() { return sEnabled; }
+
public static boolean isEnabled() {
return sEnabled || sForceEnabled;
}
@@ -515,6 +544,55 @@ public class SessionAccessibility {
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) {
@@ -532,5 +610,15 @@ public class SessionAccessibility {
@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);
+ }
+ });
+ }
}
}
diff --git a/widget/android/bindings/AccessibilityEvent-classes.txt b/widget/android/bindings/AccessibilityEvent-classes.txt
new file mode 100644
index 000000000000..afb1be9d9659
--- /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]
+ = skip:false
diff --git a/widget/android/bindings/moz.build b/widget/android/bindings/moz.build
index e16d4cdeb150..e498161b25be 100644
--- a/widget/android/bindings/moz.build
+++ b/widget/android/bindings/moz.build
@@ -10,6 +10,7 @@ with Files("**"):
# 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',