Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Android a11y bridge sets importantness #44452

Merged
merged 9 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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