diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 0095ff65e9fd9edb351132ecc728c4b0b8cdd2a5..1f1b9eed39f20f2ffb30ed38ebb302f71dffa06b 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -207,6 +207,11 @@ public class AccessibilityBridge extends AccessibilityNodeProvider { // beneath a stylus or mouse cursor. @Nullable private SemanticsNode hoveredObject; + @VisibleForTesting + public int getHoveredObjectId() { + return hoveredObject.id; + } + // A Java/Android cached representation of the Flutter app's navigation stack. The Flutter // navigation stack is tracked so that accessibility announcements can be made during Flutter's // navigation changes. @@ -2180,7 +2185,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider { return result; } } - return this; + return isFocusable() ? this : null; } // TODO(goderbauer): This should be decided by the framework once we have more information diff --git a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java index f035b9f12eb32a632b1933cb084605ea6e7968f5..90f05be5eb29716dd4e8049aca6dbd915ab9ec80 100644 --- a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java +++ b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java @@ -204,6 +204,70 @@ public class AccessibilityBridgeTest { assertEquals(sentences.get(0).toString(), "new_node2"); } + @Test + public void itIgnoresUnfocusableNodeDuringHitTest() { + 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); + when(mockManager.isTouchExplorationEnabled()).thenReturn(true); + + TestSemanticsNode root = new TestSemanticsNode(); + root.id = 0; + root.left = 0; + root.top = 0; + root.bottom = 20; + root.right = 20; + TestSemanticsNode ignored = new TestSemanticsNode(); + ignored.id = 1; + ignored.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE); + ignored.left = 0; + ignored.top = 0; + ignored.bottom = 20; + ignored.right = 20; + root.children.add(ignored); + TestSemanticsNode child = new TestSemanticsNode(); + child.id = 2; + child.label = "label"; + child.left = 0; + child.top = 0; + child.bottom = 20; + child.right = 20; + root.children.add(child); + TestSemanticsUpdate testSemanticsUpdate = root.toUpdate(); + accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings); + + ArgumentCaptor eventCaptor = + ArgumentCaptor.forClass(AccessibilityEvent.class); + verify(mockParent, times(2)) + .requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture()); + AccessibilityEvent event = eventCaptor.getAllValues().get(0); + assertEquals(event.getEventType(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + + // Synthesize an accessibility hit test event. + MotionEvent mockEvent = mock(MotionEvent.class); + when(mockEvent.getX()).thenReturn(10.0f); + when(mockEvent.getY()).thenReturn(10.0f); + when(mockEvent.getAction()).thenReturn(MotionEvent.ACTION_HOVER_ENTER); + boolean hit = accessibilityBridge.onAccessibilityHoverEvent(mockEvent); + + assertEquals(hit, true); + + eventCaptor = ArgumentCaptor.forClass(AccessibilityEvent.class); + verify(mockParent, times(3)) + .requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture()); + event = eventCaptor.getAllValues().get(2); + assertEquals(event.getEventType(), AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); + assertEquals(accessibilityBridge.getHoveredObjectId(), 2); + } + @Test public void itAnnouncesRouteNameWhenRemoveARoute() { AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);