Skip to content

Commit aed8e01

Browse files
authored
Fixes Edge trigger route change announcement (#21975)
1 parent 1233fe4 commit aed8e01

File tree

4 files changed

+326
-3
lines changed

4 files changed

+326
-3
lines changed

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1356,16 +1356,29 @@ void updateSemantics(@NonNull ByteBuffer buffer, @NonNull String[] strings) {
13561356

13571357
// Dispatch a TYPE_WINDOW_STATE_CHANGED event if the most recent route id changed from the
13581358
// previously cached route id.
1359+
1360+
// Finds the last route that is not in the previous routes.
13591361
SemanticsNode lastAdded = null;
13601362
for (SemanticsNode semanticsNode : newRoutes) {
13611363
if (!flutterNavigationStack.contains(semanticsNode.id)) {
13621364
lastAdded = semanticsNode;
13631365
}
13641366
}
1367+
1368+
// If all the routes are in the previous route, get the last route.
13651369
if (lastAdded == null && newRoutes.size() > 0) {
13661370
lastAdded = newRoutes.get(newRoutes.size() - 1);
13671371
}
1368-
if (lastAdded != null && lastAdded.id != previousRouteId) {
1372+
1373+
// There are two cases if lastAdded != nil
1374+
// 1. lastAdded is not in previous routes. In this case,
1375+
// lastAdded.id != previousRouteId
1376+
// 2. All new routes are in previous routes and
1377+
// lastAdded = newRoutes.last.
1378+
// In the first case, we need to announce new route. In the second case,
1379+
// we need to announce if one list is shorter than the other.
1380+
if (lastAdded != null
1381+
&& (lastAdded.id != previousRouteId || newRoutes.size() != flutterNavigationStack.size())) {
13691382
previousRouteId = lastAdded.id;
13701383
sendWindowChangeEvent(lastAdded);
13711384
}

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

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,134 @@ public void itUnfocusesPlatformViewWhenPlatformViewGoesAway() {
142142
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
143143
}
144144

145+
@Test
146+
public void itAnnouncesRouteNameWhenAddingNewRoute() {
147+
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
148+
AccessibilityManager mockManager = mock(AccessibilityManager.class);
149+
View mockRootView = mock(View.class);
150+
Context context = mock(Context.class);
151+
when(mockRootView.getContext()).thenReturn(context);
152+
when(context.getPackageName()).thenReturn("test");
153+
AccessibilityBridge accessibilityBridge =
154+
setUpBridge(mockRootView, mockManager, mockViewEmbedder);
155+
ViewParent mockParent = mock(ViewParent.class);
156+
when(mockRootView.getParent()).thenReturn(mockParent);
157+
when(mockManager.isEnabled()).thenReturn(true);
158+
159+
TestSemanticsNode root = new TestSemanticsNode();
160+
root.id = 0;
161+
TestSemanticsNode node1 = new TestSemanticsNode();
162+
node1.id = 1;
163+
node1.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
164+
node1.addFlag(AccessibilityBridge.Flag.NAMES_ROUTE);
165+
node1.label = "node1";
166+
root.children.add(node1);
167+
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
168+
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
169+
170+
ArgumentCaptor<AccessibilityEvent> eventCaptor =
171+
ArgumentCaptor.forClass(AccessibilityEvent.class);
172+
verify(mockParent, times(2))
173+
.requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
174+
AccessibilityEvent event = eventCaptor.getAllValues().get(0);
175+
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
176+
List<CharSequence> sentences = event.getText();
177+
assertEquals(sentences.size(), 1);
178+
assertEquals(sentences.get(0).toString(), "node1");
179+
180+
TestSemanticsNode new_root = new TestSemanticsNode();
181+
new_root.id = 0;
182+
TestSemanticsNode new_node1 = new TestSemanticsNode();
183+
new_node1.id = 1;
184+
new_node1.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
185+
new_node1.addFlag(AccessibilityBridge.Flag.NAMES_ROUTE);
186+
new_node1.label = "new_node1";
187+
new_root.children.add(new_node1);
188+
TestSemanticsNode new_node2 = new TestSemanticsNode();
189+
new_node2.id = 2;
190+
new_node2.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
191+
new_node2.addFlag(AccessibilityBridge.Flag.NAMES_ROUTE);
192+
new_node2.label = "new_node2";
193+
new_node1.children.add(new_node2);
194+
testSemanticsUpdate = new_root.toUpdate();
195+
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
196+
197+
eventCaptor = ArgumentCaptor.forClass(AccessibilityEvent.class);
198+
verify(mockParent, times(4))
199+
.requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
200+
event = eventCaptor.getAllValues().get(2);
201+
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
202+
sentences = event.getText();
203+
assertEquals(sentences.size(), 1);
204+
assertEquals(sentences.get(0).toString(), "new_node2");
205+
}
206+
207+
@Test
208+
public void itAnnouncesRouteNameWhenRemoveARoute() {
209+
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);
210+
AccessibilityManager mockManager = mock(AccessibilityManager.class);
211+
View mockRootView = mock(View.class);
212+
Context context = mock(Context.class);
213+
when(mockRootView.getContext()).thenReturn(context);
214+
when(context.getPackageName()).thenReturn("test");
215+
AccessibilityBridge accessibilityBridge =
216+
setUpBridge(mockRootView, mockManager, mockViewEmbedder);
217+
ViewParent mockParent = mock(ViewParent.class);
218+
when(mockRootView.getParent()).thenReturn(mockParent);
219+
when(mockManager.isEnabled()).thenReturn(true);
220+
221+
TestSemanticsNode root = new TestSemanticsNode();
222+
root.id = 0;
223+
TestSemanticsNode node1 = new TestSemanticsNode();
224+
node1.id = 1;
225+
node1.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
226+
node1.addFlag(AccessibilityBridge.Flag.NAMES_ROUTE);
227+
node1.label = "node1";
228+
root.children.add(node1);
229+
TestSemanticsNode node2 = new TestSemanticsNode();
230+
node2.id = 2;
231+
node2.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
232+
node2.addFlag(AccessibilityBridge.Flag.NAMES_ROUTE);
233+
node2.label = "node2";
234+
node1.children.add(node2);
235+
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
236+
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
237+
238+
ArgumentCaptor<AccessibilityEvent> eventCaptor =
239+
ArgumentCaptor.forClass(AccessibilityEvent.class);
240+
verify(mockParent, times(2))
241+
.requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
242+
AccessibilityEvent event = eventCaptor.getAllValues().get(0);
243+
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
244+
List<CharSequence> sentences = event.getText();
245+
assertEquals(sentences.size(), 1);
246+
assertEquals(sentences.get(0).toString(), "node2");
247+
248+
TestSemanticsNode new_root = new TestSemanticsNode();
249+
new_root.id = 0;
250+
TestSemanticsNode new_node1 = new TestSemanticsNode();
251+
new_node1.id = 1;
252+
new_node1.label = "new_node1";
253+
new_root.children.add(new_node1);
254+
TestSemanticsNode new_node2 = new TestSemanticsNode();
255+
new_node2.id = 2;
256+
new_node2.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
257+
new_node2.addFlag(AccessibilityBridge.Flag.NAMES_ROUTE);
258+
new_node2.label = "new_node2";
259+
new_node1.children.add(new_node2);
260+
testSemanticsUpdate = new_root.toUpdate();
261+
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
262+
263+
eventCaptor = ArgumentCaptor.forClass(AccessibilityEvent.class);
264+
verify(mockParent, times(4))
265+
.requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
266+
event = eventCaptor.getAllValues().get(2);
267+
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
268+
sentences = event.getText();
269+
assertEquals(sentences.size(), 1);
270+
assertEquals(sentences.get(0).toString(), "new_node2");
271+
}
272+
145273
@Test
146274
public void itAnnouncesWhiteSpaceWhenNoNamesRoute() {
147275
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);

shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,17 +167,27 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification,
167167
}
168168
NSMutableArray<SemanticsObject*>* newRoutes = [[[NSMutableArray alloc] init] autorelease];
169169
[root collectRoutes:newRoutes];
170+
// Finds the last route that is not in the previous routes.
170171
for (SemanticsObject* route in newRoutes) {
171-
if (std::find(previous_routes_.begin(), previous_routes_.end(), [route uid]) !=
172+
if (std::find(previous_routes_.begin(), previous_routes_.end(), [route uid]) ==
172173
previous_routes_.end()) {
173174
lastAdded = route;
174175
}
175176
}
177+
// If all the routes are in the previous route, get the last route.
176178
if (lastAdded == nil && [newRoutes count] > 0) {
177179
int index = [newRoutes count] - 1;
178180
lastAdded = [newRoutes objectAtIndex:index];
179181
}
180-
if (lastAdded != nil && [lastAdded uid] != previous_route_id_) {
182+
// There are two cases if lastAdded != nil
183+
// 1. lastAdded is not in previous routes. In this case,
184+
// [lastAdded uid] != previous_route_id_
185+
// 2. All new routes are in previous routes and
186+
// lastAdded = newRoutes.last.
187+
// In the first case, we need to announce new route. In the second case,
188+
// we need to announce if one list is shorter than the other.
189+
if (lastAdded != nil &&
190+
([lastAdded uid] != previous_route_id_ || [newRoutes count] != previous_routes_.size())) {
181191
previous_route_id_ = [lastAdded uid];
182192
routeChanged = true;
183193
}

shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,178 @@ - (void)testAnnouncesRouteChanges {
333333
UIAccessibilityScreenChangedNotification);
334334
}
335335

336+
- (void)testAnnouncesRouteChangesWhenAddAdditionalRoute {
337+
flutter::MockDelegate mock_delegate;
338+
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
339+
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
340+
/*platform=*/thread_task_runner,
341+
/*raster=*/thread_task_runner,
342+
/*ui=*/thread_task_runner,
343+
/*io=*/thread_task_runner);
344+
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
345+
/*delegate=*/mock_delegate,
346+
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
347+
/*task_runners=*/runners);
348+
id mockFlutterView = OCMClassMock([FlutterView class]);
349+
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
350+
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
351+
352+
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
353+
[[[NSMutableArray alloc] init] autorelease];
354+
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
355+
ios_delegate->on_PostAccessibilityNotification_ =
356+
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
357+
[accessibility_notifications addObject:@{
358+
@"notification" : @(notification),
359+
@"argument" : argument ? argument : [NSNull null],
360+
}];
361+
};
362+
__block auto bridge =
363+
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
364+
/*platform_view=*/platform_view.get(),
365+
/*platform_views_controller=*/nil,
366+
/*ios_delegate=*/std::move(ios_delegate));
367+
368+
flutter::CustomAccessibilityActionUpdates actions;
369+
flutter::SemanticsNodeUpdates nodes;
370+
371+
flutter::SemanticsNode node1;
372+
node1.id = 1;
373+
node1.label = "node1";
374+
node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
375+
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
376+
nodes[node1.id] = node1;
377+
flutter::SemanticsNode root_node;
378+
root_node.id = kRootNodeId;
379+
root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
380+
root_node.childrenInTraversalOrder = {1};
381+
root_node.childrenInHitTestOrder = {1};
382+
nodes[root_node.id] = root_node;
383+
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
384+
385+
XCTAssertEqual([accessibility_notifications count], 1ul);
386+
XCTAssertEqualObjects(accessibility_notifications[0][@"argument"], @"node1");
387+
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
388+
UIAccessibilityScreenChangedNotification);
389+
390+
flutter::SemanticsNodeUpdates new_nodes;
391+
392+
flutter::SemanticsNode new_node1;
393+
new_node1.id = 1;
394+
new_node1.label = "new_node1";
395+
new_node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
396+
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
397+
new_node1.childrenInTraversalOrder = {2};
398+
new_node1.childrenInHitTestOrder = {2};
399+
new_nodes[new_node1.id] = new_node1;
400+
flutter::SemanticsNode new_node2;
401+
new_node2.id = 2;
402+
new_node2.label = "new_node2";
403+
new_node2.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
404+
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
405+
new_nodes[new_node2.id] = new_node2;
406+
flutter::SemanticsNode new_root_node;
407+
new_root_node.id = kRootNodeId;
408+
new_root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
409+
new_root_node.childrenInTraversalOrder = {1};
410+
new_root_node.childrenInHitTestOrder = {1};
411+
new_nodes[new_root_node.id] = new_root_node;
412+
bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
413+
XCTAssertEqual([accessibility_notifications count], 2ul);
414+
XCTAssertEqualObjects(accessibility_notifications[1][@"argument"], @"new_node2");
415+
XCTAssertEqual([accessibility_notifications[1][@"notification"] unsignedIntValue],
416+
UIAccessibilityScreenChangedNotification);
417+
}
418+
419+
- (void)testAnnouncesRouteChangesRemoveRouteInMiddle {
420+
flutter::MockDelegate mock_delegate;
421+
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
422+
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
423+
/*platform=*/thread_task_runner,
424+
/*raster=*/thread_task_runner,
425+
/*ui=*/thread_task_runner,
426+
/*io=*/thread_task_runner);
427+
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
428+
/*delegate=*/mock_delegate,
429+
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
430+
/*task_runners=*/runners);
431+
id mockFlutterView = OCMClassMock([FlutterView class]);
432+
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
433+
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
434+
435+
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
436+
[[[NSMutableArray alloc] init] autorelease];
437+
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
438+
ios_delegate->on_PostAccessibilityNotification_ =
439+
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
440+
[accessibility_notifications addObject:@{
441+
@"notification" : @(notification),
442+
@"argument" : argument ? argument : [NSNull null],
443+
}];
444+
};
445+
__block auto bridge =
446+
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
447+
/*platform_view=*/platform_view.get(),
448+
/*platform_views_controller=*/nil,
449+
/*ios_delegate=*/std::move(ios_delegate));
450+
451+
flutter::CustomAccessibilityActionUpdates actions;
452+
flutter::SemanticsNodeUpdates nodes;
453+
454+
flutter::SemanticsNode node1;
455+
node1.id = 1;
456+
node1.label = "node1";
457+
node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
458+
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
459+
node1.childrenInTraversalOrder = {2};
460+
node1.childrenInHitTestOrder = {2};
461+
nodes[node1.id] = node1;
462+
flutter::SemanticsNode node2;
463+
node2.id = 2;
464+
node2.label = "node2";
465+
node2.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
466+
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
467+
nodes[node2.id] = node2;
468+
flutter::SemanticsNode root_node;
469+
root_node.id = kRootNodeId;
470+
root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
471+
root_node.childrenInTraversalOrder = {1};
472+
root_node.childrenInHitTestOrder = {1};
473+
nodes[root_node.id] = root_node;
474+
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
475+
476+
XCTAssertEqual([accessibility_notifications count], 1ul);
477+
XCTAssertEqualObjects(accessibility_notifications[0][@"argument"], @"node2");
478+
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
479+
UIAccessibilityScreenChangedNotification);
480+
481+
flutter::SemanticsNodeUpdates new_nodes;
482+
483+
flutter::SemanticsNode new_node1;
484+
new_node1.id = 1;
485+
new_node1.label = "new_node1";
486+
new_node1.childrenInTraversalOrder = {2};
487+
new_node1.childrenInHitTestOrder = {2};
488+
new_nodes[new_node1.id] = new_node1;
489+
flutter::SemanticsNode new_node2;
490+
new_node2.id = 2;
491+
new_node2.label = "new_node2";
492+
new_node2.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
493+
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
494+
new_nodes[new_node2.id] = new_node2;
495+
flutter::SemanticsNode new_root_node;
496+
new_root_node.id = kRootNodeId;
497+
new_root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
498+
new_root_node.childrenInTraversalOrder = {1};
499+
new_root_node.childrenInHitTestOrder = {1};
500+
new_nodes[new_root_node.id] = new_root_node;
501+
bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
502+
XCTAssertEqual([accessibility_notifications count], 2ul);
503+
XCTAssertEqualObjects(accessibility_notifications[1][@"argument"], @"new_node2");
504+
XCTAssertEqual([accessibility_notifications[1][@"notification"] unsignedIntValue],
505+
UIAccessibilityScreenChangedNotification);
506+
}
507+
336508
- (void)testAnnouncesRouteChangesWhenNoNamesRoute {
337509
flutter::MockDelegate mock_delegate;
338510
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");

0 commit comments

Comments
 (0)