Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
// This is null when a node embedded by the AccessibilityViewEmbedder has the focus.
@Nullable SemanticsNode accessibilityFocusedSemanticsNode;

@Nullable private SemanticsNode accessibilityFocusClearingNode;

private final Runnable clearFocusClearingNodeRunnable =
() -> accessibilityFocusClearingNode = null;

// The virtual ID of the currently embedded node with accessibility focus.
//
// This is the ID of a node generated by the AccessibilityViewEmbedder if an embedded node is
Expand Down Expand Up @@ -644,6 +649,8 @@ private static float[] getMatrix4FromBuffer(@NonNull ByteBuffer buffer, float[]
*/
public void release() {
isReleased = true;
rootAccessibilityView.removeCallbacks(clearFocusClearingNodeRunnable);
accessibilityFocusClearingNode = null;
platformViewsAccessibilityDelegate.detachAccessibilityBridge();
setOnAccessibilityChangeListener(null);
accessibilityManager.removeAccessibilityStateChangeListener(accessibilityStateChangeListener);
Expand Down Expand Up @@ -1052,6 +1059,7 @@ public boolean performAction(
// TalkBack may think the node is still focused.
if (accessibilityFocusedSemanticsNode != null
&& accessibilityFocusedSemanticsNode.id == virtualViewId) {
accessibilityFocusClearingNode = accessibilityFocusedSemanticsNode;
accessibilityFocusedSemanticsNode = null;
}
if (embeddedAccessibilityFocusedNodeId != null
Expand All @@ -1062,16 +1070,20 @@ public boolean performAction(
virtualViewId, Action.DID_LOSE_ACCESSIBILITY_FOCUS);
sendAccessibilityEvent(
virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
rootAccessibilityView.removeCallbacks(clearFocusClearingNodeRunnable);
rootAccessibilityView.post(clearFocusClearingNodeRunnable);
return true;
}
case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
{
if (accessibilityFocusedSemanticsNode == null) {
if (accessibilityFocusedSemanticsNode == null && accessibilityFocusClearingNode == 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();
}
accessibilityFocusClearingNode = null;
rootAccessibilityView.removeCallbacks(clearFocusClearingNodeRunnable);
// 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;
Expand Down Expand Up @@ -2008,6 +2020,8 @@ public void reset() {
AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
}
accessibilityFocusedSemanticsNode = null;
rootAccessibilityView.removeCallbacks(clearFocusClearingNodeRunnable);
accessibilityFocusClearingNode = null;
hoveredObject = null;
sendWindowContentChangeEvent(0, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
Expand Down Expand Up @@ -3280,6 +3281,62 @@ public void itDoesNotSetHeadingWhenHeadingLevelIsZero() {
assertFalse(nonHeadingInfo.isHeading());
}

@Test
public void itDoesNotRedundantlyInvalidateRootViewWhenFocusChanges() {
BasicMessageChannel mockChannel = mock(BasicMessageChannel.class);
AccessibilityChannel accessibilityChannel =
new AccessibilityChannel(mockChannel, mock(FlutterJNI.class));
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
AccessibilityManager mockManager = mock(AccessibilityManager.class);
when(mockManager.isEnabled()).thenReturn(true);
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= */ accessibilityChannel,
/* accessibilityManager= */ mockManager,
/* contentResolver= */ null,
/* accessibilityViewEmbedder= */ mockViewEmbedder,
/* platformViewsAccessibilityDelegate= */ null);

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

// Create two semantics nodes
TestSemanticsNode root = new TestSemanticsNode();
root.id = 0;
root.label = "root";
TestSemanticsNode node1 = new TestSemanticsNode();
node1.id = 1;
node1.value = "node1";
root.children.add(node1);
TestSemanticsNode node2 = new TestSemanticsNode();
node2.id = 2;
node2.value = "node2";
root.children.add(node2);

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

// 1. Initial focus on node1 (nothing was focused before).
// This should trigger invalidate() on rootAccessibilityView.
accessibilityBridge.performAction(1, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
verify(mockRootView, times(1)).invalidate();

// 2. Clear focus on node1, and then focus node2 (focus shifting).
// This should NOT trigger invalidate() on rootAccessibilityView because Android
// invalidates the view when focus is cleared, making it redundant to invalidate again when
// focusing.
clearInvocations(mockRootView);
accessibilityBridge.performAction(
1, AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
accessibilityBridge.performAction(2, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
verify(mockRootView, never()).invalidate();
}

AccessibilityBridge setUpBridge() {
return setUpBridge(null, null, null, null, null, null);
}
Expand Down
Loading