Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Issues/79528 reland #28117

Merged
merged 3 commits into from
Aug 16, 2021
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
23 changes: 17 additions & 6 deletions shell/platform/android/io/flutter/view/AccessibilityBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -1020,28 +1020,39 @@ public boolean performAction(
}
case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
{
// Focused semantics node must be reset before sending the
// TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED event. Otherwise,
// TalkBack may think the node is still focused.
if (accessibilityFocusedSemanticsNode != null
&& accessibilityFocusedSemanticsNode.id == virtualViewId) {
accessibilityFocusedSemanticsNode = null;
}
if (embeddedAccessibilityFocusedNodeId != null
&& embeddedAccessibilityFocusedNodeId == virtualViewId) {
embeddedAccessibilityFocusedNodeId = null;
}
accessibilityChannel.dispatchSemanticsAction(
virtualViewId, Action.DID_LOSE_ACCESSIBILITY_FOCUS);
sendAccessibilityEvent(
virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
accessibilityFocusedSemanticsNode = null;
embeddedAccessibilityFocusedNodeId = null;
return true;
}
case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
{
accessibilityChannel.dispatchSemanticsAction(
virtualViewId, Action.DID_GAIN_ACCESSIBILITY_FOCUS);
sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);

if (accessibilityFocusedSemanticsNode == null) {
// When Android focuses a node, it doesn't invalidate the view.
// (It does when it sends ACTION_CLEAR_ACCESSIBILITY_FOCUS, so
// we only have to worry about this when the focused node is null.)
rootAccessibilityView.invalidate();
}
// Focused semantics node must be set before sending the TYPE_VIEW_ACCESSIBILITY_FOCUSED
// event. Otherwise, TalkBack may think the node is not focused yet.
accessibilityFocusedSemanticsNode = semanticsNode;

accessibilityChannel.dispatchSemanticsAction(
virtualViewId, Action.DID_GAIN_ACCESSIBILITY_FOCUS);
sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);

if (semanticsNode.hasAction(Action.INCREASE)
|| semanticsNode.hasAction(Action.DECREASE)) {
// SeekBars only announce themselves after this event.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package io.flutter.view;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.eq;
Expand Down Expand Up @@ -46,6 +47,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
Expand Down Expand Up @@ -798,6 +800,170 @@ public void itCanPredictSetSelection() {
assertEquals(nodeInfo.getTextSelectionEnd(), expectedEnd);
}

@Test
public void itPerformsClearAccessibilityFocusCorrectly() {
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");
AccessibilityBridge accessibilityBridge =
setUpBridge(
/*rootAccessibilityView=*/ mockRootView,
/*accessibilityChannel=*/ mockChannel,
/*accessibilityManager=*/ mockManager,
/*contentResolver=*/ null,
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
/*platformViewsAccessibilityDelegate=*/ null);

ViewParent mockParent = mock(ViewParent.class);
when(mockRootView.getParent()).thenReturn(mockParent);
when(mockManager.isEnabled()).thenReturn(true);

TestSemanticsNode root = new TestSemanticsNode();
root.id = 0;
root.label = "root";
TestSemanticsNode node1 = new TestSemanticsNode();
node1.id = 1;
node1.value = "some text";
root.children.add(node1);

TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
accessibilityBridge.performAction(0, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(0);
assertTrue(nodeInfo.isAccessibilityFocused());
// Clear focus on non-focused node shouldn't do anything
accessibilityBridge.performAction(
1, AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(0);
assertTrue(nodeInfo.isAccessibilityFocused());

// Now, clear the focus for real.
accessibilityBridge.performAction(
0, AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(0);
assertFalse(nodeInfo.isAccessibilityFocused());
}

@Test
public void itSetsFocusedNodeBeforeSendingEvent() {
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");
AccessibilityBridge accessibilityBridge =
setUpBridge(
/*rootAccessibilityView=*/ mockRootView,
/*accessibilityChannel=*/ mockChannel,
/*accessibilityManager=*/ mockManager,
/*contentResolver=*/ null,
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
/*platformViewsAccessibilityDelegate=*/ null);

ViewParent mockParent = mock(ViewParent.class);
when(mockRootView.getParent()).thenReturn(mockParent);
when(mockManager.isEnabled()).thenReturn(true);

TestSemanticsNode root = new TestSemanticsNode();
root.id = 0;
root.label = "root";

TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);

class Verifier {
public Verifier(AccessibilityBridge accessibilityBridge) {
this.accessibilityBridge = accessibilityBridge;
}

public AccessibilityBridge accessibilityBridge;
public boolean verified = false;

public boolean verify(InvocationOnMock invocation) {
AccessibilityEvent event = (AccessibilityEvent) invocation.getArguments()[1];
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
// The accessibility focus must be set before sending out
// the TYPE_VIEW_ACCESSIBILITY_FOCUSED event.
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(0);
assertTrue(nodeInfo.isAccessibilityFocused());
verified = true;
return true;
}
};
Verifier verifier = new Verifier(accessibilityBridge);
when(mockParent.requestSendAccessibilityEvent(eq(mockRootView), any(AccessibilityEvent.class)))
.thenAnswer(invocation -> verifier.verify(invocation));
accessibilityBridge.performAction(0, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
assertTrue(verifier.verified);
}

@Test
public void itClearsFocusedNodeBeforeSendingEvent() {
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");
AccessibilityBridge accessibilityBridge =
setUpBridge(
/*rootAccessibilityView=*/ mockRootView,
/*accessibilityChannel=*/ mockChannel,
/*accessibilityManager=*/ mockManager,
/*contentResolver=*/ null,
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
/*platformViewsAccessibilityDelegate=*/ null);

ViewParent mockParent = mock(ViewParent.class);
when(mockRootView.getParent()).thenReturn(mockParent);
when(mockManager.isEnabled()).thenReturn(true);

TestSemanticsNode root = new TestSemanticsNode();
root.id = 0;
root.label = "root";

TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
// Set the focus on root.
accessibilityBridge.performAction(0, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(0);
assertTrue(nodeInfo.isAccessibilityFocused());

class Verifier {
public Verifier(AccessibilityBridge accessibilityBridge) {
this.accessibilityBridge = accessibilityBridge;
}

public AccessibilityBridge accessibilityBridge;
public boolean verified = false;

public boolean verify(InvocationOnMock invocation) {
AccessibilityEvent event = (AccessibilityEvent) invocation.getArguments()[1];
assertEquals(
event.getEventType(), AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
// The accessibility focus must be cleared before sending out
// the TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED event.
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(0);
assertFalse(nodeInfo.isAccessibilityFocused());
verified = true;
return true;
}
};
Verifier verifier = new Verifier(accessibilityBridge);
when(mockParent.requestSendAccessibilityEvent(eq(mockRootView), any(AccessibilityEvent.class)))
.thenAnswer(invocation -> verifier.verify(invocation));
accessibilityBridge.performAction(
0, AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
assertTrue(verifier.verified);
}

@Test
public void itCanPredictCursorMovementsWithGranularityWord() {
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
Expand Down