Skip to content

Commit

Permalink
Android a11y bridge sets importantness (flutter#44452)
Browse files Browse the repository at this point in the history
Accessibility scanner uses isImportantForAccessibility to decide whether to scan the node. If not set, the isImportantForAccessibility is default to false, thus skips all node except for the rootview which defaults to true.

fixes flutter/flutter#39531

[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
  • Loading branch information
chunhtai authored Aug 9, 2023
1 parent 5081fac commit 6ca60a8
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 1 deletion.
34 changes: 33 additions & 1 deletion shell/platform/android/io/flutter/view/AccessibilityBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,11 @@ private void setBoldTextFlag() {
sendLatestAccessibilityFlagsToFlutter();
}

@VisibleForTesting
public AccessibilityNodeInfo obtainAccessibilityNodeInfo(View rootView) {
return AccessibilityNodeInfo.obtain(rootView);
}

@VisibleForTesting
public AccessibilityNodeInfo obtainAccessibilityNodeInfo(View rootView, int virtualViewId) {
return AccessibilityNodeInfo.obtain(rootView, virtualViewId);
Expand Down Expand Up @@ -616,13 +621,14 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
}

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);
}
result.setImportantForAccessibility(false);
return result;
}

Expand Down Expand Up @@ -653,6 +659,11 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {

AccessibilityNodeInfo result =
obtainAccessibilityNodeInfo(rootAccessibilityView, virtualViewId);

// Accessibility Scanner uses isImportantForAccessibility to decide whether to check
// or skip this node.
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("");
Expand Down Expand Up @@ -983,6 +994,19 @@ && shouldSetCollectionInfo(semanticsNode)) {
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.
*
Expand Down Expand Up @@ -2141,6 +2165,14 @@ public enum Action {
}
}

// 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
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;
Expand Down Expand Up @@ -321,6 +322,129 @@ public void itSetsTraversalAfter() {
verify(mockNodeInfo2, times(1)).setTraversalAfter(eq(mockRootView), eq(1));
}

@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));
}

@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));
}

@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));
}

@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() {
Expand Down

0 comments on commit 6ca60a8

Please sign in to comment.