Commit 528fea0c authored by Mark Schillaci's avatar Mark Schillaci Committed by Commit Bot

Addressing text selection mode on Android and AccessibilityEvent discrepancies

This CL addresses various coupled bugs in both text selection and non-
default granularity navigation for input fields/contenteditables on
Android. It fixes various issues with improper announcements,
announcements at the wrong time, and text selection not functioning in
general. It also addresses AccessibilityEvent discrepancies under-the-
hood in cases where we appeared to have the correct functionality to
the user, but only by happenstance as we were passing bad data to
TalkBack.

This CL addresses:
Bug 1041649 - Text selection on Android broken/not functioning
Bug 1017466 - Contenteditable word granularity navigation not reading
correctly
Bug 1041587 - Beginning/End of field announcements being read at
wrong index

Bug: 1041649,1017466,1041587
Change-Id: I30a8888e99e90356398d14319fbdf225839f5dac
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1998829
Commit-Queue: Mark Schillaci <mschillaci@google.com>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Cr-Commit-Position: refs/heads/master@{#734171}
parent b66375b9
...@@ -85,8 +85,7 @@ class AndroidGranularityMovementBrowserTest : public ContentBrowserTest { ...@@ -85,8 +85,7 @@ class AndroidGranularityMovementBrowserTest : public ContentBrowserTest {
int previous_end_index = -1; int previous_end_index = -1;
while (manager->NextAtGranularity(granularity, end_index, android_node, while (manager->NextAtGranularity(granularity, end_index, android_node,
&start_index, &end_index)) { &start_index, &end_index)) {
int len = int len = end_index - start_index;
(granularity == GRANULARITY_CHARACTER) ? 1 : end_index - start_index;
base::string16 selection = text.substr(start_index, len); base::string16 selection = text.substr(start_index, len);
if (base::EndsWith(selection, base::ASCIIToUTF16("\n"), if (base::EndsWith(selection, base::ASCIIToUTF16("\n"),
base::CompareCase::INSENSITIVE_ASCII)) base::CompareCase::INSENSITIVE_ASCII))
...@@ -111,8 +110,7 @@ class AndroidGranularityMovementBrowserTest : public ContentBrowserTest { ...@@ -111,8 +110,7 @@ class AndroidGranularityMovementBrowserTest : public ContentBrowserTest {
start_index = end_index; start_index = end_index;
while (manager->PreviousAtGranularity( while (manager->PreviousAtGranularity(
granularity, start_index, android_node, &start_index, &end_index)) { granularity, start_index, android_node, &start_index, &end_index)) {
int len = int len = end_index - start_index;
(granularity == GRANULARITY_CHARACTER) ? 1 : end_index - start_index;
base::string16 selection = text.substr(start_index, len); base::string16 selection = text.substr(start_index, len);
if (base::EndsWith(selection, base::ASCIIToUTF16("\n"), if (base::EndsWith(selection, base::ASCIIToUTF16("\n"),
base::CompareCase::INSENSITIVE_ASCII)) base::CompareCase::INSENSITIVE_ASCII))
......
...@@ -287,8 +287,9 @@ bool BrowserAccessibilityManagerAndroid::NextAtGranularity( ...@@ -287,8 +287,9 @@ bool BrowserAccessibilityManagerAndroid::NextAtGranularity(
base::i18n::UTF16CharIterator iter(text.data(), text.size()); base::i18n::UTF16CharIterator iter(text.data(), text.size());
while (!iter.end() && iter.array_pos() <= cursor_index) while (!iter.end() && iter.array_pos() <= cursor_index)
iter.Advance(); iter.Advance();
*start_index = iter.array_pos();
*end_index = iter.array_pos(); *end_index = iter.array_pos();
iter.Rewind();
*start_index = iter.array_pos();
break; break;
} }
case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_WORD: case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_WORD:
...@@ -335,7 +336,7 @@ bool BrowserAccessibilityManagerAndroid::PreviousAtGranularity( ...@@ -335,7 +336,7 @@ bool BrowserAccessibilityManagerAndroid::PreviousAtGranularity(
iter.Advance(); iter.Advance();
} }
*start_index = previous_index; *start_index = previous_index;
*end_index = previous_index; *end_index = iter.array_pos();
break; break;
} }
case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_WORD: case ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_WORD:
......
...@@ -1066,9 +1066,9 @@ jboolean WebContentsAccessibilityAndroid::NextAtGranularity( ...@@ -1066,9 +1066,9 @@ jboolean WebContentsAccessibilityAndroid::NextAtGranularity(
if (root_manager_->NextAtGranularity(granularity, cursor_index, node, if (root_manager_->NextAtGranularity(granularity, cursor_index, node,
&start_index, &end_index)) { &start_index, &end_index)) {
base::string16 text = node->GetInnerText(); base::string16 text = node->GetInnerText();
Java_WebContentsAccessibilityImpl_finishGranularityMove( Java_WebContentsAccessibilityImpl_finishGranularityMoveNext(
env, obj, base::android::ConvertUTF16ToJavaString(env, text), env, obj, base::android::ConvertUTF16ToJavaString(env, text),
extend_selection, start_index, end_index, true); extend_selection, start_index, end_index);
return true; return true;
} }
return false; return false;
...@@ -1132,10 +1132,10 @@ jboolean WebContentsAccessibilityAndroid::PreviousAtGranularity( ...@@ -1132,10 +1132,10 @@ jboolean WebContentsAccessibilityAndroid::PreviousAtGranularity(
int end_index = -1; int end_index = -1;
if (root_manager_->PreviousAtGranularity(granularity, cursor_index, node, if (root_manager_->PreviousAtGranularity(granularity, cursor_index, node,
&start_index, &end_index)) { &start_index, &end_index)) {
Java_WebContentsAccessibilityImpl_finishGranularityMove( Java_WebContentsAccessibilityImpl_finishGranularityMovePrevious(
env, obj, env, obj,
base::android::ConvertUTF16ToJavaString(env, node->GetInnerText()), base::android::ConvertUTF16ToJavaString(env, node->GetInnerText()),
extend_selection, start_index, end_index, false); extend_selection, start_index, end_index);
return true; return true;
} }
return false; return false;
......
...@@ -137,8 +137,8 @@ class CONTENT_EXPORT WebContentsAccessibilityAndroid ...@@ -137,8 +137,8 @@ class CONTENT_EXPORT WebContentsAccessibilityAndroid
// of our own selection in BrowserAccessibilityManager.java for static // of our own selection in BrowserAccessibilityManager.java for static
// text, but if this is an editable text node, updates the selected text // text, but if this is an editable text node, updates the selected text
// in Blink, too, and either way calls // in Blink, too, and either way calls
// Java_BrowserAccessibilityManager_finishGranularityMove with the // Java_BrowserAccessibilityManager_finishGranularityMove[NEXT/PREVIOUS]
// result. // with the result.
jboolean NextAtGranularity(JNIEnv* env, jboolean NextAtGranularity(JNIEnv* env,
const base::android::JavaParamRef<jobject>& obj, const base::android::JavaParamRef<jobject>& obj,
jint granularity, jint granularity,
......
...@@ -90,20 +90,25 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider ...@@ -90,20 +90,25 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider
private boolean mIsHovering; private boolean mIsHovering;
private int mLastHoverId = View.NO_ID; private int mLastHoverId = View.NO_ID;
protected int mCurrentRootId; protected int mCurrentRootId;
private int[] mTempLocation = new int[2];
protected ViewGroup mView; protected ViewGroup mView;
private boolean mUserHasTouchExplored; private boolean mUserHasTouchExplored;
private boolean mPendingScrollToMakeNodeVisible; private boolean mPendingScrollToMakeNodeVisible;
private boolean mNotifyFrameInfoInitializedCalled; private boolean mNotifyFrameInfoInitializedCalled;
private boolean mAccessibilityEnabledForTesting; private boolean mAccessibilityEnabledForTesting;
private int mSelectionGranularity; private int mSelectionGranularity;
private int mSelectionStartIndex;
private int mSelectionEndIndex;
protected int mAccessibilityFocusId; protected int mAccessibilityFocusId;
protected int mSelectionNodeId; protected int mSelectionNodeId;
private Runnable mSendWindowContentChangedRunnable; private Runnable mSendWindowContentChangedRunnable;
private View mAutofillPopupView; private View mAutofillPopupView;
private CaptioningController mCaptioningController; private CaptioningController mCaptioningController;
private boolean mIsCurrentlyExtendingSelection;
private int mSelectionStart;
private int mCursorIndex;
// Whether or not the next selection event should be fired. We only want to sent one traverse
// and one selection event per granularity move, this ensures no double events while still
// sending events when the user is using other assistive technology (e.g. external keyboard)
private boolean mSuppressNextSelectionEvent;
// Whether native accessibility is allowed. // Whether native accessibility is allowed.
private boolean mNativeAccessibilityAllowed; private boolean mNativeAccessibilityAllowed;
...@@ -692,19 +697,20 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider ...@@ -692,19 +697,20 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider
private void setGranularityAndUpdateSelection(int granularity) { private void setGranularityAndUpdateSelection(int granularity) {
mSelectionGranularity = granularity; mSelectionGranularity = granularity;
if (mSelectionGranularity == NO_GRANULARITY_SELECTED) {
mSelectionStartIndex = -1;
mSelectionEndIndex = -1;
}
if (WebContentsAccessibilityImplJni.get().isEditableText( if (WebContentsAccessibilityImplJni.get().isEditableText(
mNativeObj, WebContentsAccessibilityImpl.this, mAccessibilityFocusId) mNativeObj, WebContentsAccessibilityImpl.this, mAccessibilityFocusId)
&& WebContentsAccessibilityImplJni.get().isFocused( && WebContentsAccessibilityImplJni.get().isFocused(
mNativeObj, WebContentsAccessibilityImpl.this, mAccessibilityFocusId)) { mNativeObj, WebContentsAccessibilityImpl.this, mAccessibilityFocusId)) {
mSelectionStartIndex = // If selection/cursor are "unassigned" (e.g. first user swipe), then assign as needed
WebContentsAccessibilityImplJni.get().getEditableTextSelectionStart( if (mSelectionStart == -1 || mCursorIndex == -1) {
mNativeObj, WebContentsAccessibilityImpl.this, mAccessibilityFocusId); mSelectionStart =
mSelectionEndIndex = WebContentsAccessibilityImplJni.get().getEditableTextSelectionEnd( WebContentsAccessibilityImplJni.get().getEditableTextSelectionStart(
mNativeObj, WebContentsAccessibilityImpl.this, mAccessibilityFocusId); mNativeObj, WebContentsAccessibilityImpl.this,
mAccessibilityFocusId);
mCursorIndex = WebContentsAccessibilityImplJni.get().getEditableTextSelectionEnd(
mNativeObj, WebContentsAccessibilityImpl.this, mAccessibilityFocusId);
}
} }
} }
...@@ -712,10 +718,10 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider ...@@ -712,10 +718,10 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider
if (virtualViewId != mSelectionNodeId) return false; if (virtualViewId != mSelectionNodeId) return false;
setGranularityAndUpdateSelection(granularity); setGranularityAndUpdateSelection(granularity);
// This calls finishGranularityMove when it's done. // This calls finishGranularityMoveNext when it's done.
return WebContentsAccessibilityImplJni.get().nextAtGranularity(mNativeObj, return WebContentsAccessibilityImplJni.get().nextAtGranularity(mNativeObj,
WebContentsAccessibilityImpl.this, mSelectionGranularity, extendSelection, WebContentsAccessibilityImpl.this, mSelectionGranularity, extendSelection,
virtualViewId, mSelectionStartIndex); virtualViewId, mCursorIndex);
} }
private boolean previousAtGranularity( private boolean previousAtGranularity(
...@@ -723,19 +729,20 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider ...@@ -723,19 +729,20 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider
if (virtualViewId != mSelectionNodeId) return false; if (virtualViewId != mSelectionNodeId) return false;
setGranularityAndUpdateSelection(granularity); setGranularityAndUpdateSelection(granularity);
// This calls finishGranularityMove when it's done. // This calls finishGranularityMovePrevious when it's done.
return WebContentsAccessibilityImplJni.get().previousAtGranularity(mNativeObj, return WebContentsAccessibilityImplJni.get().previousAtGranularity(mNativeObj,
WebContentsAccessibilityImpl.this, mSelectionGranularity, extendSelection, WebContentsAccessibilityImpl.this, mSelectionGranularity, extendSelection,
virtualViewId, mSelectionEndIndex); virtualViewId, mCursorIndex);
} }
@CalledByNative @CalledByNative
private void finishGranularityMove(String text, boolean extendSelection, int itemStartIndex, private void finishGranularityMoveNext(
int itemEndIndex, boolean forwards) { String text, boolean extendSelection, int itemStartIndex, int itemEndIndex) {
// Prepare to send both a selection and a traversal event in sequence. // Prepare to send both a selection and a traversal event in sequence.
AccessibilityEvent selectionEvent = buildAccessibilityEvent( AccessibilityEvent selectionEvent = buildAccessibilityEvent(
mSelectionNodeId, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED); mSelectionNodeId, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
if (selectionEvent == null) return; if (selectionEvent == null) return;
AccessibilityEvent traverseEvent = buildAccessibilityEvent(mSelectionNodeId, AccessibilityEvent traverseEvent = buildAccessibilityEvent(mSelectionNodeId,
AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY); AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
if (traverseEvent == null) { if (traverseEvent == null) {
...@@ -743,55 +750,116 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider ...@@ -743,55 +750,116 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider
return; return;
} }
// Update the cursor or selection based on the traversal. If it's an editable // Build selection event dependent on whether user is extending selection or not
// text node, set the real editing cursor too. if (extendSelection) {
if (forwards) { // User started selecting, set the selection start point (only set once per selection)
mSelectionEndIndex = itemEndIndex; if (!mIsCurrentlyExtendingSelection) {
mIsCurrentlyExtendingSelection = true;
mSelectionStart = itemStartIndex;
}
selectionEvent.setFromIndex(mSelectionStart);
selectionEvent.setToIndex(itemEndIndex);
} else { } else {
mSelectionEndIndex = itemStartIndex; // User is no longer selecting, or wasn't originally, reset values
} mIsCurrentlyExtendingSelection = false;
if (!extendSelection) { mSelectionStart = -1;
mSelectionStartIndex = mSelectionEndIndex;
} // Set selection to/from indices to new cursor position, itemEndIndex with forwards nav
if (WebContentsAccessibilityImplJni.get().isEditableText( selectionEvent.setFromIndex(itemEndIndex);
mNativeObj, WebContentsAccessibilityImpl.this, mSelectionNodeId) selectionEvent.setToIndex(itemEndIndex);
&& WebContentsAccessibilityImplJni.get().isFocused(
mNativeObj, WebContentsAccessibilityImpl.this, mSelectionNodeId)) {
WebContentsAccessibilityImplJni.get().setSelection(mNativeObj,
WebContentsAccessibilityImpl.this, mSelectionNodeId, mSelectionStartIndex,
mSelectionEndIndex);
} }
// The selection event's "from" and "to" indices are just a cursor at the focus // Moving forwards, cursor is now at end of granularity move (itemEndIndex)
// end of the movement, or a selection if extendSelection is true. mCursorIndex = itemEndIndex;
selectionEvent.setFromIndex(mSelectionStartIndex);
selectionEvent.setToIndex(mSelectionStartIndex);
selectionEvent.setItemCount(text.length()); selectionEvent.setItemCount(text.length());
// The traverse event's "from" and "to" indices surround the item (e.g. the word, // Call back to native code to update selection
// etc.) with no whitespace. setSelection(selectionEvent);
if (forwards
&& WebContentsAccessibilityImplJni.get().isEditableText( // Build traverse event, set appropriate action
mNativeObj, WebContentsAccessibilityImpl.this, mSelectionNodeId)) { traverseEvent.setFromIndex(itemStartIndex);
traverseEvent.setFromIndex(itemStartIndex - 1); traverseEvent.setToIndex(itemEndIndex);
traverseEvent.setToIndex(itemEndIndex - 1);
} else {
traverseEvent.setFromIndex(itemStartIndex);
traverseEvent.setToIndex(itemEndIndex);
}
traverseEvent.setItemCount(text.length()); traverseEvent.setItemCount(text.length());
traverseEvent.setMovementGranularity(mSelectionGranularity); traverseEvent.setMovementGranularity(mSelectionGranularity);
traverseEvent.setContentDescription(text); traverseEvent.setContentDescription(text);
traverseEvent.setAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
mView.requestSendAccessibilityEvent(mView, selectionEvent);
mView.requestSendAccessibilityEvent(mView, traverseEvent);
// Suppress the next event since we have already sent traverse and selection for this move
mSuppressNextSelectionEvent = true;
}
@CalledByNative
private void finishGranularityMovePrevious(
String text, boolean extendSelection, int itemStartIndex, int itemEndIndex) {
// Prepare to send both a selection and a traversal event in sequence.
AccessibilityEvent selectionEvent = buildAccessibilityEvent(
mSelectionNodeId, AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
if (selectionEvent == null) return;
AccessibilityEvent traverseEvent = buildAccessibilityEvent(mSelectionNodeId,
AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY);
if (traverseEvent == null) {
selectionEvent.recycle();
return;
}
// Build selection event dependent on whether user is extending selection or not
if (extendSelection) {
// User started selecting, set the selection start point (only set once per selection)
if (!mIsCurrentlyExtendingSelection) {
mIsCurrentlyExtendingSelection = true;
mSelectionStart = itemEndIndex;
}
selectionEvent.setFromIndex(mSelectionStart);
selectionEvent.setToIndex(itemStartIndex);
// The traverse event needs to set its associated action that triggered it.
if (forwards) {
traverseEvent.setAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
} else { } else {
traverseEvent.setAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY); // User is no longer selecting, or wasn't originally, reset values
mIsCurrentlyExtendingSelection = false;
mSelectionStart = -1;
// Set selection to/from indices to new cursor position, itemStartIndex with back nav
selectionEvent.setFromIndex(itemStartIndex);
selectionEvent.setToIndex(itemStartIndex);
} }
// Moving backwards, cursor is now at the start of the granularity move (itemStartIndex)
mCursorIndex = itemStartIndex;
selectionEvent.setItemCount(text.length());
// Call back to native code to update selection
setSelection(selectionEvent);
// Build traverse event, set appropriate action
traverseEvent.setFromIndex(itemStartIndex);
traverseEvent.setToIndex(itemEndIndex);
traverseEvent.setItemCount(text.length());
traverseEvent.setMovementGranularity(mSelectionGranularity);
traverseEvent.setContentDescription(text);
traverseEvent.setAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
mView.requestSendAccessibilityEvent(mView, selectionEvent); mView.requestSendAccessibilityEvent(mView, selectionEvent);
mView.requestSendAccessibilityEvent(mView, traverseEvent); mView.requestSendAccessibilityEvent(mView, traverseEvent);
// Suppress the next event since we have already sent traverse and selection for this move
mSuppressNextSelectionEvent = true;
}
private void setSelection(AccessibilityEvent selectionEvent) {
if (WebContentsAccessibilityImplJni.get().isEditableText(
mNativeObj, WebContentsAccessibilityImpl.this, mSelectionNodeId)
&& WebContentsAccessibilityImplJni.get().isFocused(
mNativeObj, WebContentsAccessibilityImpl.this, mSelectionNodeId)) {
WebContentsAccessibilityImplJni.get().setSelection(mNativeObj,
WebContentsAccessibilityImpl.this, mSelectionNodeId,
selectionEvent.getFromIndex(), selectionEvent.getToIndex());
}
} }
private boolean scrollForward(int virtualViewId) { private boolean scrollForward(int virtualViewId) {
...@@ -829,9 +897,10 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider ...@@ -829,9 +897,10 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider
// focus is on the edit field. Granularity move needs to know where the input focus is. // focus is on the edit field. Granularity move needs to know where the input focus is.
mSelectionNodeId = mAccessibilityFocusId; mSelectionNodeId = mAccessibilityFocusId;
mSelectionGranularity = NO_GRANULARITY_SELECTED; mSelectionGranularity = NO_GRANULARITY_SELECTED;
mSelectionStartIndex = -1; mIsCurrentlyExtendingSelection = false;
mSelectionEndIndex = WebContentsAccessibilityImplJni.get().getTextLength( mSelectionStart = -1;
mNativeObj, WebContentsAccessibilityImpl.this, newAccessibilityFocusId); mCursorIndex = -1;
mSuppressNextSelectionEvent = false;
if (WebContentsAccessibilityImplJni.get().isAutofillPopupNode( if (WebContentsAccessibilityImplJni.get().isAutofillPopupNode(
mNativeObj, WebContentsAccessibilityImpl.this, mAccessibilityFocusId)) { mNativeObj, WebContentsAccessibilityImpl.this, mAccessibilityFocusId)) {
...@@ -893,6 +962,13 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider ...@@ -893,6 +962,13 @@ public class WebContentsAccessibilityImpl extends AccessibilityNodeProvider
return; return;
} }
// Do not send an event when we want to suppress this event, update flag for next event
if (mSuppressNextSelectionEvent
&& eventType == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) {
mSuppressNextSelectionEvent = false;
return;
}
AccessibilityEvent event = buildAccessibilityEvent(virtualViewId, eventType); AccessibilityEvent event = buildAccessibilityEvent(virtualViewId, eventType);
if (event != null) { if (event != null) {
mView.requestSendAccessibilityEvent(mView, event); mView.requestSendAccessibilityEvent(mView, event);
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment