From 26e14a019b31e922b93501e21ee2bbcec61fa3df Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Wed, 17 Aug 2022 15:54:15 -0700 Subject: [PATCH 1/4] Updates accessible_navigation trigger in Android --- .../io/flutter/view/AccessibilityBridge.java | 27 ++++++++++--- .../flutter/view/AccessibilityBridgeTest.java | 38 +++++++++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 28b09507214b3..2785cb7ddfa16 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -257,6 +257,23 @@ public int getHoveredObjectId() { @Nullable private OnAccessibilityChangeListener onAccessibilityChangeListener; + // The widget within Flutter that currently sits beneath a cursor, e.g, + // beneath a stylus or mouse cursor. + @VisibleForTesting public boolean hasAssistiveTechnology = false; + + private void setHasAssistiveTechnology(boolean value) { + if (hasAssistiveTechnology == value) { + return; + } + hasAssistiveTechnology = value; + if (hasAssistiveTechnology) { + 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 +348,7 @@ public void onAccessibilityStateChanged(boolean accessibilityEnabled) { accessibilityChannel.setAccessibilityMessageHandler(accessibilityMessageHandler); accessibilityChannel.onAndroidAccessibilityEnabled(); } else { + setHasAssistiveTechnology(false); accessibilityChannel.setAccessibilityMessageHandler(null); accessibilityChannel.onAndroidAccessibilityDisabled(); } @@ -409,7 +427,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 +442,10 @@ public void onTouchExplorationStateChanged(boolean isTouchExplorationEnabled) { if (isReleased) { return; } - if (isTouchExplorationEnabled) { - accessibilityFeatureFlags |= AccessibilityFeature.ACCESSIBLE_NAVIGATION.value; - } else { + if (!isTouchExplorationEnabled) { + setHasAssistiveTechnology(false); onTouchExplorationExit(); - accessibilityFeatureFlags &= ~AccessibilityFeature.ACCESSIBLE_NAVIGATION.value; } - sendLatestAccessibilityFlagsToFlutter(); if (onAccessibilityChangeListener != null) { onAccessibilityChangeListener.onAccessibilityChanged( @@ -551,6 +565,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) { + setHasAssistiveTechnology(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..7a60b0056fc69 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 itSetsHasAssistiveTechnology() { + 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.hasAssistiveTechnology, false); + verify(mockChannel).setAccessibilityFeatures(0); + reset(mockChannel); + + // Simulate assistive technology accessing accessibility tree. + accessibilityBridge.createAccessibilityNodeInfo(0); + verify(mockChannel).setAccessibilityFeatures(1); + assertEquals(accessibilityBridge.hasAssistiveTechnology, true); + + // Simulate turning of talkback. + reset(mockChannel); + listenerCaptor.getValue().onTouchExplorationStateChanged(false); + verify(mockChannel).setAccessibilityFeatures(0); + assertEquals(accessibilityBridge.hasAssistiveTechnology, false); + } + @Test public void itDoesNotContainADescriptionIfScopesRoute() { AccessibilityBridge accessibilityBridge = setUpBridge(); From be610d30b96d79d6c852895e668f6f8894aee6cc Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Wed, 17 Aug 2022 15:56:52 -0700 Subject: [PATCH 2/4] update doc comments --- .../android/io/flutter/view/AccessibilityBridge.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 2785cb7ddfa16..65dce037cf73e 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -257,8 +257,9 @@ public int getHoveredObjectId() { @Nullable private OnAccessibilityChangeListener onAccessibilityChangeListener; - // The widget within Flutter that currently sits beneath a cursor, e.g, - // beneath a stylus or mouse cursor. + // Whether there is assitive technology[s] running. + // + // Use the setter to update this property if needed. @VisibleForTesting public boolean hasAssistiveTechnology = false; private void setHasAssistiveTechnology(boolean value) { From 79fb63c6a7dc2af4fe6e639529e73341b693b7bf Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Wed, 17 Aug 2022 16:31:10 -0700 Subject: [PATCH 3/4] comment --- .../android/test/io/flutter/view/AccessibilityBridgeTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java index 7a60b0056fc69..6d25092e0d78f 100644 --- a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java +++ b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java @@ -155,7 +155,7 @@ public void itSetsHasAssistiveTechnology() { verify(mockChannel).setAccessibilityFeatures(1); assertEquals(accessibilityBridge.hasAssistiveTechnology, true); - // Simulate turning of talkback. + // Simulate turning off TalkBack. reset(mockChannel); listenerCaptor.getValue().onTouchExplorationStateChanged(false); verify(mockChannel).setAccessibilityFeatures(0); From b23fb6dd3e2dc61cc821bd11d2093ddaf16745e2 Mon Sep 17 00:00:00 2001 From: Chun-Heng Tai Date: Fri, 19 Aug 2022 12:03:23 -0700 Subject: [PATCH 4/4] addressing comment --- .../io/flutter/view/AccessibilityBridge.java | 26 ++++++++++++------- .../flutter/view/AccessibilityBridgeTest.java | 8 +++--- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/shell/platform/android/io/flutter/view/AccessibilityBridge.java b/shell/platform/android/io/flutter/view/AccessibilityBridge.java index 65dce037cf73e..6168954f58bb0 100644 --- a/shell/platform/android/io/flutter/view/AccessibilityBridge.java +++ b/shell/platform/android/io/flutter/view/AccessibilityBridge.java @@ -257,17 +257,23 @@ public int getHoveredObjectId() { @Nullable private OnAccessibilityChangeListener onAccessibilityChangeListener; - // Whether there is assitive technology[s] running. + // Whether the users are using assistive technologies to interact with the devices. // - // Use the setter to update this property if needed. - @VisibleForTesting public boolean hasAssistiveTechnology = false; + // 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 setHasAssistiveTechnology(boolean value) { - if (hasAssistiveTechnology == value) { + private void setAccessibleNavigation(boolean value) { + if (accessibleNavigation == value) { return; } - hasAssistiveTechnology = value; - if (hasAssistiveTechnology) { + accessibleNavigation = value; + if (accessibleNavigation) { accessibilityFeatureFlags |= AccessibilityFeature.ACCESSIBLE_NAVIGATION.value; } else { accessibilityFeatureFlags &= ~AccessibilityFeature.ACCESSIBLE_NAVIGATION.value; @@ -349,7 +355,7 @@ public void onAccessibilityStateChanged(boolean accessibilityEnabled) { accessibilityChannel.setAccessibilityMessageHandler(accessibilityMessageHandler); accessibilityChannel.onAndroidAccessibilityEnabled(); } else { - setHasAssistiveTechnology(false); + setAccessibleNavigation(false); accessibilityChannel.setAccessibilityMessageHandler(null); accessibilityChannel.onAndroidAccessibilityDisabled(); } @@ -444,7 +450,7 @@ public void onTouchExplorationStateChanged(boolean isTouchExplorationEnabled) { return; } if (!isTouchExplorationEnabled) { - setHasAssistiveTechnology(false); + setAccessibleNavigation(false); onTouchExplorationExit(); } @@ -566,7 +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) { - setHasAssistiveTechnology(true); + 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 6d25092e0d78f..155401992df05 100644 --- a/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java +++ b/shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java @@ -125,7 +125,7 @@ public void itTakesGlobalCoordinatesOfFlutterViewIntoAccount() { } @Test - public void itSetsHasAssistiveTechnology() { + public void itSetsAccessibleNavigation() { AccessibilityChannel mockChannel = mock(AccessibilityChannel.class); AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class); AccessibilityManager mockManager = mock(AccessibilityManager.class); @@ -146,20 +146,20 @@ public void itSetsHasAssistiveTechnology() { ArgumentCaptor.forClass(AccessibilityManager.TouchExplorationStateChangeListener.class); verify(mockManager).addTouchExplorationStateChangeListener(listenerCaptor.capture()); - assertEquals(accessibilityBridge.hasAssistiveTechnology, false); + 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.hasAssistiveTechnology, true); + assertEquals(accessibilityBridge.getAccessibleNavigation(), true); // Simulate turning off TalkBack. reset(mockChannel); listenerCaptor.getValue().onTouchExplorationStateChanged(false); verify(mockChannel).setAccessibilityFeatures(0); - assertEquals(accessibilityBridge.hasAssistiveTechnology, false); + assertEquals(accessibilityBridge.getAccessibleNavigation(), false); } @Test