diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 134c9fbcff43a..c7c249713fff8 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -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); @@ -616,13 +621,16 @@ 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); } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + result.setImportantForAccessibility(false); + } return result; } @@ -653,6 +661,13 @@ public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { 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 @@ && 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. * @@ -2141,6 +2169,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 { diff --git a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java index 434843e1a8748..256ba35bc702a 100644 --- a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java +++ b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java @@ -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; @@ -321,6 +322,133 @@ public void itSetsTraversalAfter() { 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() {