diff --git a/accessible/android/AccessibleWrap.cpp b/accessible/android/AccessibleWrap.cpp index 302074daec37..a7183671a6a9 100644 --- a/accessible/android/AccessibleWrap.cpp +++ b/accessible/android/AccessibleWrap.cpp @@ -301,6 +301,78 @@ void AccessibleWrap::ExploreByTouch(float aX, float aY) { } } +void AccessibleWrap::NavigateText(int32_t aGranularity, int32_t aStartOffset, + int32_t aEndOffset, bool aForward, + bool aSelect) { + a11y::Pivot pivot(RootAccessible()); + + HyperTextAccessible* editable = + (State() & states::EDITABLE) != 0 ? AsHyperText() : nullptr; + + int32_t start = aStartOffset, end = aEndOffset; + // If the accessible is an editable, set the virtual cursor position + // to its caret offset. Otherwise use the document's virtual cursor + // position as a starting offset. + if (editable) { + start = end = editable->CaretOffset(); + } + + uint16_t pivotGranularity = nsIAccessiblePivot::LINE_BOUNDARY; + switch (aGranularity) { + case 1: // MOVEMENT_GRANULARITY_CHARACTER + pivotGranularity = nsIAccessiblePivot::CHAR_BOUNDARY; + break; + case 2: // MOVEMENT_GRANULARITY_WORD + pivotGranularity = nsIAccessiblePivot::WORD_BOUNDARY; + break; + default: + break; + } + + int32_t newOffset; + Accessible* newAnchor = nullptr; + if (aForward) { + newAnchor = pivot.NextText(this, &start, &end, pivotGranularity); + newOffset = end; + } else { + newAnchor = pivot.PrevText(this, &start, &end, pivotGranularity); + newOffset = start; + } + + if (newAnchor && (start != aStartOffset || end != aEndOffset)) { + RefPtr event = new AccVCChangeEvent( + newAnchor->Document(), this, aStartOffset, aEndOffset, newAnchor, start, + end, nsIAccessiblePivot::REASON_NONE, pivotGranularity, eFromUserInput); + nsEventShell::FireEvent(event); + } + + // If we are in an editable, move the caret to the new virtual cursor + // offset. + if (editable) { + if (aSelect) { + int32_t anchor = editable->CaretOffset(); + if (editable->SelectionCount()) { + int32_t startSel, endSel; + GetSelectionOrCaret(&startSel, &endSel); + anchor = startSel == anchor ? endSel : startSel; + } + editable->SetSelectionBoundsAt(0, anchor, newOffset); + } else { + editable->SetCaretOffset(newOffset); + } + } +} + +void AccessibleWrap::GetSelectionOrCaret(int32_t* aStartOffset, + int32_t* aEndOffset) { + *aStartOffset = *aEndOffset = -1; + if (HyperTextAccessible* textAcc = AsHyperText()) { + if (!textAcc->SelectionBoundsAt(0, aStartOffset, aEndOffset)) { + *aStartOffset = *aEndOffset = textAcc->CaretOffset(); + } + } +} + uint32_t AccessibleWrap::GetFlags(role aRole, uint64_t aState, uint8_t aActionCount) { uint32_t flags = 0; diff --git a/accessible/android/AccessibleWrap.h b/accessible/android/AccessibleWrap.h index b5041aaa85f1..f49aba253a9e 100644 --- a/accessible/android/AccessibleWrap.h +++ b/accessible/android/AccessibleWrap.h @@ -39,6 +39,9 @@ class AccessibleWrap : public Accessible { virtual void ExploreByTouch(float aX, float aY); + virtual void NavigateText(int32_t aGranularity, int32_t aStartOffset, + int32_t aEndOffset, bool aForward, bool aSelect); + mozilla::java::GeckoBundle::LocalRef ToBundle(bool aSmall = false); mozilla::java::GeckoBundle::LocalRef ToBundle( @@ -89,6 +92,8 @@ class AccessibleWrap : public Accessible { bool HandleLiveRegionEvent(AccEvent* aEvent); + void GetSelectionOrCaret(int32_t* aStartOffset, int32_t* aEndOffset); + static void GetRoleDescription(role aRole, nsIPersistentProperties* aAttributes, nsAString& aGeckoRole, diff --git a/accessible/android/ProxyAccessibleWrap.cpp b/accessible/android/ProxyAccessibleWrap.cpp index 55e83faa5aba..50bc9aa0cddf 100644 --- a/accessible/android/ProxyAccessibleWrap.cpp +++ b/accessible/android/ProxyAccessibleWrap.cpp @@ -120,6 +120,13 @@ void ProxyAccessibleWrap::ExploreByTouch(float aX, float aY) { Proxy()->ID(), aX, aY); } +void ProxyAccessibleWrap::NavigateText(int32_t aGranularity, + int32_t aStartOffset, int32_t aEndOffset, + bool aForward, bool aSelect) { + Unused << Proxy()->Document()->GetPlatformExtension()->SendNavigateText( + Proxy()->ID(), aGranularity, aStartOffset, aEndOffset, aForward, aSelect); +} + role ProxyAccessibleWrap::WrapperRole() { return Proxy()->Role(); } AccessibleWrap* ProxyAccessibleWrap::WrapperParent() { diff --git a/accessible/android/ProxyAccessibleWrap.h b/accessible/android/ProxyAccessibleWrap.h index ae89b5a0edfc..05d6d794b481 100644 --- a/accessible/android/ProxyAccessibleWrap.h +++ b/accessible/android/ProxyAccessibleWrap.h @@ -62,6 +62,10 @@ class ProxyAccessibleWrap : public AccessibleWrap { virtual void Pivot(int32_t aGranularity, bool aForward, bool aInclusive) override; + virtual void NavigateText(int32_t aGranularity, int32_t aStartOffset, + int32_t aEndOffset, bool aForward, + bool aSelect) override; + virtual void ExploreByTouch(float aX, float aY) override; virtual void WrapperDOMNodeID(nsString& aDOMNodeID) override; diff --git a/accessible/android/SessionAccessibility.cpp b/accessible/android/SessionAccessibility.cpp index 15dd4c955620..8b1e385149bb 100644 --- a/accessible/android/SessionAccessibility.cpp +++ b/accessible/android/SessionAccessibility.cpp @@ -124,6 +124,14 @@ void SessionAccessibility::ExploreByTouch(int32_t aID, float aX, float aY) { FORWARD_ACTION_TO_ACCESSIBLE(ExploreByTouch, aX, aY); } +void SessionAccessibility::NavigateText(int32_t aID, int32_t aGranularity, + int32_t aStartOffset, + int32_t aEndOffset, bool aForward, + bool aSelect) { + FORWARD_ACTION_TO_ACCESSIBLE(NavigateText, aGranularity, aStartOffset, + aEndOffset, aForward, aSelect); +} + SessionAccessibility* SessionAccessibility::GetInstanceFor( ProxyAccessible* aAccessible) { auto tab = diff --git a/accessible/android/SessionAccessibility.h b/accessible/android/SessionAccessibility.h index f862787bee85..7970514d0590 100644 --- a/accessible/android/SessionAccessibility.h +++ b/accessible/android/SessionAccessibility.h @@ -56,6 +56,8 @@ class SessionAccessibility final void Click(int32_t aID); void Pivot(int32_t aID, int32_t aGranularity, bool aForward, bool aInclusive); void ExploreByTouch(int32_t aID, float aX, float aY); + void NavigateText(int32_t aID, int32_t aGranularity, int32_t aStartOffset, + int32_t aEndOffset, bool aForward, bool aSelect); void StartNativeAccessibility(); // Event methods diff --git a/accessible/ipc/extension/android/DocAccessiblePlatformExtChild.cpp b/accessible/ipc/extension/android/DocAccessiblePlatformExtChild.cpp index 99a30eb9cebc..1f8853b8b723 100644 --- a/accessible/ipc/extension/android/DocAccessiblePlatformExtChild.cpp +++ b/accessible/ipc/extension/android/DocAccessiblePlatformExtChild.cpp @@ -22,10 +22,11 @@ mozilla::ipc::IPCResult DocAccessiblePlatformExtChild::RecvPivot( } mozilla::ipc::IPCResult DocAccessiblePlatformExtChild::RecvNavigateText( - int32_t aID, int32_t aGranularity, int32_t aStartOffset, int32_t aEndOffset, + uint64_t aID, int32_t aGranularity, int32_t aStartOffset, int32_t aEndOffset, bool aForward, bool aSelect) { if (auto acc = IdToAccessibleWrap(aID)) { - // XXX: Forward to appropriate wrapper method. + acc->NavigateText(aGranularity, aStartOffset, aEndOffset, aForward, + aSelect); } return IPC_OK(); diff --git a/accessible/ipc/extension/android/DocAccessiblePlatformExtChild.h b/accessible/ipc/extension/android/DocAccessiblePlatformExtChild.h index c56c8b06a9ac..cb9958a0204b 100644 --- a/accessible/ipc/extension/android/DocAccessiblePlatformExtChild.h +++ b/accessible/ipc/extension/android/DocAccessiblePlatformExtChild.h @@ -19,7 +19,7 @@ class DocAccessiblePlatformExtChild : public PDocAccessiblePlatformExtChild { mozilla::ipc::IPCResult RecvPivot(uint64_t aID, int32_t aGranularity, bool aForward, bool aInclusive); - mozilla::ipc::IPCResult RecvNavigateText(int32_t aID, int32_t aGranularity, + mozilla::ipc::IPCResult RecvNavigateText(uint64_t aID, int32_t aGranularity, int32_t aStartOffset, int32_t aEndOffset, bool aForward, bool aSelect); diff --git a/accessible/ipc/extension/android/PDocAccessiblePlatformExt.ipdl b/accessible/ipc/extension/android/PDocAccessiblePlatformExt.ipdl index 8f72eaa3d998..ea190d032025 100644 --- a/accessible/ipc/extension/android/PDocAccessiblePlatformExt.ipdl +++ b/accessible/ipc/extension/android/PDocAccessiblePlatformExt.ipdl @@ -17,7 +17,7 @@ child: async Pivot(uint64_t aID, int32_t aGranularity, bool aForward, bool aInclusive); - async NavigateText(int32_t aID, int32_t aGranularity, int32_t aStartOffset, int32_t aEndOffset, bool aForward, bool aSelect); + async NavigateText(uint64_t aID, int32_t aGranularity, int32_t aStartOffset, int32_t aEndOffset, bool aForward, bool aSelect); async SetSelection(int32_t aID, int32_t aStart, int32_t aEnd); 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 9281babfbb85..6e3b73e43a40 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 @@ -318,14 +318,17 @@ class ZZAccessibilityTest : BaseSessionTest() { } while (fromIndex != eventFromIndex || toIndex != eventToIndex) } - private fun waitUntilTextTraversed(fromIndex: Int, toIndex: Int) { + private fun waitUntilTextTraversed(fromIndex: Int, toIndex: Int): Int { + var nodeId: Int = AccessibilityNodeProvider.HOST_VIEW_ID sessionRule.waitUntilCalled(object : EventDelegate { @AssertCalled(count = 1) override fun onTextTraversal(event: AccessibilityEvent) { + nodeId = getSourceId(event) assertThat("fromIndex matches", event.fromIndex, equalTo(fromIndex)) assertThat("toIndex matches", event.toIndex, equalTo(toIndex)) } }) + return nodeId } private fun waitUntilClick(checked: Boolean) { @@ -414,6 +417,22 @@ class ZZAccessibilityTest : BaseSessionTest() { assertThat("text should be pasted", event.text[0].toString(), equalTo("hello cruel cruel cruel")) } }) + + provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_SET_SELECTION, setSelectionArguments(0, 0)) + waitUntilTextSelectionChanged(0, 0) + + provider.performAction(nodeId, + AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, + moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD, true)) + waitUntilTextSelectionChanged(0, 5) + + provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_CUT, null) + sessionRule.waitUntilCalled(object : EventDelegate { + @AssertCalled + override fun onTextChanged(event: AccessibilityEvent) { + assertThat("text should be cut", event.text[0].toString(), equalTo(" cruel cruel cruel")) + } + }) } @Test fun testMoveByCharacter() { @@ -433,17 +452,17 @@ class ZZAccessibilityTest : BaseSessionTest() { provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER)) - waitUntilTextTraversed(0, 1) // "L" + nodeId = waitUntilTextTraversed(0, 1) // "L" provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER)) - waitUntilTextTraversed(1, 2) // "o" + nodeId = waitUntilTextTraversed(1, 2) // "o" provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER)) - waitUntilTextTraversed(0, 1) // "L" + nodeId = waitUntilTextTraversed(0, 1) // "L" } @Test fun testMoveByWord() { @@ -463,12 +482,12 @@ class ZZAccessibilityTest : BaseSessionTest() { provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD)) - waitUntilTextTraversed(0, 5) // "Lorem" + nodeId = waitUntilTextTraversed(0, 5) // "Lorem" provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD)) - waitUntilTextTraversed(6, 11) // "ipsum" + nodeId = waitUntilTextTraversed(6, 11) // "ipsum" provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, @@ -496,17 +515,17 @@ class ZZAccessibilityTest : BaseSessionTest() { provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE)) - waitUntilTextTraversed(0, 18) // "Lorem ipsum dolor " + nodeId = waitUntilTextTraversed(0, 18) // "Lorem ipsum dolor " provider.performAction(nodeId, AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, moveByGranularityArguments(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE)) - waitUntilTextTraversed(18, 28) // "sit amet, " + nodeId = 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 " + nodeId = waitUntilTextTraversed(0, 18) // "Lorem ipsum dolor " } @Test fun testHeadings() { 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 8423bf892c92..6b674dbdabb9 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 @@ -271,11 +271,7 @@ public class SessionAccessibility { mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityActivate", data); } else if (granularity > 0) { boolean extendSelection = arguments.getBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN); - data = new GeckoBundle(3); - data.putString("direction", action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY ? "Next" : "Previous"); - data.putInt("granularity", granularity); - data.putBoolean("select", extendSelection); - mSession.getEventDispatcher().dispatch("GeckoView:AccessibilityByGranularity", data); + nativeProvider.navigateText(virtualViewId, granularity, mStartOffset, mEndOffset, action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, extendSelection); } return true; case AccessibilityNodeInfo.ACTION_SET_SELECTION: @@ -548,6 +544,8 @@ public class SessionAccessibility { private int mAccessibilityFocusedNode = 0; // The current node with focus private int mFocusedNode = 0; + private int mStartOffset = -1; + private int mEndOffset = -1; // Viewport cache final SparseArray mViewportCache = new SparseArray<>(); // Focus cache @@ -807,6 +805,8 @@ public class SessionAccessibility { } break; case AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED: + mStartOffset = -1; + mEndOffset = -1; mAccessibilityFocusedNode = sourceId; break; case AccessibilityEvent.TYPE_VIEW_FOCUSED: @@ -816,6 +816,10 @@ public class SessionAccessibility { return; } break; + case AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY: + mStartOffset = event.getFromIndex(); + mEndOffset = event.getToIndex(); + break; } ((ViewParent) mView).requestSendAccessibilityEvent(mView, event); @@ -864,6 +868,9 @@ public class SessionAccessibility { @WrapForJNI(dispatchTo = "gecko") public native void exploreByTouch(int id, float x, float y); + @WrapForJNI(dispatchTo = "gecko") + public native void navigateText(int id, int granularity, int startOffset, int endOffset, boolean forward, boolean select); + @WrapForJNI(calledFrom = "gecko", stubName = "SendEvent") private void sendEventNative(final int eventType, final int sourceId, final int className, final GeckoBundle eventData) { ThreadUtils.postToUiThread(new Runnable() {