From edd83ae5faae0919dceb4e7b20f8c7718deee848 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Thu, 29 Oct 2020 11:45:13 -0700 Subject: [PATCH 1/2] makes android semanticsnode to ignore hittest if it is not focusable --- shell/platform/android/io/flutter/view/AccessibilityBridge.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 0095ff65e9fd9..a1e5daf7b95ca 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -2180,7 +2180,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 From b84e387dac6af179e172aed6ff01c9d65f7c9fae Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Fri, 30 Oct 2020 11:08:46 -0700 Subject: [PATCH 2/2] add test --- .../io/flutter/view/AccessibilityBridge.java | 5 ++ .../flutter/view/AccessibilityBridgeTest.java | 64 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index a1e5daf7b95ca..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. 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);