mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Reland "Fixes android voice access delete text, redo, and undo action" (flutter/engine#25289)
* Reland "Fixes android voice access delete text, redo, and undo actions. (#25050)" This reverts commit b1b20aedd68a6cfea50b61e4f4aca74c3909407c. * fix condition
This commit is contained in:
parent
058ebe70e5
commit
0b40ddfde0
@ -34,6 +34,8 @@ import io.flutter.util.Predicate;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Bridge between Android's OS accessibility system and Flutter's accessibility system.
|
||||
@ -633,8 +635,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
}
|
||||
|
||||
// These are non-ops on older devices. Attempting to interact with the text will cause Talkback
|
||||
// to read the
|
||||
// contents of the text box instead.
|
||||
// to read the contents of the text box instead.
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
if (semanticsNode.hasAction(Action.SET_SELECTION)) {
|
||||
result.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION);
|
||||
@ -650,6 +651,13 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// Set text API isn't available until API 21.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (semanticsNode.hasAction(Action.SET_TEXT)) {
|
||||
result.addAction(AccessibilityNodeInfo.ACTION_SET_TEXT);
|
||||
}
|
||||
}
|
||||
|
||||
if (semanticsNode.hasFlag(Flag.IS_BUTTON) || semanticsNode.hasFlag(Flag.IS_LINK)) {
|
||||
result.setClassName("android.widget.Button");
|
||||
}
|
||||
@ -1034,6 +1042,12 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
}
|
||||
accessibilityChannel.dispatchSemanticsAction(
|
||||
virtualViewId, Action.SET_SELECTION, selection);
|
||||
// The voice access expects the semantics node to update immediately. We update the
|
||||
// semantics node based on prediction. If the result is incorrect, it will be updated in
|
||||
// the next frame.
|
||||
SemanticsNode node = flutterSemanticsTree.get(virtualViewId);
|
||||
node.textSelectionBase = selection.get("base");
|
||||
node.textSelectionExtent = selection.get("extent");
|
||||
return true;
|
||||
}
|
||||
case AccessibilityNodeInfo.ACTION_COPY:
|
||||
@ -1064,7 +1078,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
return false;
|
||||
}
|
||||
return performSetText(virtualViewId, arguments);
|
||||
return performSetText(semanticsNode, virtualViewId, arguments);
|
||||
}
|
||||
default:
|
||||
// might be a custom accessibility accessibilityAction.
|
||||
@ -1094,6 +1108,9 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
|
||||
final boolean extendSelection =
|
||||
arguments.getBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN);
|
||||
// The voice access expects the semantics node to update immediately. We update the semantics
|
||||
// node based on prediction. If the result is incorrect, it will be updated in the next frame.
|
||||
predictCursorMovement(semanticsNode, granularity, forward, extendSelection);
|
||||
switch (granularity) {
|
||||
case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER:
|
||||
{
|
||||
@ -1121,23 +1138,99 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE:
|
||||
case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH:
|
||||
case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void predictCursorMovement(
|
||||
@NonNull SemanticsNode node, int granularity, boolean forward, boolean extendSelection) {
|
||||
if (node.textSelectionExtent < 0 || node.textSelectionBase < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (granularity) {
|
||||
case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER:
|
||||
if (forward && node.textSelectionExtent < node.value.length()) {
|
||||
node.textSelectionExtent += 1;
|
||||
} else if (!forward && node.textSelectionExtent > 0) {
|
||||
node.textSelectionExtent -= 1;
|
||||
}
|
||||
break;
|
||||
case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD:
|
||||
if (forward && node.textSelectionExtent < node.value.length()) {
|
||||
Pattern pattern = Pattern.compile("\\p{L}(\\b)");
|
||||
Matcher result = pattern.matcher(node.value.substring(node.textSelectionExtent));
|
||||
// we discard the first result because we want to find the "next" word
|
||||
result.find();
|
||||
if (result.find()) {
|
||||
node.textSelectionExtent += result.start(1);
|
||||
} else {
|
||||
node.textSelectionExtent = node.value.length();
|
||||
}
|
||||
} else if (!forward && node.textSelectionExtent > 0) {
|
||||
// Finds last beginning of the word boundary.
|
||||
Pattern pattern = Pattern.compile("(?s:.*)(\\b)\\p{L}");
|
||||
Matcher result = pattern.matcher(node.value.substring(0, node.textSelectionExtent));
|
||||
if (result.find()) {
|
||||
node.textSelectionExtent = result.start(1);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE:
|
||||
if (forward && node.textSelectionExtent < node.value.length()) {
|
||||
// Finds the next new line.
|
||||
Pattern pattern = Pattern.compile("(?!^)(\\n)");
|
||||
Matcher result = pattern.matcher(node.value.substring(node.textSelectionExtent));
|
||||
if (result.find()) {
|
||||
node.textSelectionExtent += result.start(1);
|
||||
} else {
|
||||
node.textSelectionExtent = node.value.length();
|
||||
}
|
||||
} else if (!forward && node.textSelectionExtent > 0) {
|
||||
// Finds the last new line.
|
||||
Pattern pattern = Pattern.compile("(?s:.*)(\\n)");
|
||||
Matcher result = pattern.matcher(node.value.substring(0, node.textSelectionExtent));
|
||||
if (result.find()) {
|
||||
node.textSelectionExtent = result.start(1);
|
||||
} else {
|
||||
node.textSelectionExtent = 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH:
|
||||
case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE:
|
||||
if (forward) {
|
||||
node.textSelectionExtent = node.value.length();
|
||||
} else {
|
||||
node.textSelectionExtent = 0;
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!extendSelection) {
|
||||
node.textSelectionBase = node.textSelectionExtent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the responsibilities of {@link #performAction(int, int, Bundle)} for the specific
|
||||
* scenario of cursor movement.
|
||||
*/
|
||||
@TargetApi(21)
|
||||
@RequiresApi(21)
|
||||
private boolean performSetText(int virtualViewId, @NonNull Bundle arguments) {
|
||||
private boolean performSetText(SemanticsNode node, int virtualViewId, @NonNull Bundle arguments) {
|
||||
String newText = "";
|
||||
if (arguments != null
|
||||
&& arguments.containsKey(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE)) {
|
||||
newText = arguments.getString(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE);
|
||||
}
|
||||
accessibilityChannel.dispatchSemanticsAction(virtualViewId, Action.SET_TEXT, newText);
|
||||
// The voice access expects the semantics node to update immediately. We update the semantics
|
||||
// node based on prediction. If the result is incorrect, it will be updated in the next frame.
|
||||
node.value = newText;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ package io.flutter.view;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.anyInt;
|
||||
@ -372,6 +373,357 @@ public class AccessibilityBridgeTest {
|
||||
.dispatchSemanticsAction(1, AccessibilityBridge.Action.SET_TEXT, expectedText);
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
@Test
|
||||
public void itCanPredictSetText() {
|
||||
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
|
||||
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
|
||||
AccessibilityManager mockManager = mock(AccessibilityManager.class);
|
||||
View mockRootView = mock(View.class);
|
||||
Context context = mock(Context.class);
|
||||
when(mockRootView.getContext()).thenReturn(context);
|
||||
when(context.getPackageName()).thenReturn("test");
|
||||
AccessibilityBridge accessibilityBridge =
|
||||
setUpBridge(
|
||||
/*rootAccessibilityView=*/ mockRootView,
|
||||
/*accessibilityChannel=*/ mockChannel,
|
||||
/*accessibilityManager=*/ mockManager,
|
||||
/*contentResolver=*/ null,
|
||||
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
|
||||
/*platformViewsAccessibilityDelegate=*/ null);
|
||||
|
||||
ViewParent mockParent = mock(ViewParent.class);
|
||||
when(mockRootView.getParent()).thenReturn(mockParent);
|
||||
when(mockManager.isEnabled()).thenReturn(true);
|
||||
|
||||
TestSemanticsNode root = new TestSemanticsNode();
|
||||
root.id = 0;
|
||||
TestSemanticsNode node1 = new TestSemanticsNode();
|
||||
node1.id = 1;
|
||||
node1.addFlag(AccessibilityBridge.Flag.IS_TEXT_FIELD);
|
||||
root.children.add(node1);
|
||||
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
|
||||
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
|
||||
Bundle bundle = new Bundle();
|
||||
String expectedText = "some string";
|
||||
bundle.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, expectedText);
|
||||
accessibilityBridge.performAction(1, AccessibilityNodeInfo.ACTION_SET_TEXT, bundle);
|
||||
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(1);
|
||||
assertEquals(nodeInfo.getText(), expectedText);
|
||||
}
|
||||
|
||||
@TargetApi(21)
|
||||
@Test
|
||||
public void itCanCreateAccessibilityNodeInfoWithSetText() {
|
||||
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
|
||||
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
|
||||
AccessibilityManager mockManager = mock(AccessibilityManager.class);
|
||||
View mockRootView = mock(View.class);
|
||||
Context context = mock(Context.class);
|
||||
when(mockRootView.getContext()).thenReturn(context);
|
||||
when(context.getPackageName()).thenReturn("test");
|
||||
AccessibilityBridge accessibilityBridge =
|
||||
setUpBridge(
|
||||
/*rootAccessibilityView=*/ mockRootView,
|
||||
/*accessibilityChannel=*/ mockChannel,
|
||||
/*accessibilityManager=*/ mockManager,
|
||||
/*contentResolver=*/ null,
|
||||
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
|
||||
/*platformViewsAccessibilityDelegate=*/ null);
|
||||
|
||||
ViewParent mockParent = mock(ViewParent.class);
|
||||
when(mockRootView.getParent()).thenReturn(mockParent);
|
||||
when(mockManager.isEnabled()).thenReturn(true);
|
||||
|
||||
TestSemanticsNode root = new TestSemanticsNode();
|
||||
root.id = 0;
|
||||
TestSemanticsNode node1 = new TestSemanticsNode();
|
||||
node1.id = 1;
|
||||
node1.addFlag(AccessibilityBridge.Flag.IS_TEXT_FIELD);
|
||||
node1.addAction(AccessibilityBridge.Action.SET_TEXT);
|
||||
root.children.add(node1);
|
||||
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
|
||||
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
|
||||
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(1);
|
||||
List<AccessibilityNodeInfo.AccessibilityAction> actions = nodeInfo.getActionList();
|
||||
assertTrue(actions.contains(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itCanPredictSetSelection() {
|
||||
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
|
||||
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
|
||||
AccessibilityManager mockManager = mock(AccessibilityManager.class);
|
||||
View mockRootView = mock(View.class);
|
||||
Context context = mock(Context.class);
|
||||
when(mockRootView.getContext()).thenReturn(context);
|
||||
when(context.getPackageName()).thenReturn("test");
|
||||
AccessibilityBridge accessibilityBridge =
|
||||
setUpBridge(
|
||||
/*rootAccessibilityView=*/ mockRootView,
|
||||
/*accessibilityChannel=*/ mockChannel,
|
||||
/*accessibilityManager=*/ mockManager,
|
||||
/*contentResolver=*/ null,
|
||||
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
|
||||
/*platformViewsAccessibilityDelegate=*/ null);
|
||||
|
||||
ViewParent mockParent = mock(ViewParent.class);
|
||||
when(mockRootView.getParent()).thenReturn(mockParent);
|
||||
when(mockManager.isEnabled()).thenReturn(true);
|
||||
|
||||
TestSemanticsNode root = new TestSemanticsNode();
|
||||
root.id = 0;
|
||||
TestSemanticsNode node1 = new TestSemanticsNode();
|
||||
node1.id = 1;
|
||||
node1.value = "some text";
|
||||
node1.textSelectionBase = -1;
|
||||
node1.textSelectionExtent = -1;
|
||||
node1.addFlag(AccessibilityBridge.Flag.IS_TEXT_FIELD);
|
||||
root.children.add(node1);
|
||||
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
|
||||
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
|
||||
Bundle bundle = new Bundle();
|
||||
int expectedStart = 1;
|
||||
int expectedEnd = 3;
|
||||
bundle.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, expectedStart);
|
||||
bundle.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, expectedEnd);
|
||||
accessibilityBridge.performAction(1, AccessibilityNodeInfo.ACTION_SET_SELECTION, bundle);
|
||||
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(1);
|
||||
assertEquals(nodeInfo.getTextSelectionStart(), expectedStart);
|
||||
assertEquals(nodeInfo.getTextSelectionEnd(), expectedEnd);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itCanPredictCursorMovementsWithGranularityWord() {
|
||||
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
|
||||
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
|
||||
AccessibilityManager mockManager = mock(AccessibilityManager.class);
|
||||
View mockRootView = mock(View.class);
|
||||
Context context = mock(Context.class);
|
||||
when(mockRootView.getContext()).thenReturn(context);
|
||||
when(context.getPackageName()).thenReturn("test");
|
||||
AccessibilityBridge accessibilityBridge =
|
||||
setUpBridge(
|
||||
/*rootAccessibilityView=*/ mockRootView,
|
||||
/*accessibilityChannel=*/ mockChannel,
|
||||
/*accessibilityManager=*/ mockManager,
|
||||
/*contentResolver=*/ null,
|
||||
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
|
||||
/*platformViewsAccessibilityDelegate=*/ null);
|
||||
|
||||
ViewParent mockParent = mock(ViewParent.class);
|
||||
when(mockRootView.getParent()).thenReturn(mockParent);
|
||||
when(mockManager.isEnabled()).thenReturn(true);
|
||||
|
||||
TestSemanticsNode root = new TestSemanticsNode();
|
||||
root.id = 0;
|
||||
TestSemanticsNode node1 = new TestSemanticsNode();
|
||||
node1.id = 1;
|
||||
node1.value = "some text";
|
||||
node1.textSelectionBase = 0;
|
||||
node1.textSelectionExtent = 0;
|
||||
node1.addFlag(AccessibilityBridge.Flag.IS_TEXT_FIELD);
|
||||
root.children.add(node1);
|
||||
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
|
||||
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(
|
||||
AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
|
||||
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD);
|
||||
bundle.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, false);
|
||||
accessibilityBridge.performAction(
|
||||
1, AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, bundle);
|
||||
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(1);
|
||||
// The seletction should be at the end of 'text'
|
||||
assertEquals(nodeInfo.getTextSelectionStart(), 9);
|
||||
assertEquals(nodeInfo.getTextSelectionEnd(), 9);
|
||||
|
||||
bundle = new Bundle();
|
||||
bundle.putInt(
|
||||
AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
|
||||
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD);
|
||||
bundle.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, false);
|
||||
accessibilityBridge.performAction(
|
||||
1, AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, bundle);
|
||||
nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(1);
|
||||
// The seletction should be go to beginning of 'text'.
|
||||
assertEquals(nodeInfo.getTextSelectionStart(), 5);
|
||||
assertEquals(nodeInfo.getTextSelectionEnd(), 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itCanPredictCursorMovementsWithGranularityWordUnicode() {
|
||||
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
|
||||
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
|
||||
AccessibilityManager mockManager = mock(AccessibilityManager.class);
|
||||
View mockRootView = mock(View.class);
|
||||
Context context = mock(Context.class);
|
||||
when(mockRootView.getContext()).thenReturn(context);
|
||||
when(context.getPackageName()).thenReturn("test");
|
||||
AccessibilityBridge accessibilityBridge =
|
||||
setUpBridge(
|
||||
/*rootAccessibilityView=*/ mockRootView,
|
||||
/*accessibilityChannel=*/ mockChannel,
|
||||
/*accessibilityManager=*/ mockManager,
|
||||
/*contentResolver=*/ null,
|
||||
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
|
||||
/*platformViewsAccessibilityDelegate=*/ null);
|
||||
|
||||
ViewParent mockParent = mock(ViewParent.class);
|
||||
when(mockRootView.getParent()).thenReturn(mockParent);
|
||||
when(mockManager.isEnabled()).thenReturn(true);
|
||||
|
||||
TestSemanticsNode root = new TestSemanticsNode();
|
||||
root.id = 0;
|
||||
TestSemanticsNode node1 = new TestSemanticsNode();
|
||||
node1.id = 1;
|
||||
node1.value = "你 好 嗎";
|
||||
node1.textSelectionBase = 0;
|
||||
node1.textSelectionExtent = 0;
|
||||
node1.addFlag(AccessibilityBridge.Flag.IS_TEXT_FIELD);
|
||||
root.children.add(node1);
|
||||
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
|
||||
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(
|
||||
AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
|
||||
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD);
|
||||
bundle.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, false);
|
||||
accessibilityBridge.performAction(
|
||||
1, AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, bundle);
|
||||
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(1);
|
||||
// The seletction should be at the end of '好'
|
||||
assertEquals(nodeInfo.getTextSelectionStart(), 3);
|
||||
assertEquals(nodeInfo.getTextSelectionEnd(), 3);
|
||||
|
||||
bundle = new Bundle();
|
||||
bundle.putInt(
|
||||
AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
|
||||
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD);
|
||||
bundle.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, false);
|
||||
accessibilityBridge.performAction(
|
||||
1, AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, bundle);
|
||||
nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(1);
|
||||
// The seletction should be go to beginning of '好'.
|
||||
assertEquals(nodeInfo.getTextSelectionStart(), 2);
|
||||
assertEquals(nodeInfo.getTextSelectionEnd(), 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itCanPredictCursorMovementsWithGranularityLine() {
|
||||
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
|
||||
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
|
||||
AccessibilityManager mockManager = mock(AccessibilityManager.class);
|
||||
View mockRootView = mock(View.class);
|
||||
Context context = mock(Context.class);
|
||||
when(mockRootView.getContext()).thenReturn(context);
|
||||
when(context.getPackageName()).thenReturn("test");
|
||||
AccessibilityBridge accessibilityBridge =
|
||||
setUpBridge(
|
||||
/*rootAccessibilityView=*/ mockRootView,
|
||||
/*accessibilityChannel=*/ mockChannel,
|
||||
/*accessibilityManager=*/ mockManager,
|
||||
/*contentResolver=*/ null,
|
||||
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
|
||||
/*platformViewsAccessibilityDelegate=*/ null);
|
||||
|
||||
ViewParent mockParent = mock(ViewParent.class);
|
||||
when(mockRootView.getParent()).thenReturn(mockParent);
|
||||
when(mockManager.isEnabled()).thenReturn(true);
|
||||
|
||||
TestSemanticsNode root = new TestSemanticsNode();
|
||||
root.id = 0;
|
||||
TestSemanticsNode node1 = new TestSemanticsNode();
|
||||
node1.id = 1;
|
||||
node1.value = "How are you\nI am fine\nThank you";
|
||||
// Selection is at the second line.
|
||||
node1.textSelectionBase = 14;
|
||||
node1.textSelectionExtent = 14;
|
||||
node1.addFlag(AccessibilityBridge.Flag.IS_TEXT_FIELD);
|
||||
root.children.add(node1);
|
||||
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
|
||||
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(
|
||||
AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
|
||||
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE);
|
||||
bundle.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, false);
|
||||
accessibilityBridge.performAction(
|
||||
1, AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, bundle);
|
||||
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(1);
|
||||
// The seletction should be at the beginning of the third line.
|
||||
assertEquals(nodeInfo.getTextSelectionStart(), 21);
|
||||
assertEquals(nodeInfo.getTextSelectionEnd(), 21);
|
||||
|
||||
bundle = new Bundle();
|
||||
bundle.putInt(
|
||||
AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
|
||||
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE);
|
||||
bundle.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, false);
|
||||
accessibilityBridge.performAction(
|
||||
1, AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, bundle);
|
||||
nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(1);
|
||||
// The seletction should be at the beginning of the second line.
|
||||
assertEquals(nodeInfo.getTextSelectionStart(), 11);
|
||||
assertEquals(nodeInfo.getTextSelectionEnd(), 11);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itCanPredictCursorMovementsWithGranularityCharacter() {
|
||||
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
|
||||
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
|
||||
AccessibilityManager mockManager = mock(AccessibilityManager.class);
|
||||
View mockRootView = mock(View.class);
|
||||
Context context = mock(Context.class);
|
||||
when(mockRootView.getContext()).thenReturn(context);
|
||||
when(context.getPackageName()).thenReturn("test");
|
||||
AccessibilityBridge accessibilityBridge =
|
||||
setUpBridge(
|
||||
/*rootAccessibilityView=*/ mockRootView,
|
||||
/*accessibilityChannel=*/ mockChannel,
|
||||
/*accessibilityManager=*/ mockManager,
|
||||
/*contentResolver=*/ null,
|
||||
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
|
||||
/*platformViewsAccessibilityDelegate=*/ null);
|
||||
|
||||
ViewParent mockParent = mock(ViewParent.class);
|
||||
when(mockRootView.getParent()).thenReturn(mockParent);
|
||||
when(mockManager.isEnabled()).thenReturn(true);
|
||||
|
||||
TestSemanticsNode root = new TestSemanticsNode();
|
||||
root.id = 0;
|
||||
TestSemanticsNode node1 = new TestSemanticsNode();
|
||||
node1.id = 1;
|
||||
node1.value = "some text";
|
||||
node1.textSelectionBase = 0;
|
||||
node1.textSelectionExtent = 0;
|
||||
node1.addFlag(AccessibilityBridge.Flag.IS_TEXT_FIELD);
|
||||
root.children.add(node1);
|
||||
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
|
||||
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(
|
||||
AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
|
||||
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER);
|
||||
bundle.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, false);
|
||||
accessibilityBridge.performAction(
|
||||
1, AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, bundle);
|
||||
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(1);
|
||||
assertEquals(nodeInfo.getTextSelectionStart(), 1);
|
||||
assertEquals(nodeInfo.getTextSelectionEnd(), 1);
|
||||
|
||||
bundle = new Bundle();
|
||||
bundle.putInt(
|
||||
AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT,
|
||||
AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER);
|
||||
bundle.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, false);
|
||||
accessibilityBridge.performAction(
|
||||
1, AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, bundle);
|
||||
nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(1);
|
||||
assertEquals(nodeInfo.getTextSelectionStart(), 0);
|
||||
assertEquals(nodeInfo.getTextSelectionEnd(), 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void itAnnouncesWhiteSpaceWhenNoNamesRoute() {
|
||||
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
|
||||
@ -600,6 +952,10 @@ public class AccessibilityBridgeTest {
|
||||
flags |= flag.value;
|
||||
}
|
||||
|
||||
void addAction(AccessibilityBridge.Action action) {
|
||||
actions |= action.value;
|
||||
}
|
||||
|
||||
// These fields are declared in the order they should be
|
||||
// encoded.
|
||||
int id = 0;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user