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
2 changes: 2 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,8 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Headers/FlutterPlugin
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Info.plist
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegateTest.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Test.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelay.h
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelay.mm
FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelayTest.mm
Expand Down
1 change: 1 addition & 0 deletions shell/platform/darwin/ios/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ shared_library("ios_test_flutter") {
"//build/config:symbol_visibility_hidden",
]
sources = [
"framework/Source/FlutterAppDelegateTest.mm",
"framework/Source/FlutterBinaryMessengerRelayTest.mm",
"framework/Source/FlutterDartProjectTest.mm",
"framework/Source/FlutterEngineTest.mm",
Expand Down
63 changes: 46 additions & 17 deletions shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@

#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterAppDelegate.h"

#include "flutter/fml/logging.h"
#import "flutter/fml/logging.h"
Copy link
Contributor

Choose a reason for hiding this comment

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

How do we decide whether we use import vs include? I originally thought we always include c++ file and import objective c file

Copy link
Member Author

Choose a reason for hiding this comment

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

If the file is objective-c or objective-c++ just always use import.

#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterPluginAppLifeCycleDelegate.h"
#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Test.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegate_internal.h"

static NSString* kUIBackgroundMode = @"UIBackgroundModes";
static NSString* kRemoteNotificationCapabitiliy = @"remote-notification";
static NSString* kBackgroundFetchCapatibility = @"fetch";

@interface FlutterAppDelegate ()
@property(nonatomic, copy) FlutterViewController* (^rootFlutterViewControllerGetter)(void);
@end

@implementation FlutterAppDelegate {
FlutterPluginAppLifeCycleDelegate* _lifeCycleDelegate;
}
Expand All @@ -26,6 +32,7 @@ - (instancetype)init {

- (void)dealloc {
[_lifeCycleDelegate release];
[_rootFlutterViewControllerGetter release];
Copy link
Contributor

Choose a reason for hiding this comment

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

This class does not create the getter, it seems weird that this class is responsible for destroying it.

Copy link
Member Author

Choose a reason for hiding this comment

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

It actually does create it in its declaration of the property. This is related to your other comment, too. The file FlutterAppDelegate_Test is a thing called a category which allows you to extend classes or make methods selectively visible. You can think of this as declaring that property as @VisibleForTesting.

[super dealloc];
}

Expand All @@ -41,10 +48,13 @@ - (BOOL)application:(UIApplication*)application

// Returns the key window's rootViewController, if it's a FlutterViewController.
// Otherwise, returns nil.
+ (FlutterViewController*)rootFlutterViewController {
UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;
if ([viewController isKindOfClass:[FlutterViewController class]]) {
return (FlutterViewController*)viewController;
- (FlutterViewController*)rootFlutterViewController {
if (_rootFlutterViewControllerGetter != nil) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this only used in test? It seems weird that we inject the getter this way, is it possible for the test to override this class and override this method to inject the getter there?

Copy link
Member Author

Choose a reason for hiding this comment

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

I could, but it's the class I'm testing. In order to do that I'd have to make a partial mock of the class I actually want to test which is weird, too.

I could do more traditional dependency injection and pass in an object that can get the root FlutterViewController in with the FlutterAppDelegate's initializer. This is basically the same thing but with a setter. I wanted to introduce the least amount of overhead possible, that's why it's a pointer check that will basically be ignored with branch prediction. If I always passed the logic through a block or a protocol we would always incur a dynamic dispatch.

return _rootFlutterViewControllerGetter();
}
UIViewController* rootViewController = _window.rootViewController;
if ([rootViewController isKindOfClass:[FlutterViewController class]]) {
return (FlutterViewController*)rootViewController;
}
return nil;
}
Expand Down Expand Up @@ -124,7 +134,28 @@ - (void)userNotificationCenter:(UNUserNotificationCenter*)center
- (BOOL)application:(UIApplication*)application
openURL:(NSURL*)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {
return [_lifeCycleDelegate application:application openURL:url options:options];
if ([_lifeCycleDelegate application:application openURL:url options:options]) {
return YES;
} else {
FlutterViewController* flutterViewController = [self rootFlutterViewController];
if (flutterViewController) {
[flutterViewController.engine
waitForFirstFrame:3.0
callback:^(BOOL didTimeout) {
if (didTimeout) {
FML_LOG(ERROR)
<< "Timeout waiting for the first frame when launching an URL.";
} else {
[flutterViewController.engine.navigationChannel invokeMethod:@"pushRoute"
arguments:url.path];
}
}];
return YES;
} else {
FML_LOG(ERROR) << "Attempting to open an URL without a Flutter RootViewController.";
Copy link
Contributor

Choose a reason for hiding this comment

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

In what situation this can happen?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's unlikely but if you edit the storyboard for the iOS project this could happen. Also, if you were doing add to app and you call into a FlutterAppDelegate it could be a problem.

return NO;
}
}
}

- (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url {
Expand Down Expand Up @@ -175,27 +206,25 @@ - (BOOL)application:(UIApplication*)application
#pragma mark - FlutterPluginRegistry methods. All delegating to the rootViewController

- (NSObject<FlutterPluginRegistrar>*)registrarForPlugin:(NSString*)pluginKey {
UIViewController* rootViewController = _window.rootViewController;
if ([rootViewController isKindOfClass:[FlutterViewController class]]) {
return
[[(FlutterViewController*)rootViewController pluginRegistry] registrarForPlugin:pluginKey];
FlutterViewController* flutterRootViewController = [self rootFlutterViewController];
if (flutterRootViewController) {
return [[flutterRootViewController pluginRegistry] registrarForPlugin:pluginKey];
}
return nil;
}

- (BOOL)hasPlugin:(NSString*)pluginKey {
UIViewController* rootViewController = _window.rootViewController;
if ([rootViewController isKindOfClass:[FlutterViewController class]]) {
return [[(FlutterViewController*)rootViewController pluginRegistry] hasPlugin:pluginKey];
FlutterViewController* flutterRootViewController = [self rootFlutterViewController];
if (flutterRootViewController) {
return [[flutterRootViewController pluginRegistry] hasPlugin:pluginKey];
}
return false;
}

- (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {
UIViewController* rootViewController = _window.rootViewController;
if ([rootViewController isKindOfClass:[FlutterViewController class]]) {
return [[(FlutterViewController*)rootViewController pluginRegistry]
valuePublishedByPlugin:pluginKey];
FlutterViewController* flutterRootViewController = [self rootFlutterViewController];
if (flutterRootViewController) {
return [[flutterRootViewController pluginRegistry] valuePublishedByPlugin:pluginKey];
}
return nil;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// 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 <OCMock/OCMock.h>
#import <XCTest/XCTest.h>

#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterAppDelegate.h"
#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h"
#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Test.h"
#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Test.h"

FLUTTER_ASSERT_ARC

@interface FlutterAppDelegateTest : XCTestCase
@end

@implementation FlutterAppDelegateTest

- (void)testLaunchUrl {
FlutterAppDelegate* appDelegate = [[FlutterAppDelegate alloc] init];
FlutterViewController* viewController = OCMClassMock([FlutterViewController class]);
FlutterEngine* engine = OCMClassMock([FlutterEngine class]);
FlutterMethodChannel* navigationChannel = OCMClassMock([FlutterMethodChannel class]);
OCMStub([engine navigationChannel]).andReturn(navigationChannel);
OCMStub([viewController engine]).andReturn(engine);
OCMStub([engine waitForFirstFrame:3.0 callback:([OCMArg invokeBlockWithArgs:@(NO), nil])]);
appDelegate.rootFlutterViewControllerGetter = ^{
return viewController;
};
NSURL* url = [NSURL URLWithString:@"http://example.com"];
NSDictionary<UIApplicationOpenURLOptionsKey, id>* options = @{};
BOOL result = [appDelegate application:[UIApplication sharedApplication]
openURL:url
options:options];
XCTAssertTrue(result);
OCMVerify([navigationChannel invokeMethod:@"pushRoute" arguments:url.path]);
}

@end
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 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.

@class FlutterViewController;

@interface FlutterAppDelegate (Test)
@property(nonatomic, copy) FlutterViewController* (^rootFlutterViewControllerGetter)(void);
@end
13 changes: 13 additions & 0 deletions shell/platform/darwin/ios/framework/Source/FlutterEngine.mm
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,19 @@ - (void)onLocaleUpdated:(NSNotification*)notification {
[self.localizationChannel invokeMethod:@"setLocale" arguments:localeData];
}

- (void)waitForFirstFrame:(NSTimeInterval)timeout
callback:(void (^_Nonnull)(BOOL didTimeout))callback {
dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0);
dispatch_async(queue, ^{
fml::TimeDelta waitTime = fml::TimeDelta::FromMilliseconds(timeout * 1000);
BOOL didTimeout =
self.shell.WaitForFirstFrame(waitTime).code() == fml::StatusCode::kDeadlineExceeded;
dispatch_async(dispatch_get_main_queue(), ^{
callback(didTimeout);
});
});
}

@end

@implementation FlutterEngineRegistrar {
Expand Down
13 changes: 13 additions & 0 deletions shell/platform/darwin/ios/framework/Source/FlutterEngineTest.mm
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,17 @@ - (void)testRunningInitialRouteSendsNavigationMessage {
message:encodedSetInitialRouteMethod]);
}

- (void)testWaitForFirstFrameTimeout {
FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar"];
[engine run];
XCTestExpectation* timeoutFirstFrame = [self expectationWithDescription:@"timeoutFirstFrame"];
[engine waitForFirstFrame:0.1
callback:^(BOOL didTimeout) {
if (timeoutFirstFrame) {
[timeoutFirstFrame fulfill];
}
}];
[self waitForExpectationsWithTimeout:1 handler:nil];
}

@end
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
- (void)notifyLowMemory;
- (flutter::PlatformViewIOS*)iosPlatformView;

- (void)waitForFirstFrame:(NSTimeInterval)timeout callback:(void (^)(BOOL didTimeout))callback;
@end

#endif // FLUTTER_SHELL_PLATFORM_DARWIN_IOS_FRAMEWORK_SOURCE_FLUTTERENGINE_INTERNAL_H_
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

#import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h"

@class FlutterBinaryMessengerRelay;

// Category to add test-only visibility.
@interface FlutterEngine (Test) <FlutterBinaryMessenger>
- (void)setBinaryMessenger:(FlutterBinaryMessengerRelay*)binaryMessenger;
- (void)waitForFirstFrame:(NSTimeInterval)timeout callback:(void (^)(BOOL didTimeout))callback;
@end