Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 28 additions & 6 deletions shell/platform/android/io/flutter/view/AccessibilityBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -331,6 +355,7 @@ public void onAccessibilityStateChanged(boolean accessibilityEnabled) {
accessibilityChannel.setAccessibilityMessageHandler(accessibilityMessageHandler);
accessibilityChannel.onAndroidAccessibilityEnabled();
} else {
setAccessibleNavigation(false);
accessibilityChannel.setAccessibilityMessageHandler(null);
accessibilityChannel.onAndroidAccessibilityDisabled();
}
Expand Down Expand Up @@ -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());
Expand All @@ -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(
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AccessibilityManager.TouchExplorationStateChangeListener> 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();
Expand Down