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

Commit 6ca60a8

Browse files
authored
Android a11y bridge sets importantness (#44452)
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
1 parent 5081fac commit 6ca60a8

File tree

2 files changed

+157
-1
lines changed

2 files changed

+157
-1
lines changed

shell/platform/android/io/flutter/view/AccessibilityBridge.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,11 @@ private void setBoldTextFlag() {
575575
sendLatestAccessibilityFlagsToFlutter();
576576
}
577577

578+
@VisibleForTesting
579+
public AccessibilityNodeInfo obtainAccessibilityNodeInfo(View rootView) {
580+
return AccessibilityNodeInfo.obtain(rootView);
581+
}
582+
578583
@VisibleForTesting
579584
public AccessibilityNodeInfo obtainAccessibilityNodeInfo(View rootView, int virtualViewId) {
580585
return AccessibilityNodeInfo.obtain(rootView, virtualViewId);
@@ -616,13 +621,14 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
616621
}
617622

618623
if (virtualViewId == View.NO_ID) {
619-
AccessibilityNodeInfo result = AccessibilityNodeInfo.obtain(rootAccessibilityView);
624+
AccessibilityNodeInfo result = obtainAccessibilityNodeInfo(rootAccessibilityView);
620625
rootAccessibilityView.onInitializeAccessibilityNodeInfo(result);
621626
// TODO(mattcarroll): what does it mean for the semantics tree to contain or not contain
622627
// the root node ID?
623628
if (flutterSemanticsTree.containsKey(ROOT_NODE_ID)) {
624629
result.addChild(rootAccessibilityView, ROOT_NODE_ID);
625630
}
631+
result.setImportantForAccessibility(false);
626632
return result;
627633
}
628634

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

654660
AccessibilityNodeInfo result =
655661
obtainAccessibilityNodeInfo(rootAccessibilityView, virtualViewId);
662+
663+
// Accessibility Scanner uses isImportantForAccessibility to decide whether to check
664+
// or skip this node.
665+
result.setImportantForAccessibility(isImportant(semanticsNode));
666+
656667
// Work around for https://github.com/flutter/flutter/issues/2101
657668
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
658669
result.setViewIdResourceName("");
@@ -983,6 +994,19 @@ && shouldSetCollectionInfo(semanticsNode)) {
983994
return result;
984995
}
985996

