diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index ea8afcf06e49d..568a222455681 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -39,6 +39,7 @@ @interface FlutterViewController () @property(nonatomic, readwrite, getter=isDisplayingFlutterUI) BOOL displayingFlutterUI; @property(nonatomic, assign) BOOL isHomeIndicatorHidden; +@property(nonatomic, assign) BOOL isPresentingViewControllerAnimating; @end // The following conditional compilation defines an API 13 concept on earlier API targets so that @@ -1240,4 +1241,22 @@ - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey { return [_engine.get() valuePublishedByPlugin:pluginKey]; } +- (void)presentViewController:(UIViewController*)viewControllerToPresent + animated:(BOOL)flag + completion:(void (^)(void))completion { + self.isPresentingViewControllerAnimating = YES; + [super presentViewController:viewControllerToPresent + animated:flag + completion:^{ + self.isPresentingViewControllerAnimating = NO; + if (completion) { + completion(); + } + }]; +} + +- (BOOL)isPresentingViewController { + return self.presentedViewController != nil || self.isPresentingViewControllerAnimating; +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h index 98f222b3254ac..b1ffc01aeb4fd 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h @@ -23,6 +23,7 @@ extern NSNotificationName const FlutterViewControllerShowHomeIndicator; @interface FlutterViewController () +@property(nonatomic, readonly) BOOL isPresentingViewController; - (fml::WeakPtr)getWeakPtr; - (flutter::FlutterPlatformViewsController*)platformViewsController; diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h index cbb08421e17ac..77e5813792ad1 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h @@ -36,9 +36,21 @@ class PlatformViewIOS; */ class AccessibilityBridge final : public AccessibilityBridgeIos { public: + /** Delegate for handling iOS operations. */ + class IosDelegate { + public: + virtual ~IosDelegate() = default; + /// Returns true when the FlutterViewController associated with the `view` + /// is presenting a modal view controller. + virtual bool IsFlutterViewControllerPresentingModalViewController(UIView* view) = 0; + virtual void PostAccessibilityNotification(UIAccessibilityNotifications notification, + id argument) = 0; + }; + AccessibilityBridge(UIView* view, PlatformViewIOS* platform_view, - FlutterPlatformViewsController* platform_views_controller); + FlutterPlatformViewsController* platform_views_controller, + std::unique_ptr ios_delegate = nullptr); ~AccessibilityBridge(); void UpdateSemantics(flutter::SemanticsNodeUpdates nodes, @@ -75,6 +87,7 @@ class AccessibilityBridge final : public AccessibilityBridgeIos { int32_t previous_route_id_; std::unordered_map actions_; std::vector previous_routes_; + std::unique_ptr ios_delegate_; FML_DISALLOW_COPY_AND_ASSIGN(AccessibilityBridge); }; diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm index 3e1da1bdd1de1..9a10dd53655d5 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge.mm @@ -4,6 +4,7 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/accessibility_bridge.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/accessibility_text_entry.h" #import "flutter/shell/platform/darwin/ios/platform_view_ios.h" @@ -13,17 +14,54 @@ FLUTTER_ASSERT_NOT_ARC namespace flutter { +namespace { + +FlutterViewController* _Nullable GetFlutterViewControllerForView(UIView* view) { + // There is no way to get a view's view controller in UIKit directly, this is + // somewhat of a hacky solution to get that. This could be eliminated if the + // bridge actually kept a reference to a FlutterViewController instead of a + // UIView. + id nextResponder = [view nextResponder]; + if ([nextResponder isKindOfClass:[FlutterViewController class]]) { + return nextResponder; + } else if ([nextResponder isKindOfClass:[UIView class]]) { + return GetFlutterViewControllerForView(nextResponder); + } else { + return nil; + } +} + +class DefaultIosDelegate : public AccessibilityBridge::IosDelegate { + public: + bool IsFlutterViewControllerPresentingModalViewController(UIView* view) override { + FlutterViewController* viewController = GetFlutterViewControllerForView(view); + if (viewController) { + return viewController.isPresentingViewController; + } else { + return false; + } + } + + void PostAccessibilityNotification(UIAccessibilityNotifications notification, + id argument) override { + UIAccessibilityPostNotification(notification, argument); + } +}; +} // namespace AccessibilityBridge::AccessibilityBridge(UIView* view, PlatformViewIOS* platform_view, - FlutterPlatformViewsController* platform_views_controller) + FlutterPlatformViewsController* platform_views_controller, + std::unique_ptr ios_delegate) : view_(view), platform_view_(platform_view), platform_views_controller_(platform_views_controller), objects_([[NSMutableDictionary alloc] init]), weak_factory_(this), previous_route_id_(0), - previous_routes_({}) { + previous_routes_({}), + ios_delegate_(ios_delegate ? std::move(ios_delegate) + : std::make_unique()) { accessibility_channel_.reset([[FlutterBasicMessageChannel alloc] initWithName:@"flutter/accessibility" binaryMessenger:platform_view->GetOwnerViewController().get().engine.binaryMessenger @@ -137,15 +175,18 @@ layoutChanged = layoutChanged || [doomed_uids count] > 0; if (routeChanged) { - NSString* routeName = [lastAdded routeName]; - UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, routeName); + if (!ios_delegate_->IsFlutterViewControllerPresentingModalViewController(view_)) { + NSString* routeName = [lastAdded routeName]; + ios_delegate_->PostAccessibilityNotification(UIAccessibilityScreenChangedNotification, + routeName); + } } else if (layoutChanged) { // TODO(goderbauer): figure out which node to focus next. - UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, nil); + ios_delegate_->PostAccessibilityNotification(UIAccessibilityLayoutChangedNotification, nil); } if (scrollOccured) { // TODO(tvolkert): provide meaningful string (e.g. "page 2 of 5") - UIAccessibilityPostNotification(UIAccessibilityPageScrolledNotification, @""); + ios_delegate_->PostAccessibilityNotification(UIAccessibilityPageScrolledNotification, @""); } } @@ -233,7 +274,7 @@ static bool DidFlagChange(const flutter::SemanticsNode& oldNode, NSString* type = annotatedEvent[@"type"]; if ([type isEqualToString:@"announce"]) { NSString* message = annotatedEvent[@"data"][@"message"]; - UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, message); + ios_delegate_->PostAccessibilityNotification(UIAccessibilityAnnouncementNotification, message); } } diff --git a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm index c242c437c930f..f293be0895786 100644 --- a/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm +++ b/shell/platform/darwin/ios/framework/Source/accessibility_bridge_test.mm @@ -86,6 +86,22 @@ void OnPlatformViewRegisterTexture(std::shared_ptr texture) override {} void OnPlatformViewUnregisterTexture(int64_t texture_id) override {} void OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) override {} }; + +class MockIosDelegate : public AccessibilityBridge::IosDelegate { + public: + bool IsFlutterViewControllerPresentingModalViewController(UIView* view) override { + return result_IsFlutterViewControllerPresentingModalViewController_; + }; + + void PostAccessibilityNotification(UIAccessibilityNotifications notification, + id argument) override { + if (on_PostAccessibilityNotification_) { + on_PostAccessibilityNotification_(notification, argument); + } + } + std::function on_PostAccessibilityNotification_; + bool result_IsFlutterViewControllerPresentingModalViewController_ = false; +}; } // namespace } // namespace flutter @@ -238,4 +254,112 @@ - (void)testSemanticsDeallocated { XCTAssertNil(gMockPlatformView); } +- (void)testAnnouncesRouteChanges { + 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( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + id mockFlutterView = OCMClassMock([FlutterView class]); + std::string label = "some label"; + + NSMutableArray*>* accessibility_notifications = + [[[NSMutableArray alloc] init] autorelease]; + auto ios_delegate = std::make_unique(); + 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(/*view=*/mockFlutterView, + /*platform_view=*/platform_view.get(), + /*platform_views_controller=*/nil, + /*ios_delegate=*/std::move(ios_delegate)); + + flutter::CustomAccessibilityActionUpdates actions; + flutter::SemanticsNodeUpdates nodes; + + flutter::SemanticsNode route_node; + route_node.id = 1; + route_node.label = label; + route_node.flags = static_cast(flutter::SemanticsFlags::kScopesRoute) | + static_cast(flutter::SemanticsFlags::kNamesRoute); + route_node.label = "route"; + nodes[route_node.id] = route_node; + flutter::SemanticsNode root_node; + root_node.id = kRootNodeId; + root_node.label = label; + 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"], @"route"); + XCTAssertEqual([accessibility_notifications[0][@"notification"] unsignedIntValue], + UIAccessibilityScreenChangedNotification); +} + +- (void)testAnnouncesIgnoresRouteChangesWhenModal { + 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( + /*delegate=*/mock_delegate, + /*rendering_api=*/flutter::IOSRenderingAPI::kSoftware, + /*task_runners=*/runners); + id mockFlutterView = OCMClassMock([FlutterView class]); + std::string label = "some label"; + + NSMutableArray*>* accessibility_notifications = + [[[NSMutableArray alloc] init] autorelease]; + auto ios_delegate = std::make_unique(); + ios_delegate->on_PostAccessibilityNotification_ = + [accessibility_notifications](UIAccessibilityNotifications notification, id argument) { + [accessibility_notifications addObject:@{ + @"notification" : @(notification), + @"argument" : argument ? argument : [NSNull null], + }]; + }; + ios_delegate->result_IsFlutterViewControllerPresentingModalViewController_ = true; + __block auto bridge = + std::make_unique(/*view=*/mockFlutterView, + /*platform_view=*/platform_view.get(), + /*platform_views_controller=*/nil, + /*ios_delegate=*/std::move(ios_delegate)); + + flutter::CustomAccessibilityActionUpdates actions; + flutter::SemanticsNodeUpdates nodes; + + flutter::SemanticsNode route_node; + route_node.id = 1; + route_node.label = label; + route_node.flags = static_cast(flutter::SemanticsFlags::kScopesRoute) | + static_cast(flutter::SemanticsFlags::kNamesRoute); + route_node.label = "route"; + nodes[route_node.id] = route_node; + flutter::SemanticsNode root_node; + root_node.id = kRootNodeId; + root_node.label = label; + root_node.childrenInTraversalOrder = {1}; + root_node.childrenInHitTestOrder = {1}; + nodes[root_node.id] = root_node; + bridge->UpdateSemantics(/*nodes=*/nodes, /*actions=*/actions); + + XCTAssertEqual([accessibility_notifications count], 0ul); +} + @end