diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index e03bc02042017..4f61ca522e2f6 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -1566,7 +1566,7 @@ void updateSemantics( if (lastAdded != null && (lastAdded.id != previousRouteId || newRoutes.size() != flutterNavigationStack.size())) { previousRouteId = lastAdded.id; - sendWindowChangeEvent(lastAdded); + onWindowNameChange(lastAdded); } flutterNavigationStack.clear(); for (SemanticsNode semanticsNode : newRoutes) { @@ -1778,15 +1778,17 @@ private void sendAccessibilityEvent(@NonNull AccessibilityEvent event) { } /** - * Creates a {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED} and sends the event to Android's - * accessibility system. + * Informs the TalkBack user about window name changes. + * + *

This method sets accessibility panel title if the API level >= 28, otherwise, it creates a + * {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED} and sends the event to Android's + * accessibility system. In both cases, TalkBack announces the label of the route and re-addjusts + * the accessibility focus. * *

The given {@code route} should be a {@link SemanticsNode} that represents a navigation route * in the Flutter app. */ - private void sendWindowChangeEvent(@NonNull SemanticsNode route) { - AccessibilityEvent event = - obtainAccessibilityEvent(route.id, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + private void onWindowNameChange(@NonNull SemanticsNode route) { String routeName = route.getRouteName(); if (routeName == null) { // The routeName will be null when there is no semantics node that represnets namesRoute in @@ -1799,8 +1801,20 @@ private void sendWindowChangeEvent(@NonNull SemanticsNode route) { // next. routeName = " "; } - event.getText().add(routeName); - sendAccessibilityEvent(event); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + setAccessibilityPaneTitle(routeName); + } else { + AccessibilityEvent event = + obtainAccessibilityEvent(route.id, AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + event.getText().add(routeName); + sendAccessibilityEvent(event); + } + } + + @TargetApi(28) + @RequiresApi(28) + private void setAccessibilityPaneTitle(String title) { + rootAccessibilityView.setAccessibilityPaneTitle(title); } /** diff --git a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java index 758ce6379643b..1bb45d8592a72 100644 --- a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java +++ b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java @@ -210,15 +210,7 @@ public void itAnnouncesRouteNameWhenAddingNewRoute() { TestSemanticsUpdate testSemanticsUpdate = root.toUpdate(); testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge); - 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(), "node1"); + verify(mockRootView, times(1)).setAccessibilityPaneTitle(eq("node1")); TestSemanticsNode new_root = new TestSemanticsNode(); new_root.id = 0; @@ -237,14 +229,7 @@ public void itAnnouncesRouteNameWhenAddingNewRoute() { testSemanticsUpdate = new_root.toUpdate(); testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge); - eventCaptor = ArgumentCaptor.forClass(AccessibilityEvent.class); - verify(mockParent, times(4)) - .requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture()); - event = eventCaptor.getAllValues().get(2); - assertEquals(event.getEventType(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); - sentences = event.getText(); - assertEquals(sentences.size(), 1); - assertEquals(sentences.get(0).toString(), "new_node2"); + verify(mockRootView, times(1)).setAccessibilityPaneTitle(eq("new_node2")); } @Test @@ -517,12 +502,7 @@ public void itIgnoresUnfocusableNodeDuringHitTest() { TestSemanticsUpdate testSemanticsUpdate = root.toUpdate(); testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge); - 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); + verify(mockRootView, times(1)).setAccessibilityPaneTitle(eq(" ")); // Synthesize an accessibility hit test event. MotionEvent mockEvent = mock(MotionEvent.class); @@ -533,10 +513,11 @@ public void itIgnoresUnfocusableNodeDuringHitTest() { assertEquals(hit, true); - eventCaptor = ArgumentCaptor.forClass(AccessibilityEvent.class); - verify(mockParent, times(3)) + ArgumentCaptor eventCaptor = + ArgumentCaptor.forClass(AccessibilityEvent.class); + verify(mockParent, times(2)) .requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture()); - event = eventCaptor.getAllValues().get(2); + AccessibilityEvent event = eventCaptor.getAllValues().get(1); assertEquals(event.getEventType(), AccessibilityEvent.TYPE_VIEW_HOVER_ENTER); assertEquals(accessibilityBridge.getHoveredObjectId(), 2); } @@ -572,15 +553,7 @@ public void itAnnouncesRouteNameWhenRemoveARoute() { TestSemanticsUpdate testSemanticsUpdate = root.toUpdate(); testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge); - 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(), "node2"); + verify(mockRootView, times(1)).setAccessibilityPaneTitle(eq("node2")); TestSemanticsNode new_root = new TestSemanticsNode(); new_root.id = 0; @@ -597,14 +570,7 @@ public void itAnnouncesRouteNameWhenRemoveARoute() { testSemanticsUpdate = new_root.toUpdate(); testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge); - eventCaptor = ArgumentCaptor.forClass(AccessibilityEvent.class); - verify(mockParent, times(4)) - .requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture()); - event = eventCaptor.getAllValues().get(2); - assertEquals(event.getEventType(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); - sentences = event.getText(); - assertEquals(sentences.size(), 1); - assertEquals(sentences.get(0).toString(), "new_node2"); + verify(mockRootView, times(1)).setAccessibilityPaneTitle(eq("new_node2")); } @TargetApi(21) @@ -1087,15 +1053,7 @@ public void itAnnouncesWhiteSpaceWhenNoNamesRoute() { TestSemanticsUpdate testSemanticsUpdate = root.toUpdate(); testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge); - 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(), " "); + verify(mockRootView, times(1)).setAccessibilityPaneTitle(eq(" ")); } @Test