Skip to content

Commit a7899f0

Browse files
authored
Issues/79528 reland (flutter#28117)
1 parent 90c9a91 commit a7899f0

File tree

2 files changed

+183
-6
lines changed

2 files changed

+183
-6
lines changed

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,28 +1020,39 @@ 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+
if (accessibilityFocusedSemanticsNode != null
1027+
&& accessibilityFocusedSemanticsNode.id == virtualViewId) {
1028+
accessibilityFocusedSemanticsNode = null;
1029+
}
1030+
if (embeddedAccessibilityFocusedNodeId != null
1031+
&& embeddedAccessibilityFocusedNodeId == virtualViewId) {
1032+
embeddedAccessibilityFocusedNodeId = null;
1033+
}
10231034
accessibilityChannel.dispatchSemanticsAction(
10241035
virtualViewId, Action.DID_LOSE_ACCESSIBILITY_FOCUS);
10251036
sendAccessibilityEvent(
10261037
virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
1027-
accessibilityFocusedSemanticsNode = null;
1028-
embeddedAccessibilityFocusedNodeId = null;
10291038
return true;
10301039
}
10311040
case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS:
10321041
{
1033-
accessibilityChannel.dispatchSemanticsAction(
1034-
virtualViewId, Action.DID_GAIN_ACCESSIBILITY_FOCUS);
1035-
sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
1036-
10371042
if (accessibilityFocusedSemanticsNode == null) {
10381043
// When Android focuses a node, it doesn't invalidate the view.
10391044
// (It does when it sends ACTION_CLEAR_ACCESSIBILITY_FOCUS, so
10401045
// we only have to worry about this when the focused node is null.)
10411046
rootAccessibilityView.invalidate();
10421047
}
1048+
// Focused semantics node must be set before sending the TYPE_VIEW_ACCESSIBILITY_FOCUSED
1049+
// event. Otherwise, TalkBack may think the node is not focused yet.
10431050
accessibilityFocusedSemanticsNode = semanticsNode;
10441051

1052+
accessibilityChannel.dispatchSemanticsAction(
1053+
virtualViewId, Action.DID_GAIN_ACCESSIBILITY_FOCUS);
1054+
sendAccessibilityEvent(virtualViewId, AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
1055+
10451056
if (semanticsNode.hasAction(Action.INCREASE)
10461057
|| semanticsNode.hasAction(Action.DECREASE)) {
10471058
// SeekBars only announce themselves after this event.

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

Lines changed: 166 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,170 @@ public void itCanPredictSetSelection() {
798800
assertEquals(nodeInfo.getTextSelectionEnd(), expectedEnd);
799801
}
800802

803+
@Test
804+
public void itPerformsClearAccessibilityFocusCorrectly() {
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+
TestSemanticsNode node1 = new TestSemanticsNode();
829+
node1.id = 1;
830+
node1.value = "some text";
831+
root.children.add(node1);
832+
833+
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
834+
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
835+
accessibilityBridge.performAction(0, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
836+
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(0);
837+
assertTrue(nodeInfo.isAccessibilityFocused());
838+
// Clear focus on non-focused node shouldn't do anything
839+
accessibilityBridge.performAction(
840+
1, AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
841+
nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(0);
842+
assertTrue(nodeInfo.isAccessibilityFocused());
843+
844+
// Now, clear the focus for real.
845+
accessibilityBridge.performAction(
846+
0, AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
847+
nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(0);
848+
assertFalse(nodeInfo.isAccessibilityFocused());
849+
}
850+
851+
@Test
852+
public void itSetsFocusedNodeBeforeSendingEvent() {
853+
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
854+
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
855+
AccessibilityManager mockManager = mock(AccessibilityManager.class);
856+
View mockRootView = mock(View.class);
857+
Context context = mock(Context.class);
858+
when(mockRootView.getContext()).thenReturn(context);
859+
when(context.getPackageName()).thenReturn("test");
860+
AccessibilityBridge accessibilityBridge =
861+
setUpBridge(
862+
/*rootAccessibilityView=*/ mockRootView,
863+
/*accessibilityChannel=*/ mockChannel,
864+
/*accessibilityManager=*/ mockManager,
865+
/*contentResolver=*/ null,
866+
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
867+
/*platformViewsAccessibilityDelegate=*/ null);
868+
869+
ViewParent mockParent = mock(ViewParent.class);
870+
when(mockRootView.getParent()).thenReturn(mockParent);
871+
when(mockManager.isEnabled()).thenReturn(true);
872+
873+
TestSemanticsNode root = new TestSemanticsNode();
874+
root.id = 0;
875+
root.label = "root";
876+
877+
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
878+
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
879+
880+
class Verifier {
881+
public Verifier(AccessibilityBridge accessibilityBridge) {
882+
this.accessibilityBridge = accessibilityBridge;
883+
}
884+
885+
public AccessibilityBridge accessibilityBridge;
886+
public boolean verified = false;
887+
888+
public boolean verify(InvocationOnMock invocation) {
889+
AccessibilityEvent event = (AccessibilityEvent) invocation.getArguments()[1];
890+
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
891+
// The accessibility focus must be set before sending out
892+
// the TYPE_VIEW_ACCESSIBILITY_FOCUSED event.
893+
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(0);
894+
assertTrue(nodeInfo.isAccessibilityFocused());
895+
verified = true;
896+
return true;
897+
}
898+
};
899+
Verifier verifier = new Verifier(accessibilityBridge);
900+
when(mockParent.requestSendAccessibilityEvent(eq(mockRootView), any(AccessibilityEvent.class)))
901+
.thenAnswer(invocation -> verifier.verify(invocation));
902+
accessibilityBridge.performAction(0, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
903+
assertTrue(verifier.verified);
904+
}
905+
906+
@Test
907+
public void itClearsFocusedNodeBeforeSendingEvent() {
908+
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);
909+
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
910+
AccessibilityManager mockManager = mock(AccessibilityManager.class);
911+
View mockRootView = mock(View.class);
912+
Context context = mock(Context.class);
913+
when(mockRootView.getContext()).thenReturn(context);
914+
when(context.getPackageName()).thenReturn("test");
915+
AccessibilityBridge accessibilityBridge =
916+
setUpBridge(
917+
/*rootAccessibilityView=*/ mockRootView,
918+
/*accessibilityChannel=*/ mockChannel,
919+
/*accessibilityManager=*/ mockManager,
920+
/*contentResolver=*/ null,
921+
/*accessibilityViewEmbedder=*/ mockViewEmbedder,
922+
/*platformViewsAccessibilityDelegate=*/ null);
923+
924+
ViewParent mockParent = mock(ViewParent.class);
925+
when(mockRootView.getParent()).thenReturn(mockParent);
926+
when(mockManager.isEnabled()).thenReturn(true);
927+
928+
TestSemanticsNode root = new TestSemanticsNode();
929+
root.id = 0;
930+
root.label = "root";
931+
932+
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
933+
testSemanticsUpdate.sendUpdateToBridge(accessibilityBridge);
934+
// Set the focus on root.
935+
accessibilityBridge.performAction(0, AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
936+
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(0);
937+
assertTrue(nodeInfo.isAccessibilityFocused());
938+
939+
class Verifier {
940+
public Verifier(AccessibilityBridge accessibilityBridge) {
941+
this.accessibilityBridge = accessibilityBridge;
942+
}
943+
944+
public AccessibilityBridge accessibilityBridge;
945+
public boolean verified = false;
946+
947+
public boolean verify(InvocationOnMock invocation) {
948+
AccessibilityEvent event = (AccessibilityEvent) invocation.getArguments()[1];
949+
assertEquals(
950+
event.getEventType(), AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
951+
// The accessibility focus must be cleared before sending out
952+
// the TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED event.
953+
AccessibilityNodeInfo nodeInfo = accessibilityBridge.createAccessibilityNodeInfo(0);
954+
assertFalse(nodeInfo.isAccessibilityFocused());
955+
verified = true;
956+
return true;
957+
}
958+
};
959+
Verifier verifier = new Verifier(accessibilityBridge);
960+
when(mockParent.requestSendAccessibilityEvent(eq(mockRootView), any(AccessibilityEvent.class)))
961+
.thenAnswer(invocation -> verifier.verify(invocation));
962+
accessibilityBridge.performAction(
963+
0, AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS, null);
964+
assertTrue(verifier.verified);
965+
}
966+
801967
@Test
802968
public void itCanPredictCursorMovementsWithGranularityWord() {
803969
AccessibilityChannel mockChannel = mock(AccessibilityChannel.class);

0 commit comments

Comments
 (0)