997+
private boolean isImportant(SemanticsNode node) {
998+
if (node.hasFlag(Flag.SCOPES_ROUTE)) {
999+
return false;
1000+
}
1001+
1002+
if (node.getValueLabelHint() != null) {
1003+
return true;
1004+
}
1005+
1006+
// Return true if the node has had any user action (not including system actions)
1007+
return (node.actions & ~systemAction) != 0;
1008+
}
1009+
9861010
/**
9871011
* Get the bounds in screen with root FlutterView's offset.
9881012
*
@@ -2141,6 +2165,14 @@ public enum Action {
21412165
}
21422166
}
21432167

2168+
// Actions that are triggered by Android OS, as opposed to user-triggered actions.
2169+
//
2170+
// This int is intended to be use in a bitwise comparison.
2171+
static int systemAction =
2172+
Action.DID_GAIN_ACCESSIBILITY_FOCUS.value
2173+
& Action.DID_LOSE_ACCESSIBILITY_FOCUS.value
2174+
& Action.SHOW_ON_SCREEN.value;
2175+
21442176
// Must match SemanticsFlag in semantics.dart
21452177
// https://github.com/flutter/engine/blob/main/lib/ui/semantics.dart
21462178
/* Package */ enum Flag {

shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import io.flutter.embedding.engine.systemchannels.AccessibilityChannel;
5050
import io.flutter.plugin.common.BasicMessageChannel;
5151
import io.flutter.plugin.platform.PlatformViewsAccessibilityDelegate;
52+
import io.flutter.view.AccessibilityBridge.Action;
5253
import io.flutter.view.AccessibilityBridge.Flag;
5354
import java.nio.ByteBuffer;
5455
import java.nio.charset.Charset;
@@ -321,6 +322,129 @@ public void itSetsTraversalAfter() {
321322
verify(mockNodeInfo2, times(1)).setTraversalAfter(eq(mockRootView), eq(1));
322323
}
323324

325+
@Test
326+
public void itSetsRootViewNotImportantForAccessibility() {
327+
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
328+
AccessibilityManager mockManager = mock(AccessibilityManager.class);
329+
View mockRootView = mock(View.class);
330+
Context context = mock(Context.class);
331+
when(mockRootView.getContext()).thenReturn(context);
332+
when(context.getPackageName()).thenReturn("test");
333+
AccessibilityBridge accessibilityBridge =
334+
setUpBridge(mockRootView, mockManager, mockViewEmbedder);
335+
ViewParent mockParent = mock(ViewParent.class);
336+
when(mockRootView.getParent()).thenReturn(mockParent);
337+
when(mockManager.isEnabled()).thenReturn(true);
338+
339+
TestSemanticsNode root = new TestSemanticsNode();
340+
root.id = 0;
341+
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
342+
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
343+
344+
AccessibilityBridge spyAccessibilityBridge = spy(accessibilityBridge);
345+
AccessibilityNodeInfo mockNodeInfo = mock(AccessibilityNodeInfo.class);
346+
347+
when(spyAccessibilityBridge.obtainAccessibilityNodeInfo(mockRootView)).thenReturn(mockNodeInfo);
348+
spyAccessibilityBridge.createAccessibilityNodeInfo(View.NO_ID);
349+
verify(mockNodeInfo, times(1)).setImportantForAccessibility(eq(false));
350+
}
351+
352+
@Test
353+
public void itSetsNodeImportantForAccessibilityIfItHasContent() {
354+
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
355+
AccessibilityManager mockManager = mock(AccessibilityManager.class);
356+
View mockRootView = mock(View.class);
357+
Context context = mock(Context.class);
358+
when(mockRootView.getContext()).thenReturn(context);
359+
when(context.getPackageName()).thenReturn("test");
360+
AccessibilityBridge accessibilityBridge =
361+
setUpBridge(mockRootView, mockManager, mockViewEmbedder);
362+
ViewParent mockParent = mock(ViewParent.class);
363+
when(mockRootView.getParent()).thenReturn(mockParent);
364+
when(mockManager.isEnabled()).thenReturn(true);
365+
366+
TestSemanticsNode root = new TestSemanticsNode();
367+
root.id = 0;
368+
root.label = "some label";
369+
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
370+
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
371+
372+
AccessibilityBridge spyAccessibilityBridge = spy(accessibilityBridge);
373+
AccessibilityNodeInfo mockNodeInfo = mock(AccessibilityNodeInfo.class);
374+
375+
when(spyAccessibilityBridge.obtainAccessibilityNodeInfo(mockRootView, 0))
376+
.thenReturn(mockNodeInfo);
377+
spyAccessibilityBridge.createAccessibilityNodeInfo(0);
378+
verify(mockNodeInfo, times(1)).setImportantForAccessibility(eq(true));
379+
}
380+
381+
@Test
382+
public void itSetsNodeImportantForAccessibilityIfItHasActions() {
383+
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
384+
AccessibilityManager mockManager = mock(AccessibilityManager.class);
385+
View mockRootView = mock(View.class);
386+
Context context = mock(Context.class);
387+
when(mockRootView.getContext()).thenReturn(context);
388+
when(context.getPackageName()).thenReturn("test");
389+
AccessibilityBridge accessibilityBridge =
390+
setUpBridge(mockRootView, mockManager, mockViewEmbedder);
391+
ViewParent mockParent = mock(ViewParent.class);
392+
when(mockRootView.getParent()).thenReturn(mockParent);
393+
when(mockManager.isEnabled()).thenReturn(true);
394+
395+
TestSemanticsNode root = new TestSemanticsNode();
396+
root.id = 0;
397+
root.addAction(Action.TAP);
398+
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
399+
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
400+
401+
AccessibilityBridge spyAccessibilityBridge = spy(accessibilityBridge);
402+
AccessibilityNodeInfo mockNodeInfo = mock(AccessibilityNodeInfo.class);
403+
404+
when(spyAccessibilityBridge.obtainAccessibilityNodeInfo(mockRootView, 0))
405+
.thenReturn(mockNodeInfo);
406+
spyAccessibilityBridge.createAccessibilityNodeInfo(0);
407+
verify(mockNodeInfo, times(1)).setImportantForAccessibility(eq(true));
408+
}
409+
410+
@Test
411+
public void itSetsNodeUnImportantForAccessibilityIfItIsEmpty() {
412+
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
413+
AccessibilityManager mockManager = mock(AccessibilityManager.class);
414+
View mockRootView = mock(View.class);
415+
Context context = mock(Context.class);
416+
when(mockRootView.getContext()).thenReturn(context);
417+
when(context.getPackageName()).thenReturn("test");
418+
AccessibilityBridge accessibilityBridge =
419+
setUpBridge(mockRootView, mockManager, mockViewEmbedder);
420+
ViewParent mockParent = mock(ViewParent.class);
421+
when(mockRootView.getParent()).thenReturn(mockParent);
422+
when(mockManager.isEnabled()).thenReturn(true);
423+
424+
TestSemanticsNode root = new TestSemanticsNode();
425+
root.id = 0;
426+
TestSemanticsNode node = new TestSemanticsNode();
427+
node.id = 1;
428+
root.children.add(node);
429+
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
430+
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
431+
432+
AccessibilityBridge spyAccessibilityBridge = spy(accessibilityBridge);
433+
AccessibilityNodeInfo mockNodeInfo = mock(AccessibilityNodeInfo.class);
434+
435+
when(spyAccessibilityBridge.obtainAccessibilityNodeInfo(mockRootView, 0))
436+
.thenReturn(mockNodeInfo);
437+
spyAccessibilityBridge.createAccessibilityNodeInfo(0);
438+
verify(mockNodeInfo, times(1)).setImportantForAccessibility(eq(false));
439+
440+
AccessibilityNodeInfo mockNodeInfo1 = mock(AccessibilityNodeInfo.class);
441+
442+
when(spyAccessibilityBridge.obtainAccessibilityNodeInfo(mockRootView, 1))
443+
.thenReturn(mockNodeInfo1);
444+
spyAccessibilityBridge.createAccessibilityNodeInfo(1);
445+
verify(mockNodeInfo1, times(1)).setImportantForAccessibility(eq(false));
446+
}
447+
324448
@TargetApi(28)
325449
@Test
326450
public void itSetCutoutInsetBasedonLayoutModeNever() {

0 commit comments

Comments
 (0)