Fixes Edge trigger route change announcement (#21975)

This commit is contained in:
chunhtai 2020-10-22 14:22:03 -07:00 committed by GitHub
parent 1233fe4902
commit aed8e019be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 326 additions and 3 deletions

View File

@ -1356,16 +1356,29 @@ public class AccessibilityBridge extends AccessibilityNodeProvider {
// Dispatch a TYPE_WINDOW_STATE_CHANGED event if the most recent route id changed from the
// previously cached route id.
// Finds the last route that is not in the previous routes.
SemanticsNode lastAdded = null;
for (SemanticsNode semanticsNode : newRoutes) {
if (!flutterNavigationStack.contains(semanticsNode.id)) {
lastAdded = semanticsNode;
}
}
// If all the routes are in the previous route, get the last route.
if (lastAdded == null && newRoutes.size() > 0) {
lastAdded = newRoutes.get(newRoutes.size() - 1);
}
if (lastAdded != null && lastAdded.id != previousRouteId) {
// There are two cases if lastAdded != nil
// 1. lastAdded is not in previous routes. In this case,
// lastAdded.id != previousRouteId
// 2. All new routes are in previous routes and
// lastAdded = newRoutes.last.
// In the first case, we need to announce new route. In the second case,
// we need to announce if one list is shorter than the other.
if (lastAdded != null
&& (lastAdded.id != previousRouteId || newRoutes.size() != flutterNavigationStack.size())) {
previousRouteId = lastAdded.id;
sendWindowChangeEvent(lastAdded);
}

View File

@ -142,6 +142,134 @@ public class AccessibilityBridgeTest {
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
}
@Test
public void itAnnouncesRouteNameWhenAddingNewRoute() {
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(mockRootView, mockManager, mockViewEmbedder);
ViewParent mockParent = mock(ViewParent.class);
when(mockRootView.getParent()).thenReturn(mockParent);
when(mockManager.isEnabled()).thenReturn(true);
TestSemanticsNode root = new TestSemanticsNode();
root.id = 0;
TestSemanticsNode node1 = new TestSemanticsNode();
node1.id = 1;
node1.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
node1.addFlag(AccessibilityBridge.Flag.NAMES_ROUTE);
node1.label = "node1";
root.children.add(node1);
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
ArgumentCaptor<AccessibilityEvent> eventCaptor =
ArgumentCaptor.forClass(AccessibilityEvent.class);
verify(mockParent, times(2))
.requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
AccessibilityEvent event = eventCaptor.getAllValues().get(0);
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
List<CharSequence> sentences = event.getText();
assertEquals(sentences.size(), 1);
assertEquals(sentences.get(0).toString(), "node1");
TestSemanticsNode new_root = new TestSemanticsNode();
new_root.id = 0;
TestSemanticsNode new_node1 = new TestSemanticsNode();
new_node1.id = 1;
new_node1.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
new_node1.addFlag(AccessibilityBridge.Flag.NAMES_ROUTE);
new_node1.label = "new_node1";
new_root.children.add(new_node1);
TestSemanticsNode new_node2 = new TestSemanticsNode();
new_node2.id = 2;
new_node2.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
new_node2.addFlag(AccessibilityBridge.Flag.NAMES_ROUTE);
new_node2.label = "new_node2";
new_node1.children.add(new_node2);
testSemanticsUpdate = new_root.toUpdate();
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
eventCaptor = ArgumentCaptor.forClass(AccessibilityEvent.class);
verify(mockParent, times(4))
.requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
event = eventCaptor.getAllValues().get(2);
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
sentences = event.getText();
assertEquals(sentences.size(), 1);
assertEquals(sentences.get(0).toString(), "new_node2");
}
@Test
public void itAnnouncesRouteNameWhenRemoveARoute() {
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(mockRootView, mockManager, mockViewEmbedder);
ViewParent mockParent = mock(ViewParent.class);
when(mockRootView.getParent()).thenReturn(mockParent);
when(mockManager.isEnabled()).thenReturn(true);
TestSemanticsNode root = new TestSemanticsNode();
root.id = 0;
TestSemanticsNode node1 = new TestSemanticsNode();
node1.id = 1;
node1.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
node1.addFlag(AccessibilityBridge.Flag.NAMES_ROUTE);
node1.label = "node1";
root.children.add(node1);
TestSemanticsNode node2 = new TestSemanticsNode();
node2.id = 2;
node2.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
node2.addFlag(AccessibilityBridge.Flag.NAMES_ROUTE);
node2.label = "node2";
node1.children.add(node2);
TestSemanticsUpdate testSemanticsUpdate = root.toUpdate();
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
ArgumentCaptor<AccessibilityEvent> eventCaptor =
ArgumentCaptor.forClass(AccessibilityEvent.class);
verify(mockParent, times(2))
.requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
AccessibilityEvent event = eventCaptor.getAllValues().get(0);
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
List<CharSequence> sentences = event.getText();
assertEquals(sentences.size(), 1);
assertEquals(sentences.get(0).toString(), "node2");
TestSemanticsNode new_root = new TestSemanticsNode();
new_root.id = 0;
TestSemanticsNode new_node1 = new TestSemanticsNode();
new_node1.id = 1;
new_node1.label = "new_node1";
new_root.children.add(new_node1);
TestSemanticsNode new_node2 = new TestSemanticsNode();
new_node2.id = 2;
new_node2.addFlag(AccessibilityBridge.Flag.SCOPES_ROUTE);
new_node2.addFlag(AccessibilityBridge.Flag.NAMES_ROUTE);
new_node2.label = "new_node2";
new_node1.children.add(new_node2);
testSemanticsUpdate = new_root.toUpdate();
accessibilityBridge.updateSemantics(testSemanticsUpdate.buffer, testSemanticsUpdate.strings);
eventCaptor = ArgumentCaptor.forClass(AccessibilityEvent.class);
verify(mockParent, times(4))
.requestSendAccessibilityEvent(eq(mockRootView), eventCaptor.capture());
event = eventCaptor.getAllValues().get(2);
assertEquals(event.getEventType(), AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
sentences = event.getText();
assertEquals(sentences.size(), 1);
assertEquals(sentences.get(0).toString(), "new_node2");
}
@Test
public void itAnnouncesWhiteSpaceWhenNoNamesRoute() {
AccessibilityViewEmbedder mockViewEmbedder = mock(AccessibilityViewEmbedder.class);

View File

@ -167,17 +167,27 @@ void AccessibilityBridge::UpdateSemantics(flutter::SemanticsNodeUpdates nodes,
}
NSMutableArray<SemanticsObject*>* newRoutes = [[[NSMutableArray alloc] init] autorelease];
[root collectRoutes:newRoutes];
// Finds the last route that is not in the previous routes.
for (SemanticsObject* route in newRoutes) {
if (std::find(previous_routes_.begin(), previous_routes_.end(), [route uid]) !=
if (std::find(previous_routes_.begin(), previous_routes_.end(), [route uid]) ==
previous_routes_.end()) {
lastAdded = route;
}
}
// If all the routes are in the previous route, get the last route.
if (lastAdded == nil && [newRoutes count] > 0) {
int index = [newRoutes count] - 1;
lastAdded = [newRoutes objectAtIndex:index];
}
if (lastAdded != nil && [lastAdded uid] != previous_route_id_) {
// There are two cases if lastAdded != nil
// 1. lastAdded is not in previous routes. In this case,
// [lastAdded uid] != previous_route_id_
// 2. All new routes are in previous routes and
// lastAdded = newRoutes.last.
// In the first case, we need to announce new route. In the second case,
// we need to announce if one list is shorter than the other.
if (lastAdded != nil &&
([lastAdded uid] != previous_route_id_ || [newRoutes count] != previous_routes_.size())) {
previous_route_id_ = [lastAdded uid];
routeChanged = true;
}

View File

@ -333,6 +333,178 @@ fml::RefPtr<fml::TaskRunner> CreateNewThread(std::string name) {
UIAccessibilityScreenChangedNotification);
}
- (void)testAnnouncesRouteChangesWhenAddAdditionalRoute {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode node1;
node1.id = 1;
node1.label = "node1";
node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
nodes[node1.id] = node1;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
root_node.childrenInTraversalOrder = {1};
root_node.childrenInHitTestOrder = {1};
nodes[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 1ul);
XCTAssertEqualObjects(accessibility_notifications[0][@"argument"], @"node1");
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
UIAccessibilityScreenChangedNotification);
flutter::SemanticsNodeUpdates new_nodes;
flutter::SemanticsNode new_node1;
new_node1.id = 1;
new_node1.label = "new_node1";
new_node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
new_node1.childrenInTraversalOrder = {2};
new_node1.childrenInHitTestOrder = {2};
new_nodes[new_node1.id] = new_node1;
flutter::SemanticsNode new_node2;
new_node2.id = 2;
new_node2.label = "new_node2";
new_node2.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
new_nodes[new_node2.id] = new_node2;
flutter::SemanticsNode new_root_node;
new_root_node.id = kRootNodeId;
new_root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
new_root_node.childrenInTraversalOrder = {1};
new_root_node.childrenInHitTestOrder = {1};
new_nodes[new_root_node.id] = new_root_node;
bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 2ul);
XCTAssertEqualObjects(accessibility_notifications[1][@"argument"], @"new_node2");
XCTAssertEqual([accessibility_notifications[1][@"notification"] unsignedIntValue],
UIAccessibilityScreenChangedNotification);
}
- (void)testAnnouncesRouteChangesRemoveRouteInMiddle {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");
flutter::TaskRunners runners(/*label=*/self.name.UTF8String,
/*platform=*/thread_task_runner,
/*raster=*/thread_task_runner,
/*ui=*/thread_task_runner,
/*io=*/thread_task_runner);
auto platform_view = std::make_unique<flutter::PlatformViewIOS>(
/*delegate=*/mock_delegate,
/*rendering_api=*/flutter::IOSRenderingAPI::kSoftware,
/*task_runners=*/runners);
id mockFlutterView = OCMClassMock([FlutterView class]);
id mockFlutterViewController = OCMClassMock([FlutterViewController class]);
OCMStub([mockFlutterViewController view]).andReturn(mockFlutterView);
NSMutableArray<NSDictionary<NSString*, id>*>* accessibility_notifications =
[[[NSMutableArray alloc] init] autorelease];
auto ios_delegate = std::make_unique<flutter::MockIosDelegate>();
ios_delegate->on_PostAccessibilityNotification_ =
[accessibility_notifications](UIAccessibilityNotifications notification, id argument) {
[accessibility_notifications addObject:@{
@"notification" : @(notification),
@"argument" : argument ? argument : [NSNull null],
}];
};
__block auto bridge =
std::make_unique<flutter::AccessibilityBridge>(/*view_controller=*/mockFlutterViewController,
/*platform_view=*/platform_view.get(),
/*platform_views_controller=*/nil,
/*ios_delegate=*/std::move(ios_delegate));
flutter::CustomAccessibilityActionUpdates actions;
flutter::SemanticsNodeUpdates nodes;
flutter::SemanticsNode node1;
node1.id = 1;
node1.label = "node1";
node1.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
node1.childrenInTraversalOrder = {2};
node1.childrenInHitTestOrder = {2};
nodes[node1.id] = node1;
flutter::SemanticsNode node2;
node2.id = 2;
node2.label = "node2";
node2.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
nodes[node2.id] = node2;
flutter::SemanticsNode root_node;
root_node.id = kRootNodeId;
root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
root_node.childrenInTraversalOrder = {1};
root_node.childrenInHitTestOrder = {1};
nodes[root_node.id] = root_node;
bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 1ul);
XCTAssertEqualObjects(accessibility_notifications[0][@"argument"], @"node2");
XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue],
UIAccessibilityScreenChangedNotification);
flutter::SemanticsNodeUpdates new_nodes;
flutter::SemanticsNode new_node1;
new_node1.id = 1;
new_node1.label = "new_node1";
new_node1.childrenInTraversalOrder = {2};
new_node1.childrenInHitTestOrder = {2};
new_nodes[new_node1.id] = new_node1;
flutter::SemanticsNode new_node2;
new_node2.id = 2;
new_node2.label = "new_node2";
new_node2.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute) |
static_cast<int32_t>(flutter::SemanticsFlags::kNamesRoute);
new_nodes[new_node2.id] = new_node2;
flutter::SemanticsNode new_root_node;
new_root_node.id = kRootNodeId;
new_root_node.flags = static_cast<int32_t>(flutter::SemanticsFlags::kScopesRoute);
new_root_node.childrenInTraversalOrder = {1};
new_root_node.childrenInHitTestOrder = {1};
new_nodes[new_root_node.id] = new_root_node;
bridge->UpdateSemantics(/*nodes=*/new_nodes, /*actions=*/actions);
XCTAssertEqual([accessibility_notifications count], 2ul);
XCTAssertEqualObjects(accessibility_notifications[1][@"argument"], @"new_node2");
XCTAssertEqual([accessibility_notifications[1][@"notification"] unsignedIntValue],
UIAccessibilityScreenChangedNotification);
}
- (void)testAnnouncesRouteChangesWhenNoNamesRoute {
flutter::MockDelegate mock_delegate;
auto thread_task_runner = CreateNewThread("AccessibilityBridgeTest");