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( """
  • 1
  • +
  • 2
""","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',