Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
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
15 changes: 14 additions & 1 deletion shell/platform/android/io/flutter/view/AccessibilityBridge.java
Original file line number Diff line number Diff line change
Expand Up @@ -1356,16 +1356,29 @@ void updateSemantics(@NonNull ByteBuffer buffer, @NonNull String[] strings) {

// 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,134 @@ public void itUnfocusesPlatformViewWhenPlatformViewGoesAway() {
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);
Expand Down
14 changes: 12 additions & 2 deletions shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm
Original file line number Diff line number Diff line change
Expand Up @@ -167,17 +167,27 @@ void PostAccessibilityNotification(UIAccessibilityNotifications notification,
}
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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,178 @@ - (void)testAnnouncesRouteChanges {
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");
Expand Down