diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 75865d537786f..e69eaee8688b4 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1036,6 +1036,7 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterView.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterViewControllerTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterViewTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/IOKit.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index dafb1dce0ed92..4402d1d13641e 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -186,6 +186,7 @@ source_set("ios_test_flutter_mrc") { "framework/Source/FlutterEngineTest_mrc.mm", "framework/Source/FlutterPlatformPluginTest.mm", "framework/Source/FlutterPlatformViewsTest.mm", + "framework/Source/FlutterViewTest.mm", "framework/Source/accessibility_bridge_test.mm", ] deps = [ diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index 4585c392b3eac..23e56fdc41dc7 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -761,7 +761,7 @@ - (void)showAutocorrectionPromptRectForStart:(NSUInteger)start arguments:@[ @(client), @(start), @(end) ]]; } -#pragma mark - Screenshot Delegate +#pragma mark - FlutterViewEngineDelegate - (flutter::Rasterizer::Screenshot)takeScreenshot:(flutter::Rasterizer::ScreenshotType)type asBase64Encoded:(BOOL)base64Encode { @@ -769,6 +769,12 @@ - (void)showAutocorrectionPromptRectForStart:(NSUInteger)start return _shell->Screenshot(type, base64Encode); } +- (void)flutterViewAccessibilityDidCall { + if (self.viewController.view.accessibilityElements == nil) { + [self ensureSemanticsEnabled]; + } +} + - (NSObject*)binaryMessenger { return _binaryMessenger; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngineTest_mrc.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngineTest_mrc.mm index 46a7e4abac965..a13b669b13c2f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngineTest_mrc.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngineTest_mrc.mm @@ -12,6 +12,18 @@ FLUTTER_ASSERT_NOT_ARC +@interface FlutterEngineSpy : FlutterEngine +@property(nonatomic) BOOL ensureSemanticsEnabledCalled; +@end + +@implementation FlutterEngineSpy + +- (void)ensureSemanticsEnabled { + _ensureSemanticsEnabledCalled = YES; +} + +@end + @interface FlutterEngineTest_mrc : XCTestCase @end @@ -41,4 +53,11 @@ - (void)testSpawnsShareGpuContext { [spawn release]; } +- (void)testEnableSemanticsWhenFlutterViewAccessibilityDidCall { + FlutterEngineSpy* engine = [[FlutterEngineSpy alloc] initWithName:@"foobar"]; + engine.ensureSemanticsEnabledCalled = NO; + [engine flutterViewAccessibilityDidCall]; + XCTAssertTrue(engine.ensureSemanticsEnabledCalled); +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterView.h b/shell/platform/darwin/ios/framework/Source/FlutterView.h index 0cdd56d2cfbdc..a795953c823f4 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterView.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterView.h @@ -21,6 +21,16 @@ asBase64Encoded:(BOOL)base64Encode; - (std::shared_ptr&)platformViewsController; + +/** + * A callback that is called when iOS queries accessibility information of the Flutter view. + * + * This is useful to predict the current iOS accessibility status. For example, there is + * no API to listen whether voice control is turned on or off. The Flutter engine uses + * this callback to enable semantics in order to catch the case that voice control is + * on. + */ +- (void)flutterViewAccessibilityDidCall; @end @interface FlutterView : UIView diff --git a/shell/platform/darwin/ios/framework/Source/FlutterView.mm b/shell/platform/darwin/ios/framework/Source/FlutterView.mm index 73a9b8b529088..36b1bbb346e3e 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterView.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterView.mm @@ -130,4 +130,17 @@ - (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)context { CGContextRestoreGState(context); } +- (BOOL)isAccessibilityElement { + // iOS does not provide an API to query whether the voice control + // is turned on or off. It is likely at least one of the assitive + // technologies is turned on if this method is called. If we do + // not catch it in notification center, we will catch it here. + // + // TODO(chunhtai): Remove this workaround once iOS provides an + // API to query whether voice control is enabled. + // https://github.com/flutter/flutter/issues/76808. + [_delegate flutterViewAccessibilityDidCall]; + return NO; +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewTest.mm new file mode 100644 index 0000000000000..c14bee16a6104 --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewTest.mm @@ -0,0 +1,52 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import + +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterView.h" + +@interface FakeDelegate : NSObject +@property(nonatomic) BOOL callbackCalled; +@end + +@implementation FakeDelegate { + std::shared_ptr _platformViewsController; +} + +- (instancetype)init { + _callbackCalled = NO; + _platformViewsController = std::shared_ptr(nullptr); + return self; +} + +- (flutter::Rasterizer::Screenshot)takeScreenshot:(flutter::Rasterizer::ScreenshotType)type + asBase64Encoded:(BOOL)base64Encode { + return {}; +} + +- (std::shared_ptr&)platformViewsController { + return _platformViewsController; +} + +- (void)flutterViewAccessibilityDidCall { + _callbackCalled = YES; +} + +@end + +@interface FlutterViewTest : XCTestCase +@end + +@implementation FlutterViewTest + +- (void)testFlutterViewEnableSemanticsWhenIsAccessibilityElementIsCalled { + FakeDelegate* delegate = [[FakeDelegate alloc] init]; + FlutterView* view = [[FlutterView alloc] initWithDelegate:delegate opaque:NO]; + delegate.callbackCalled = NO; + XCTAssertFalse(view.isAccessibilityElement); + XCTAssertTrue(delegate.callbackCalled); +} + +@end