diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 28b09507214b3..6168954f58bb0 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -257,6 +257,30 @@ public int getHoveredObjectId() { @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 void onAccessibilityStateChanged(boolean accessibilityEnabled) { accessibilityChannel.setAccessibilityMessageHandler(accessibilityMessageHandler); accessibilityChannel.onAndroidAccessibilityEnabled(); } else { + setAccessibleNavigation(false); accessibilityChannel.setAccessibilityMessageHandler(null); accessibilityChannel.onAndroidAccessibilityDisabled(); } @@ -409,7 +434,6 @@ public AccessibilityBridge( 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 void onTouchExplorationStateChanged(boolean isTouchExplorationEnabled) { 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 AccessibilityNodeInfo obtainAccessibilityNodeInfo(View rootView, int virt // 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/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java index 324331d9d1817..155401992df05 100644 --- a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java +++ b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java @@ -124,6 +124,44 @@ public void itTakesGlobalCoordinatesOfFlutterViewIntoAccount() { 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();