From f17517ea27a4e07fe8fda555e9fede3189ff77e5 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Mon, 26 Jul 2021 13:27:25 -0700 Subject: [PATCH 1/4] Sets accessibility panel title when route changes --- .../io/flutter/view/AccessibilityBridge.java | 29 ++++-- .../flutter/view/AccessibilityBridgeTest.java | 93 ++++++++----------- 2 files changed, 62 insertions(+), 60 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index e03bc02042017..26e966afb5537 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -33,6 +33,7 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; +import androidx.core.view.ViewCompat; import io.flutter.BuildConfig; import io.flutter.Log; import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; @@ -1566,7 +1567,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 +1779,16 @@ private void sendAccessibilityEvent(@NonNull AccessibilityEvent event) { } /** - * Creates a {@link AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED} and sends the event to Android's + * Called when needs to inform 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. * *

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,21 @@ 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) + @VisibleForTesting + public void setAccessibilityPaneTitle(String title) { + ViewCompat.setAccessibilityPaneTitle(rootAccessibilityView, 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..f76933ac162b0 100644 --- a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java +++ b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java @@ -210,15 +210,8 @@ 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"); + AccessibilityBridgeSpy spy = (AccessibilityBridgeSpy) accessibilityBridge; + assertEquals(spy.panelTitle, "node1"); TestSemanticsNode new_root = new TestSemanticsNode(); new_root.id = 0; @@ -237,14 +230,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"); + assertEquals(spy.panelTitle, "new_node2"); } @Test @@ -517,12 +503,8 @@ 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); + AccessibilityBridgeSpy spy = (AccessibilityBridgeSpy) accessibilityBridge; + assertEquals(spy.panelTitle, " "); // Synthesize an accessibility hit test event. MotionEvent mockEvent = mock(MotionEvent.class); @@ -533,10 +515,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 +555,8 @@ 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"); + AccessibilityBridgeSpy spy = (AccessibilityBridgeSpy) accessibilityBridge; + assertEquals(spy.panelTitle, "node2"); TestSemanticsNode new_root = new TestSemanticsNode(); new_root.id = 0; @@ -597,14 +573,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"); + assertEquals(spy.panelTitle, "new_node2"); } @TargetApi(21) @@ -1087,15 +1056,8 @@ 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(), " "); + AccessibilityBridgeSpy spy = (AccessibilityBridgeSpy) accessibilityBridge; + assertEquals(spy.panelTitle, " "); } @Test @@ -1339,7 +1301,7 @@ AccessibilityBridge setUpBridge( if (platformViewsAccessibilityDelegate == null) { platformViewsAccessibilityDelegate = mock(PlatformViewsAccessibilityDelegate.class); } - return new AccessibilityBridge( + return new AccessibilityBridgeSpy( rootAccessibilityView, accessibilityChannel, accessibilityManager, @@ -1541,3 +1503,28 @@ static void updateString( } } } + +class AccessibilityBridgeSpy extends AccessibilityBridge { + public AccessibilityBridgeSpy( + View rootAccessibilityView, + AccessibilityChannel accessibilityChannel, + AccessibilityManager accessibilityManager, + ContentResolver contentResolver, + AccessibilityViewEmbedder accessibilityViewEmbedder, + PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate) { + super( + rootAccessibilityView, + accessibilityChannel, + accessibilityManager, + contentResolver, + accessibilityViewEmbedder, + platformViewsAccessibilityDelegate); + } + + String panelTitle; + + @Override + public void setAccessibilityPaneTitle(String title) { + panelTitle = title; + } +} From 49ee7f466480cf28118bf64cc59d9a93c2f2f33f Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Tue, 27 Jul 2021 09:52:03 -0700 Subject: [PATCH 2/4] update --- .../io/flutter/view/AccessibilityBridge.java | 15 +++---- .../flutter/view/AccessibilityBridgeTest.java | 43 +++---------------- 2 files changed, 14 insertions(+), 44 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 26e966afb5537..53595a816bb3e 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -33,7 +33,6 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; -import androidx.core.view.ViewCompat; import io.flutter.BuildConfig; import io.flutter.Log; import io.flutter.embedding.engine.systemchannels.AccessibilityChannel; @@ -1779,11 +1778,12 @@ private void sendAccessibilityEvent(@NonNull AccessibilityEvent event) { } /** - * Called when needs to inform the TalkBack user about window name changes. + * Informs 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. + * 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. @@ -1811,11 +1811,10 @@ private void onWindowNameChange(@NonNull SemanticsNode route) { } } - @TargetApi(28) - @RequiresApi(28) - @VisibleForTesting - public void setAccessibilityPaneTitle(String title) { - ViewCompat.setAccessibilityPaneTitle(rootAccessibilityView, title); + @TargetApi(Build.VERSION_CODES.P) + @RequiresApi(Build.VERSION_CODES.P) + 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 f76933ac162b0..1bb45d8592a72 100644 --- a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java +++ b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java @@ -210,8 +210,7 @@ public void itAnnouncesRouteNameWhenAddingNewRoute() { TestSemanticsUpdate testSemanticsUpdate = root.toUpdate(); testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge); - AccessibilityBridgeSpy spy = (AccessibilityBridgeSpy) accessibilityBridge; - assertEquals(spy.panelTitle, "node1"); + verify(mockRootView, times(1)).setAccessibilityPaneTitle(eq("node1")); TestSemanticsNode new_root = new TestSemanticsNode(); new_root.id = 0; @@ -230,7 +229,7 @@ public void itAnnouncesRouteNameWhenAddingNewRoute() { testSemanticsUpdate = new_root.toUpdate(); testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge); - assertEquals(spy.panelTitle, "new_node2"); + verify(mockRootView, times(1)).setAccessibilityPaneTitle(eq("new_node2")); } @Test @@ -503,8 +502,7 @@ public void itIgnoresUnfocusableNodeDuringHitTest() { TestSemanticsUpdate testSemanticsUpdate = root.toUpdate(); testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge); - AccessibilityBridgeSpy spy = (AccessibilityBridgeSpy) accessibilityBridge; - assertEquals(spy.panelTitle, " "); + verify(mockRootView, times(1)).setAccessibilityPaneTitle(eq(" ")); // Synthesize an accessibility hit test event. MotionEvent mockEvent = mock(MotionEvent.class); @@ -555,8 +553,7 @@ public void itAnnouncesRouteNameWhenRemoveARoute() { TestSemanticsUpdate testSemanticsUpdate = root.toUpdate(); testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge); - AccessibilityBridgeSpy spy = (AccessibilityBridgeSpy) accessibilityBridge; - assertEquals(spy.panelTitle, "node2"); + verify(mockRootView, times(1)).setAccessibilityPaneTitle(eq("node2")); TestSemanticsNode new_root = new TestSemanticsNode(); new_root.id = 0; @@ -573,7 +570,7 @@ public void itAnnouncesRouteNameWhenRemoveARoute() { testSemanticsUpdate = new_root.toUpdate(); testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge); - assertEquals(spy.panelTitle, "new_node2"); + verify(mockRootView, times(1)).setAccessibilityPaneTitle(eq("new_node2")); } @TargetApi(21) @@ -1056,8 +1053,7 @@ public void itAnnouncesWhiteSpaceWhenNoNamesRoute() { TestSemanticsUpdate testSemanticsUpdate = root.toUpdate(); testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge); - AccessibilityBridgeSpy spy = (AccessibilityBridgeSpy) accessibilityBridge; - assertEquals(spy.panelTitle, " "); + verify(mockRootView, times(1)).setAccessibilityPaneTitle(eq(" ")); } @Test @@ -1301,7 +1297,7 @@ AccessibilityBridge setUpBridge( if (platformViewsAccessibilityDelegate == null) { platformViewsAccessibilityDelegate = mock(PlatformViewsAccessibilityDelegate.class); } - return new AccessibilityBridgeSpy( + return new AccessibilityBridge( rootAccessibilityView, accessibilityChannel, accessibilityManager, @@ -1503,28 +1499,3 @@ static void updateString( } } } - -class AccessibilityBridgeSpy extends AccessibilityBridge { - public AccessibilityBridgeSpy( - View rootAccessibilityView, - AccessibilityChannel accessibilityChannel, - AccessibilityManager accessibilityManager, - ContentResolver contentResolver, - AccessibilityViewEmbedder accessibilityViewEmbedder, - PlatformViewsAccessibilityDelegate platformViewsAccessibilityDelegate) { - super( - rootAccessibilityView, - accessibilityChannel, - accessibilityManager, - contentResolver, - accessibilityViewEmbedder, - platformViewsAccessibilityDelegate); - } - - String panelTitle; - - @Override - public void setAccessibilityPaneTitle(String title) { - panelTitle = title; - } -} From 7e27fa6b6d7ab2989409fce60694d540b0e87358 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Tue, 27 Jul 2021 11:48:17 -0700 Subject: [PATCH 3/4] update --- .../platform/android/io/flutter/view/AccessibilityBridge.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 53595a816bb3e..a27f4bc2d4069 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -1811,8 +1811,8 @@ private void onWindowNameChange(@NonNull SemanticsNode route) { } } - @TargetApi(Build.VERSION_CODES.P) - @RequiresApi(Build.VERSION_CODES.P) + @TargetApi(28) + @RequiresApi(28) private void setAccessibilityPaneTitle(String title) { rootAccessibilityView.setAccessibilityPaneTitle(title); } From 189c3cb6a351deecf98e2b4bd6cf2c1baeee1c19 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Wed, 28 Jul 2021 09:34:46 -0700 Subject: [PATCH 4/4] update --- 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 a27f4bc2d4069..4f61ca522e2f6 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -1778,7 +1778,7 @@ private void sendAccessibilityEvent(@NonNull AccessibilityEvent event) { } /** - * Informs TalkBack user about window name changes. + * 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