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