Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

[macOS]Support SemanticsService.announce #40585

Merged
merged 16 commits into from
Mar 30, 2023
42 changes: 42 additions & 0 deletions shell/platform/darwin/macos/framework/Source/FlutterEngine.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -409,6 +417,7 @@ - (instancetype)initWithName:(NSString*)labelPrefix

_platformViewController = [[FlutterPlatformViewController alloc] init];
[self setUpPlatformViewChannel];
[self setUpAccessibilityChannel];
[self setUpNotificationCenterListeners];

return self;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -967,7 +986,30 @@ - (void)onAccessibilityStatusChanged:(NSNotification*)notification {

self.semanticsEnabled = enabled;
}
- (void)handleAccessibilityEvent:(NSDictionary<NSString*, id>*)annotatedEvent {
NSString* type = annotatedEvent[@"type"];
if ([type isEqualToString:@"announce"]) {
NSString* message = annotatedEvent[@"data"][@"message"];
NSNumber* assertiveness = annotatedEvent[@"data"][@"assertiveness"];
Copy link
Member

@loic-sharma loic-sharma Mar 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this crash if data, message, or assertiveness properties are missing? A malformed message should be ignored, though an error can be logged.

(Asking as there was a recent crash on Windows embedder due to the framework sending unexpected clipboard messages)

Copy link
Member

@cbracken cbracken Mar 29, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These will result in nil, so the current code handles the cases where any/all of these are missing.

To add a bit of detail:

  • We guard against a nil message below since announcing nil makes no sense.
  • In the case of nil assertiveness we set the default priority; since messages to nil evaluate to nil, the ternary evaluates to NSAccessibilityPriorityMedium.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah OK perfect! :)

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];
Expand Down
23 changes: 23 additions & 0 deletions shell/platform/darwin/macos/framework/Source/FlutterEngineTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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<NSString*, id>* annotatedEvent =
@{@"type" : @"announce",
@"data" : @{@"message" : @"error message"}};

[engineMock handleAccessibilityEvent:annotatedEvent];

EXPECT_TRUE(announced);
}

} // namespace flutter::testing

// NOLINTEND(clang-analyzer-core.StackAddressEscape)
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,17 @@ typedef NS_ENUM(NSInteger, FlutterAppExitResponse) {
toTarget:(uint16_t)target
withData:(fml::MallocMapping)data;

/**
* Handles accessibility events.
*/
- (void)handleAccessibilityEvent:(NSDictionary<NSString*, id>*)annotatedEvent;

/**
* Announces accessibility messages.
*/
- (void)announceAccessibilityMessage:(NSString*)message
withPriority:(NSAccessibilityPriorityLevel)priority;

@end

NS_ASSUME_NONNULL_END