Reland "Android a11y bridge sets importantness" (flutter/engine#44589)

The previous pr was reverted due to test failures. The failure was due to API not supported in order android version. The fix is in the second commit.

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
This commit is contained in:
chunhtai
2023-08-10 13:02:12 -07:00
committed by GitHub
parent c3606872fb
commit e7cb4195f2
2 changed files with 165 additions and 1 deletions

View File

@@ -575,6 +575,11 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
sendLatestAccessibilityFlagsToFlutter();
}
@VisibleForTesting
public AccessibilityNodeInfo obtainAccessibilityNodeInfo(View rootView) {
return AccessibilityNodeInfo.obtain(rootView);
}
@VisibleForTesting
public AccessibilityNodeInfo obtainAccessibilityNodeInfo(View rootView, int virtualViewId) {
return AccessibilityNodeInfo.obtain(rootView, virtualViewId);
@@ -616,13 +621,16 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
}
if (virtualViewId == View.NO_ID) {
AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(rootAccessibilityView);
AccessibilityNodeInfo result = obtainAccessibilityNodeInfo(rootAccessibilityView);
rootAccessibilityView.onInitializeAccessibilityNodeInfo(result);
// TODO(mattcarroll): what does it mean for the semantics tree to contain or not contain
// the root node ID?
if (flutterSemanticsTree.containsKey(ROOT_NODE_ID)) {
result.addChild(rootAccessibilityView, ROOT_NODE_ID);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
result.setImportantForAccessibility(false);
}
return result;
}
@@ -653,6 +661,13 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
AccessibilityNodeInfo result =
obtainAccessibilityNodeInfo(rootAccessibilityView, virtualViewId);
// Accessibility Scanner uses isImportantForAccessibility to decide whether to check
// or skip this node.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
result.setImportantForAccessibility(isImportant(semanticsNode));
}
// Work around for https://github.com/flutter/flutter/issues/2101
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
result.setViewIdResourceName("");
@@ -983,6 +998,19 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
return result;
}
private boolean isImportant(SemanticsNode node) {
if (node.hasFlag(Flag.SCOPES_ROUTE)) {
return false;
}
if (node.getValueLabelHint() != null) {
return true;
}
// Return true if the node has had any user action (not including system actions)
return (node.actions & ~systemAction) != 0;
}
/**
* Get the bounds in screen with root FlutterView's offset.
*
@@ -2141,6 +2169,14 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
}
}
// Actions that are triggered by Android OS, as opposed to user-triggered actions.
//
// This int is intended to be use in a bitwise comparison.
static int systemAction =
Action.DID_GAIN_ACCESSIBILITY_FOCUS.value
& Action.DID_LOSE_ACCESSIBILITY_FOCUS.value
& Action.SHOW_ON_SCREEN.value;
// Must match SemanticsFlag in semantics.dart
// https://github.com/flutter/engine/blob/main/lib/ui/semantics.dart
/* Package */ enum Flag {

View File

@@ -49,6 +49,7 @@ import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
import io.flutter.plugin.common.BasicMessageChannel;
import io.flutter.plugin.platform.PlatformViewsAccessibilityDelegate;
import io.flutter.view.AccessibilityBridge.Action;
import io.flutter.view.AccessibilityBridge.Flag;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
@@ -321,6 +322,133 @@ public class AccessibilityBridgeTest {
verify(mockNodeInfo2, times(1)).setTraversalAfter(eq(mockRootView), eq(1));
}
@TargetApi(24)
@Test
public void itSetsRootViewNotImportantForAccessibility() {
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(mockRootView, mockManager, mockViewEmbedder);
ViewParent mockParent = mock(ViewParent.class);
when(mockRootView.getParent()).thenReturn(mockParent);
when(mockManager.isEnabled()).thenReturn(true);
TestSemanticsNode root = new TestSemanticsNode();
root.id = 0;
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
AccessibilityBridge spyAccessibilityBridge = spy(accessibilityBridge);
AccessibilityNodeInfo mockNodeInfo = mock(AccessibilityNodeInfo.class);
when(spyAccessibilityBridge.obtainAccessibilityNodeInfo(mockRootView)).thenReturn(mockNodeInfo);
spyAccessibilityBridge.createAccessibilityNodeInfo(View.NO_ID);
verify(mockNodeInfo, times(1)).setImportantForAccessibility(eq(false));
}
@TargetApi(24)
@Test
public void itSetsNodeImportantForAccessibilityIfItHasContent() {
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(mockRootView, mockManager, mockViewEmbedder);
ViewParent mockParent = mock(ViewParent.class);
when(mockRootView.getParent()).thenReturn(mockParent);
when(mockManager.isEnabled()).thenReturn(true);
TestSemanticsNode root = new TestSemanticsNode();
root.id = 0;
root.label = "some label";
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
AccessibilityBridge spyAccessibilityBridge = spy(accessibilityBridge);
AccessibilityNodeInfo mockNodeInfo = mock(AccessibilityNodeInfo.class);
when(spyAccessibilityBridge.obtainAccessibilityNodeInfo(mockRootView, 0))
.thenReturn(mockNodeInfo);
spyAccessibilityBridge.createAccessibilityNodeInfo(0);
verify(mockNodeInfo, times(1)).setImportantForAccessibility(eq(true));
}
@TargetApi(24)
@Test
public void itSetsNodeImportantForAccessibilityIfItHasActions() {
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(mockRootView, mockManager, mockViewEmbedder);
ViewParent mockParent = mock(ViewParent.class);
when(mockRootView.getParent()).thenReturn(mockParent);
when(mockManager.isEnabled()).thenReturn(true);
TestSemanticsNode root = new TestSemanticsNode();
root.id = 0;
root.addAction(Action.TAP);
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
AccessibilityBridge spyAccessibilityBridge = spy(accessibilityBridge);
AccessibilityNodeInfo mockNodeInfo = mock(AccessibilityNodeInfo.class);
when(spyAccessibilityBridge.obtainAccessibilityNodeInfo(mockRootView, 0))
.thenReturn(mockNodeInfo);
spyAccessibilityBridge.createAccessibilityNodeInfo(0);
verify(mockNodeInfo, times(1)).setImportantForAccessibility(eq(true));
}
@TargetApi(24)
@Test
public void itSetsNodeUnImportantForAccessibilityIfItIsEmpty() {
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(mockRootView, mockManager, mockViewEmbedder);
ViewParent mockParent = mock(ViewParent.class);
when(mockRootView.getParent()).thenReturn(mockParent);
when(mockManager.isEnabled()).thenReturn(true);
TestSemanticsNode root = new TestSemanticsNode();
root.id = 0;
TestSemanticsNode node = new TestSemanticsNode();
node.id = 1;
root.children.add(node);
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
AccessibilityBridge spyAccessibilityBridge = spy(accessibilityBridge);
AccessibilityNodeInfo mockNodeInfo = mock(AccessibilityNodeInfo.class);
when(spyAccessibilityBridge.obtainAccessibilityNodeInfo(mockRootView, 0))
.thenReturn(mockNodeInfo);
spyAccessibilityBridge.createAccessibilityNodeInfo(0);
verify(mockNodeInfo, times(1)).setImportantForAccessibility(eq(false));
AccessibilityNodeInfo mockNodeInfo1 = mock(AccessibilityNodeInfo.class);
when(spyAccessibilityBridge.obtainAccessibilityNodeInfo(mockRootView, 1))
.thenReturn(mockNodeInfo1);
spyAccessibilityBridge.createAccessibilityNodeInfo(1);
verify(mockNodeInfo1, times(1)).setImportantForAccessibility(eq(false));
}
@TargetApi(28)
@Test
public void itSetCutoutInsetBasedonLayoutModeNever() {