From 9b4bb20ac926b8d887a8f6a92173fd60aac72465 Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Mon, 2 Nov 2020 09:53:01 -0800 Subject: [PATCH] makes android semanticsnode to ignore hittest if it is not focusable (#22205) --- .../io/flutter/view/AccessibilityBridge.java | 7 +- .../flutter/view/AccessibilityBridgeTest.java | 64 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 0095ff65e9fd9..1f1b9eed39f20 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 @@ private SemanticsNode hitTest(float[] point) { 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 f035b9f12eb32..90f05be5eb297 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 void itAnnouncesRouteNameWhenAddingNewRoute() { 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);