diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm index 64af3772290ad..864049ff90452 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine.mm @@ -150,6 +150,11 @@ - (void)loadAOTData:(NSString*)assetsDir; */ - (void)setUpPlatformViewChannel; +/** + * Creates an accessibility channel and sets up the message handler. + */ +- (void)setUpAccessibilityChannel; + /** * Handles messages received from the Flutter engine on the _*Channel channels. */ @@ -366,6 +371,9 @@ @implementation FlutterEngine { // A message channel for sending user settings to the flutter engine. FlutterBasicMessageChannel* _settingsChannel; + // A message channel for accessibility. + FlutterBasicMessageChannel* _accessibilityChannel; + // A method channel for miscellaneous platform functionality. FlutterMethodChannel* _platformChannel; @@ -409,6 +417,7 @@ - (instancetype)initWithName:(NSString*)labelPrefix _platformViewController = [[FlutterPlatformViewController alloc] init]; [self setUpPlatformViewChannel]; + [self setUpAccessibilityChannel]; [self setUpNotificationCenterListeners]; return self; @@ -923,6 +932,16 @@ - (void)setUpPlatformViewChannel { }]; } +- (void)setUpAccessibilityChannel { + _accessibilityChannel = [FlutterBasicMessageChannel + messageChannelWithName:@"flutter/accessibility" + binaryMessenger:self.binaryMessenger + codec:[FlutterStandardMessageCodec sharedInstance]]; + __weak FlutterEngine* weakSelf = self; + [_accessibilityChannel setMessageHandler:^(id message, FlutterReply reply) { + [weakSelf handleAccessibilityEvent:message]; + }]; +} - (void)setUpNotificationCenterListeners { NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; // macOS fires this private message when VoiceOver turns on or off. @@ -967,7 +986,30 @@ - (void)onAccessibilityStatusChanged:(NSNotification*)notification { self.semanticsEnabled = enabled; } +- (void)handleAccessibilityEvent:(NSDictionary*)annotatedEvent { + NSString* type = annotatedEvent[@"type"]; + if ([type isEqualToString:@"announce"]) { + NSString* message = annotatedEvent[@"data"][@"message"]; + NSNumber* assertiveness = annotatedEvent[@"data"][@"assertiveness"]; + if (message == nil) { + return; + } + NSAccessibilityPriorityLevel priority = [assertiveness isEqualToNumber:@1] + ? NSAccessibilityPriorityHigh + : NSAccessibilityPriorityMedium; + + [self announceAccessibilityMessage:message withPriority:priority]; + } +} + +- (void)announceAccessibilityMessage:(NSString*)message + withPriority:(NSAccessibilityPriorityLevel)priority { + NSAccessibilityPostNotificationWithUserInfo( + [self viewControllerForId:kFlutterDefaultViewId].flutterView, + NSAccessibilityAnnouncementRequestedNotification, + @{NSAccessibilityAnnouncementKey : message, NSAccessibilityPriorityKey : @(priority)}); +} - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if ([call.method isEqualToString:@"SystemNavigator.pop"]) { [NSApp terminate:self]; diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm index c69679955f4f8..36c7d745cdf41 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm @@ -766,6 +766,29 @@ - (nonnull NSView*)createWithViewIdentifier:(int64_t)viewId arguments:(nullable EXPECT_TRUE(triedToTerminate); } +TEST_F(FlutterEngineTest, HandleAccessibilityEvent) { + __block BOOL announced = FALSE; + id engineMock = CreateMockFlutterEngine(nil); + + OCMStub([engineMock announceAccessibilityMessage:[OCMArg any] + withPriority:NSAccessibilityPriorityMedium]) + .andDo((^(NSInvocation* invocation) { + announced = TRUE; + [invocation retainArguments]; + NSString* message; + [invocation getArgument:&message atIndex:2]; + EXPECT_EQ(message, @"error message"); + })); + + NSDictionary* annotatedEvent = + @{@"type" : @"announce", + @"data" : @{@"message" : @"error message"}}; + + [engineMock handleAccessibilityEvent:annotatedEvent]; + + EXPECT_TRUE(announced); +} + } // namespace flutter::testing // NOLINTEND(clang-analyzer-core.StackAddressEscape) diff --git a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h index bbc4527543c9a..9b705a030690b 100644 --- a/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h +++ b/shell/platform/darwin/macos/framework/Source/FlutterEngine_Internal.h @@ -179,6 +179,17 @@ typedef NS_ENUM(NSInteger, FlutterAppExitResponse) { toTarget:(uint16_t)target withData:(fml::MallocMapping)data; +/** + * Handles accessibility events. + */ +- (void)handleAccessibilityEvent:(NSDictionary*)annotatedEvent; + +/** + * Announces accessibility messages. + */ +- (void)announceAccessibilityMessage:(NSString*)message + withPriority:(NSAccessibilityPriorityLevel)priority; + @end NS_ASSUME_NONNULL_END