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

Commit a1fcd39

Browse files
committed
Sets focus before sending a11y focus event in Android
1 parent 6291be0 commit a1fcd39

File tree

2 files changed

+129
-6
lines changed

2 files changed

+129
-6
lines changed

shell/platform/android/io/flutter/view/AccessibilityBridge.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,28 +1020,33 @@ public boolean performAction(
10201020
}
10211021
case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS:
10221022
{
1023+
// Focused semantics node must be reset before sending the
1024+
// TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED event. Otherwise,
1025+
// TalkBack may think the node is still focused.
1026+
accessibilityFocusedSemanticsNode = null;
1027+
embeddedAccessibilityFocusedNodeId = null;
10231028
accessibilityChannel.dispatchSemanticsAction(
10241029
virtualViewId, Action.DID_LOSE_ACCESSIBILITY_FOCUS);
10251030
sendAccessibilityEvent(
10261031
virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
1027-
accessibilityFocusedSemanticsNode = null;
1028-
embeddedAccessibilityFocusedNodeId = null;
10291032
return true;
10301033
}
10311034
case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
10321035
{
1033-
accessibilityChannel.dispatchSemanticsAction(
1034-
virtualViewId, Action.DID_GAIN_ACCESSIBILITY_FOCUS);
1035-
sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
1036-
10371036
if (accessibilityFocusedSemanticsNode == null) {
10381037
// When Android focuses a node, it doesn't invalidate the view.
10391038
// (It does when it sends ACTION_CLEAR_ACCESSIBILITY_FOCUS, so
10401039
// we only have to worry about this when the focused node is null.)
10411040
rootAccessibilityView.invalidate();
10421041
}
1042+
// Focused semantics node must be set before sending the TYPE_VIEW_ACCESSIBILITY_FOCUSED
1043+
// event. Otherwise, TalkBack may think the node is not focused yet.
10431044
accessibilityFocusedSemanticsNode = semanticsNode;
10441045

1046+
accessibilityChannel.dispatchSemanticsAction(
1047+
virtualViewId, Action.DID_GAIN_ACCESSIBILITY_FOCUS);
1048+
sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
1049+
10451050
if (semanticsNode.hasAction(Action.INCREASE)
10461051
|| semanticsNode.hasAction(Action.DECREASE)) {
10471052
// SeekBars only announce themselves after this event.

shell/platform/android/test/io/flutter/view/AccessibilityBridgeTest.java

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package io.flutter.view;
66

77
import static org.junit.Assert.assertEquals;
8+
import static org.junit.Assert.assertFalse;
89
import static org.junit.Assert.assertNotNull;
910
import static org.junit.Assert.assertTrue;
1011
import static org.mockito.Matchers.eq;
@@ -46,6 +47,7 @@
4647
import org.junit.Test;
4748
import org.junit.runner.RunWith;
4849
import org.mockito.ArgumentCaptor;
50+
import org.mockito.invocation.InvocationOnMock;
4951
import org.robolectric.RobolectricTestRunner;
5052
import org.robolectric.RuntimeEnvironment;
5153
import org.robolectric.annotation.Config;
@@ -798,6 +800,122 @@ public void itCanPredictSetSelection() {
798800
assertEquals(nodeInfo.getTextSelectionEnd(), expectedEnd);
799801
}
800802

803+
@Test
804+
public void itSetsFocusedNodeBeforeSendingEvent() {
805+
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
806+
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
807+
AccessibilityManager mockManager = mock(AccessibilityManager.class);
808+
View mockRootView = mock(View.class);
809+
Context context = mock(Context.class);
810+
when(mockRootView.getContext()).thenReturn(context);
811+
when(context.getPackageName()).thenReturn("test");
812+
AccessibilityBridge accessibilityBridge =
813+
setUpBridge(
814+
/*rootAccessibilityView=*/ mockRootView,
815+
/*accessibilityChannel=*/ mockChannel,
816+
/*accessibilityManager=*/ mockManager,
817+
/*contentResolver=*/ null,
818+
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
819+
/*platformViewsAccessibilityDelegate=*/ null);
820+
821+
ViewParent mockParent = mock(ViewParent.class);
822+
when(mockRootView.getParent()).thenReturn(mockParent);
823+
when(mockManager.isEnabled()).thenReturn(true);
824+
825+
TestSemanticsNode root = new TestSemanticsNode();
826+
root.id = 0;
827+
root.label = "root";
828+
829+
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
830+
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
831+
832+
class Verifier {
833+
public Verifier(AccessibilityBridge accessibilityBridge) {
834+
this.accessibilityBridge = accessibilityBridge;
835+
}
836+
837+
public AccessibilityBridge accessibilityBridge;
838+
public boolean verified = false;
839+
840+
public boolean verify(InvocationOnMock invocation) {
841+
AccessibilityEvent event = (AccessibilityEvent) invocation.getArguments()[1];
842+
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
843+
// The accessibility focus must be set before sending out
844+
// the TYPE_VIEW_ACCESSIBILITY_FOCUSED event.
845+
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(0);
846+
assertTrue(nodeInfo.isAccessibilityFocused());
847+
verified = true;
848+
return true;
849+
}
850+
};
851+
Verifier verifier = new Verifier(accessibilityBridge);
852+
when(mockParent.requestSendAccessibilityEvent(eq(mockRootView), any(AccessibilityEvent.class)))
853+
.thenAnswer(invocation -> verifier.verify(invocation));
854+
accessibilityBridge.performAction(0, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
855+
assertTrue(verifier.verified);
856+
}
857+
858+
@Test
859+
public void itClearsFocusedNodeBeforeSendingEvent() {
860+
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
861+
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
862+
AccessibilityManager mockManager = mock(AccessibilityManager.class);
863+
View mockRootView = mock(View.class);
864+
Context context = mock(Context.class);
865+
when(mockRootView.getContext()).thenReturn(context);
866+
when(context.getPackageName()).thenReturn("test");
867+
AccessibilityBridge accessibilityBridge =
868+
setUpBridge(
869+
/*rootAccessibilityView=*/ mockRootView,
870+
/*accessibilityChannel=*/ mockChannel,
871+
/*accessibilityManager=*/ mockManager,
872+
/*contentResolver=*/ null,
873+
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
874+
/*platformViewsAccessibilityDelegate=*/ null);
875+
876+
ViewParent mockParent = mock(ViewParent.class);
877+
when(mockRootView.getParent()).thenReturn(mockParent);
878+
when(mockManager.isEnabled()).thenReturn(true);
879+
880+
TestSemanticsNode root = new TestSemanticsNode();
881+
root.id = 0;
882+
root.label = "root";
883+
884+
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
885+
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
886+
// Set the focus on root.
887+
accessibilityBridge.performAction(0, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
888+
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(0);
889+
assertTrue(nodeInfo.isAccessibilityFocused());
890+
891+
class Verifier {
892+
public Verifier(AccessibilityBridge accessibilityBridge) {
893+
this.accessibilityBridge = accessibilityBridge;
894+
}
895+
896+
public AccessibilityBridge accessibilityBridge;
897+
public boolean verified = false;
898+
899+
public boolean verify(InvocationOnMock invocation) {
900+
AccessibilityEvent event = (AccessibilityEvent) invocation.getArguments()[1];
901+
assertEquals(
902+
event.getEventType(), AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
903+
// The accessibility focus must be cleared before sending out
904+
// the TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED event.
905+
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(0);
906+
assertFalse(nodeInfo.isAccessibilityFocused());
907+
verified = true;
908+
return true;
909+
}
910+
};
911+
Verifier verifier = new Verifier(accessibilityBridge);
912+
when(mockParent.requestSendAccessibilityEvent(eq(mockRootView), any(AccessibilityEvent.class)))
913+
.thenAnswer(invocation -> verifier.verify(invocation));
914+
accessibilityBridge.performAction(
915+
0, AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
916+
assertTrue(verifier.verified);
917+
}
918+
801919
@Test
802920
public void itCanPredictCursorMovementsWithGranularityWord() {
803921
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);

0 commit comments

Comments
 (0)