From 1a38d2e54be3a6ea7ec6b2d24670d30bbd8e53e7 Mon Sep 17 00:00:00 2001 From: chunhtai <47866232+chunhtai@users.noreply.github.com> Date: Fri, 19 Aug 2022 13:54:47 -0700 Subject: [PATCH] Updates accessible_navigation trigger in Android (flutter/engine#35478) * Updates accessible_navigation trigger in Android * update doc comments * comment * addressing comment --- .../io/flutter/view/AccessibilityBridge.java | 34 ++++++++++++++--- .../flutter/view/AccessibilityBridgeTest.java | 38 +++++++++++++++++++ 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 28b09507214..6168954f58b 100644 --- a/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/engine/src/flutter/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -257,6 +257,30 @@ public class AccessibilityBridge extends AccessibilityNodeProvider { @Nullable private OnAccessibilityChangeListener onAccessibilityChangeListener; + // Whether the users are using assistive technologies to interact with the devices. + // + // The getter returns true when at least one of the assistive technologies is running: + // TalkBack, SwitchAccess, or VoiceAccess. + @VisibleForTesting + public boolean getAccessibleNavigation() { + return accessibleNavigation; + } + + private boolean accessibleNavigation = false; + + private void setAccessibleNavigation(boolean value) { + if (accessibleNavigation == value) { + return; + } + accessibleNavigation = value; + if (accessibleNavigation) { + accessibilityFeatureFlags |= AccessibilityFeature.ACCESSIBLE_NAVIGATION.value; + } else { + accessibilityFeatureFlags &= ~AccessibilityFeature.ACCESSIBLE_NAVIGATION.value; + } + sendLatestAccessibilityFlagsToFlutter(); + } + // Set to true after {@code release} has been invoked. private boolean isReleased = false; @@ -331,6 +355,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider { accessibilityChannel.setAccessibilityMessageHandler(accessibilityMessageHandler); accessibilityChannel.onAndroidAccessibilityEnabled(); } else { + setAccessibleNavigation(false); accessibilityChannel.setAccessibilityMessageHandler(null); accessibilityChannel.onAndroidAccessibilityDisabled(); } @@ -409,7 +434,6 @@ public class AccessibilityBridge extends AccessibilityNodeProvider { this.contentResolver = contentResolver; this.accessibilityViewEmbedder = accessibilityViewEmbedder; this.platformViewsAccessibilityDelegate = platformViewsAccessibilityDelegate; - // Tell Flutter whether accessibility is initially active or not. Then register a listener // to be notified of changes in the future. accessibilityStateChangeListener.onAccessibilityStateChanged(accessibilityManager.isEnabled()); @@ -425,13 +449,10 @@ public class AccessibilityBridge extends AccessibilityNodeProvider { if (isReleased) { return; } - if (isTouchExplorationEnabled) { - accessibilityFeatureFlags |= AccessibilityFeature.ACCESSIBLE_NAVIGATION.value; - } else { + if (!isTouchExplorationEnabled) { + setAccessibleNavigation(false); onTouchExplorationExit(); - accessibilityFeatureFlags &= ~AccessibilityFeature.ACCESSIBLE_NAVIGATION.value; } - sendLatestAccessibilityFlagsToFlutter(); if (onAccessibilityChangeListener != null) { onAccessibilityChangeListener.onAccessibilityChanged( @@ -551,6 +572,7 @@ public class AccessibilityBridge extends AccessibilityNodeProvider { // Suppressing Lint warning for new API, as we are version guarding all calls to newer APIs @SuppressLint("NewApi") public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { + setAccessibleNavigation(true); if (virtualViewId >= MIN_ENGINE_GENERATED_NODE_ID) { // The node is in the engine generated range, and is provided by the accessibility view // embedder. diff --git a/engine/src/flutter/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java b/engine/src/flutter/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java index 324331d9d18..155401992df 100644 --- a/engine/src/flutter/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java +++ b/engine/src/flutter/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java @@ -124,6 +124,44 @@ public class AccessibilityBridgeTest { assertEquals(position, outBoundsInScreen.top); } + @Test + public void itSetsAccessibleNavigation() { + AccessibilityChannel mockChannel = mock(AccessibilityChannel.class); + 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"); + when(mockManager.isTouchExplorationEnabled()).thenReturn(false); + AccessibilityBridge accessibilityBridge = + setUpBridge( + /*rootAccessibilityView=*/ mockRootView, + /*accessibilityChannel=*/ mockChannel, + /*accessibilityManager=*/ mockManager, + /*contentResolver=*/ null, + /*accessibilityViewEmbedder=*/ mockViewEmbedder, + /*platformViewsAccessibilityDelegate=*/ null); + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(AccessibilityManager.TouchExplorationStateChangeListener.class); + verify(mockManager).addTouchExplorationStateChangeListener(listenerCaptor.capture()); + + assertEquals(accessibilityBridge.getAccessibleNavigation(), false); + verify(mockChannel).setAccessibilityFeatures(0); + reset(mockChannel); + + // Simulate assistive technology accessing accessibility tree. + accessibilityBridge.createAccessibilityNodeInfo(0); + verify(mockChannel).setAccessibilityFeatures(1); + assertEquals(accessibilityBridge.getAccessibleNavigation(), true); + + // Simulate turning off TalkBack. + reset(mockChannel); + listenerCaptor.getValue().onTouchExplorationStateChanged(false); + verify(mockChannel).setAccessibilityFeatures(0); + assertEquals(accessibilityBridge.getAccessibleNavigation(), false); + } + @Test public void itDoesNotContainADescriptionIfScopesRoute() { AccessibilityBridge accessibilityBridge = setUpBridge();