From ccaee70b056f3eda1f980e2de464d393581f2016 Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Wed, 19 Aug 2020 14:06:03 -0700 Subject: [PATCH] Missing default focus when navigating to a page with no SemanticsNode that sets namesRoute:true (#20516) --- .../io/flutter/view/AccessibilityBridge.java | 11 ++++++ .../flutter/view/AccessibilityBridgeTest.java | 35 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index f6ed0179be8..b54b76b02b8 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -1548,6 +1548,17 @@ public class AccessibilityBridge extends AccessibilityNodeProvider { AccessibilityEvent event = obtainAccessibilityEvent(route.id, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); String routeName = route.getRouteName(); + if (routeName == null) { + // The routeName will be null when there is no semantics node that represnets namesRoute in + // the scopeRoute. The TYPE_WINDOW_STATE_CHANGED only works the route name is not null and not + // empty. Gives it a whitespace will make it focus the first semantics node without + // pronouncing any word. + // + // The other way to trigger a focus change is to send a TYPE_VIEW_FOCUSED to the + // rootAccessibilityView. However, it is less predictable which semantics node it will focus + // next. + routeName = " "; + } event.getText().add(routeName); sendAccessibilityEvent(event); } diff --git a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java index 38a6d7e695e..96ade8718a9 100644 --- a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java +++ b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java @@ -132,6 +132,41 @@ public class AccessibilityBridgeTest { assertEquals(event.getEventType(), AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); } + @Test + public void itAnnouncesWhiteSpaceWhenNoNamesRoute() { + 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); + + // Sent a11y tree with scopeRoute without namesRoute. + TestSemanticsNode root = new TestSemanticsNode(); + root.id = 0; + TestSemanticsNode scopeRoute = new TestSemanticsNode(); + scopeRoute.id = 1; + scopeRoute.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE); + root.children.add(scopeRoute); + 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); + List sentences = event.getText(); + assertEquals(sentences.size(), 1); + assertEquals(sentences.get(0).toString(), " "); + } + @Test public void itHoverOverOutOfBoundsDoesNotCrash() { // SementicsNode.hitTest() returns null when out of bounds.