diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 31c79959f78d8..c8e0baf0286ff 100755 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -993,6 +993,9 @@ FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatfor FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegate.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegateTest.m FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegate_internal.h +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.h +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.mm +FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPluginTest.mm FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h FILE: ../../../flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.mm diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index e516886474113..dafb1dce0ed92 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -66,6 +66,8 @@ source_set("flutter_framework_source") { "framework/Source/FlutterPlatformViews_Internal.h", "framework/Source/FlutterPlatformViews_Internal.mm", "framework/Source/FlutterPluginAppLifeCycleDelegate.mm", + "framework/Source/FlutterRestorationPlugin.h", + "framework/Source/FlutterRestorationPlugin.mm", "framework/Source/FlutterTextInputDelegate.h", "framework/Source/FlutterTextInputPlugin.h", "framework/Source/FlutterTextInputPlugin.mm", @@ -221,6 +223,7 @@ shared_library("ios_test_flutter") { "framework/Source/FlutterEngineGroupTest.mm", "framework/Source/FlutterEngineTest.mm", "framework/Source/FlutterPluginAppLifeCycleDelegateTest.m", + "framework/Source/FlutterRestorationPluginTest.mm", "framework/Source/FlutterTextInputPluginTest.m", "framework/Source/FlutterViewControllerTest.mm", "framework/Source/SemanticsObjectTest.mm", diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h b/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h index 3067410f96a97..3139e741e095a 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterEngine.h @@ -135,7 +135,31 @@ FLUTTER_EXPORT */ - (instancetype)initWithName:(NSString*)labelPrefix project:(nullable FlutterDartProject*)project - allowHeadlessExecution:(BOOL)allowHeadlessExecution NS_DESIGNATED_INITIALIZER; + allowHeadlessExecution:(BOOL)allowHeadlessExecution; + +/** + * Initialize this FlutterEngine with a `FlutterDartProject`. + * + * If the FlutterDartProject is not specified, the FlutterEngine will attempt to locate + * the project in a default location (the flutter_assets folder in the iOS application + * bundle). + * + * A newly initialized engine will not run the `FlutterDartProject` until either + * `-runWithEntrypoint:` or `-runWithEntrypoint:libraryURI:` is called. + * + * @param labelPrefix The label prefix used to identify threads for this instance. Should + * be unique across FlutterEngine instances, and is used in instrumentation to label + * the threads used by this FlutterEngine. + * @param project The `FlutterDartProject` to run. + * @param allowHeadlessExecution Whether or not to allow this instance to continue + * running after passing a nil `FlutterViewController` to `-setViewController:`. + * @param restorationEnabled Whether state restoration is enabled. When true, the framework will + * wait for the attached view controller to provide restoration data. + */ +- (instancetype)initWithName:(NSString*)labelPrefix + project:(nullable FlutterDartProject*)project + allowHeadlessExecution:(BOOL)allowHeadlessExecution + restorationEnabled:(BOOL)restorationEnabled NS_DESIGNATED_INITIALIZER; + (instancetype)new NS_UNAVAILABLE; @@ -273,6 +297,16 @@ FLUTTER_EXPORT */ @property(nonatomic, readonly) FlutterMethodChannel* navigationChannel; +/** + * The `FlutterMethodChannel` used for restoration related platform messages. + * + * Can be nil after `destroyContext` is called. + * + * @see [Restoration + * Channel](https://api.flutter.dev/flutter/services/SystemChannels/restoration-constant.html) + */ +@property(nonatomic, readonly) FlutterMethodChannel* restorationChannel; + /** * The `FlutterMethodChannel` used for core platform messages, such as * information about the screen orientation. diff --git a/shell/platform/darwin/ios/framework/Headers/FlutterHeadlessDartRunner.h b/shell/platform/darwin/ios/framework/Headers/FlutterHeadlessDartRunner.h index 40bc84d82706b..17391ff937644 100644 --- a/shell/platform/darwin/ios/framework/Headers/FlutterHeadlessDartRunner.h +++ b/shell/platform/darwin/ios/framework/Headers/FlutterHeadlessDartRunner.h @@ -34,7 +34,7 @@ FLUTTER_DEPRECATED("FlutterEngine should be used rather than FlutterHeadlessDart @interface FlutterHeadlessDartRunner : FlutterEngine /** - * Iniitalize this FlutterHeadlessDartRunner with a `FlutterDartProject`. + * Initialize this FlutterHeadlessDartRunner with a `FlutterDartProject`. * * If the FlutterDartProject is not specified, the FlutterHeadlessDartRunner will attempt to locate * the project in a default location. @@ -49,7 +49,7 @@ FLUTTER_DEPRECATED("FlutterEngine should be used rather than FlutterHeadlessDart - (instancetype)initWithName:(NSString*)labelPrefix project:(FlutterDartProject*)projectOrNil; /** - * Iniitalize this FlutterHeadlessDartRunner with a `FlutterDartProject`. + * Initialize this FlutterHeadlessDartRunner with a `FlutterDartProject`. * * If the FlutterDartProject is not specified, the FlutterHeadlessDartRunner will attempt to locate * the project in a default location. @@ -64,7 +64,27 @@ FLUTTER_DEPRECATED("FlutterEngine should be used rather than FlutterHeadlessDart */ - (instancetype)initWithName:(NSString*)labelPrefix project:(FlutterDartProject*)projectOrNil - allowHeadlessExecution:(BOOL)allowHeadlessExecution NS_DESIGNATED_INITIALIZER; + allowHeadlessExecution:(BOOL)allowHeadlessExecution; + +/** + * Initialize this FlutterHeadlessDartRunner with a `FlutterDartProject`. + * + * If the FlutterDartProject is not specified, the FlutterHeadlessDartRunner will attempt to locate + * the project in a default location. + * + * A newly initialized engine will not run the `FlutterDartProject` until either + * `-runWithEntrypoint:` or `-runWithEntrypoint:libraryURI` is called. + * + * @param labelPrefix The label prefix used to identify threads for this instance. Should + * be unique across FlutterEngine instances + * @param projectOrNil The `FlutterDartProject` to run. + * @param allowHeadlessExecution Must be set to `YES`. + * @param restorationEnabled Must be set to `NO`. + */ +- (instancetype)initWithName:(NSString*)labelPrefix + project:(FlutterDartProject*)projectOrNil + allowHeadlessExecution:(BOOL)allowHeadlessExecution + restorationEnabled:(BOOL)restorationEnabled NS_DESIGNATED_INITIALIZER; /** * Not recommended for use - will initialize with a default label ("io.flutter.headless") diff --git a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm index 1e574aec6343d..72fa8344f64a6 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate.mm @@ -11,9 +11,10 @@ #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"; +static NSString* const kUIBackgroundMode = @"UIBackgroundModes"; +static NSString* const kRemoteNotificationCapabitiliy = @"remote-notification"; +static NSString* const kBackgroundFetchCapatibility = @"fetch"; +static NSString* const kRestorationStateAppModificationKey = @"mod-date"; @interface FlutterAppDelegate () @property(nonatomic, copy) FlutterViewController* (^rootFlutterViewControllerGetter)(void); @@ -308,4 +309,37 @@ - (void)logCapabilityConfigurationWarningIfNeeded:(SEL)selector { } } +#pragma mark - State Restoration + +- (BOOL)application:(UIApplication*)application shouldSaveApplicationState:(NSCoder*)coder { + [coder encodeInt64:self.lastAppModificationTime forKey:kRestorationStateAppModificationKey]; + return YES; +} + +- (BOOL)application:(UIApplication*)application shouldRestoreApplicationState:(NSCoder*)coder { + int64_t stateDate = [coder decodeInt64ForKey:kRestorationStateAppModificationKey]; + return self.lastAppModificationTime == stateDate; +} + +- (BOOL)application:(UIApplication*)application shouldSaveSecureApplicationState:(NSCoder*)coder { + [coder encodeInt64:self.lastAppModificationTime forKey:kRestorationStateAppModificationKey]; + return YES; +} + +- (BOOL)application:(UIApplication*)application + shouldRestoreSecureApplicationState:(NSCoder*)coder { + int64_t stateDate = [coder decodeInt64ForKey:kRestorationStateAppModificationKey]; + return self.lastAppModificationTime == stateDate; +} + +- (int64_t)lastAppModificationTime { + NSDate* fileDate; + NSError* error = nil; + [[[NSBundle mainBundle] executableURL] getResourceValue:&fileDate + forKey:NSURLContentModificationDateKey + error:&error]; + NSAssert(error == nil, @"Cannot obtain modification date of main bundle: %@", error); + return [fileDate timeIntervalSince1970]; +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm index 9c4a259e02c96..ccad2bb7f7582 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine.mm @@ -71,8 +71,10 @@ @implementation FlutterEngine { // Channels fml::scoped_nsobject _platformPlugin; fml::scoped_nsobject _textInputPlugin; + fml::scoped_nsobject _restorationPlugin; fml::scoped_nsobject _localizationChannel; fml::scoped_nsobject _navigationChannel; + fml::scoped_nsobject _restorationChannel; fml::scoped_nsobject _platformChannel; fml::scoped_nsobject _platformViewsChannel; fml::scoped_nsobject _textInputChannel; @@ -84,6 +86,7 @@ @implementation FlutterEngine { int64_t _nextTextureId; BOOL _allowHeadlessExecution; + BOOL _restorationEnabled; FlutterBinaryMessengerRelay* _binaryMessenger; std::unique_ptr _connections; } @@ -103,10 +106,21 @@ - (instancetype)initWithName:(NSString*)labelPrefix project:(FlutterDartProject* - (instancetype)initWithName:(NSString*)labelPrefix project:(FlutterDartProject*)project allowHeadlessExecution:(BOOL)allowHeadlessExecution { + return [self initWithName:labelPrefix + project:project + allowHeadlessExecution:allowHeadlessExecution + restorationEnabled:NO]; +} + +- (instancetype)initWithName:(NSString*)labelPrefix + project:(FlutterDartProject*)project + allowHeadlessExecution:(BOOL)allowHeadlessExecution + restorationEnabled:(BOOL)restorationEnabled { self = [super init]; NSAssert(self, @"Super init cannot be nil"); NSAssert(labelPrefix, @"labelPrefix is required"); + _restorationEnabled = restorationEnabled; _allowHeadlessExecution = allowHeadlessExecution; _labelPrefix = [labelPrefix copy]; @@ -331,12 +345,18 @@ - (FlutterPlatformPlugin*)platformPlugin { - (FlutterTextInputPlugin*)textInputPlugin { return _textInputPlugin.get(); } +- (FlutterRestorationPlugin*)restorationPlugin { + return _restorationPlugin.get(); +} - (FlutterMethodChannel*)localizationChannel { return _localizationChannel.get(); } - (FlutterMethodChannel*)navigationChannel { return _navigationChannel.get(); } +- (FlutterMethodChannel*)restorationChannel { + return _restorationChannel.get(); +} - (FlutterMethodChannel*)platformChannel { return _platformChannel.get(); } @@ -363,6 +383,7 @@ - (NSURL*)observatoryUrl { - (void)resetChannels { _localizationChannel.reset(); _navigationChannel.reset(); + _restorationChannel.reset(); _platformChannel.reset(); _platformViewsChannel.reset(); _textInputChannel.reset(); @@ -413,6 +434,11 @@ - (void)setupChannels { _initialRoute = nil; } + _restorationChannel.reset([[FlutterMethodChannel alloc] + initWithName:@"flutter/restoration" + binaryMessenger:self.binaryMessenger + codec:[FlutterStandardMethodCodec sharedInstance]]); + _platformChannel.reset([[FlutterMethodChannel alloc] initWithName:@"flutter/platform" binaryMessenger:self.binaryMessenger @@ -452,6 +478,10 @@ - (void)setupChannels { _textInputPlugin.get().textInputDelegate = self; _platformPlugin.reset([[FlutterPlatformPlugin alloc] initWithEngine:[self getWeakPtr]]); + + _restorationPlugin.reset([[FlutterRestorationPlugin alloc] + initWithChannel:_restorationChannel.get() + restorationEnabled:_restorationEnabled]); } - (void)maybeSetupPlatformViewChannels { diff --git a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h index 36c3b2c433834..698e5fdf8df2a 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h @@ -18,6 +18,7 @@ #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterDartProject_Internal.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h" #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" #import "flutter/shell/platform/darwin/ios/platform_view_ios.h" @@ -42,6 +43,7 @@ extern NSString* const FlutterEngineWillDealloc; - (FlutterPlatformPlugin*)platformPlugin; - (std::shared_ptr&)platformViewsController; - (FlutterTextInputPlugin*)textInputPlugin; +- (FlutterRestorationPlugin*)restorationPlugin; - (void)launchEngine:(NSString*)entrypoint libraryURI:(NSString*)libraryOrNil; - (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryOrNil diff --git a/shell/platform/darwin/ios/framework/Source/FlutterHeadlessDartRunner.mm b/shell/platform/darwin/ios/framework/Source/FlutterHeadlessDartRunner.mm index 71c62522a7c30..2f3dcc28774fa 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterHeadlessDartRunner.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterHeadlessDartRunner.mm @@ -35,10 +35,24 @@ - (instancetype)initWithName:(NSString*)labelPrefix allowHeadlessExecution:(BOOL)allowHeadlessExecution { NSAssert(allowHeadlessExecution == YES, @"Cannot initialize a FlutterHeadlessDartRunner without headless execution."); + return [self initWithName:labelPrefix + project:projectOrNil + allowHeadlessExecution:allowHeadlessExecution + restorationEnabled:NO]; +} + +- (instancetype)initWithName:(NSString*)labelPrefix + project:(FlutterDartProject*)projectOrNil + allowHeadlessExecution:(BOOL)allowHeadlessExecution + restorationEnabled:(BOOL)restorationEnabled { + NSAssert(allowHeadlessExecution == YES, + @"Cannot initialize a FlutterHeadlessDartRunner without headless execution."); return [super initWithName:labelPrefix project:projectOrNil - allowHeadlessExecution:allowHeadlessExecution]; + allowHeadlessExecution:allowHeadlessExecution + restorationEnabled:restorationEnabled]; } + - (instancetype)init { return [self initWithName:@"io.flutter.headless" project:nil]; } diff --git a/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.h b/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.h new file mode 100644 index 0000000000000..36311be2340fc --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.h @@ -0,0 +1,22 @@ +// 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. + +#ifndef SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERRESTORATIONPLUGIN_H_ +#define SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERRESTORATIONPLUGIN_H_ + +#import + +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" + +@interface FlutterRestorationPlugin : NSObject +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; +- (instancetype)initWithChannel:(FlutterMethodChannel*)channel + restorationEnabled:(BOOL)waitForData NS_DESIGNATED_INITIALIZER; + +@property(nonatomic, strong) NSData* restorationData; +- (void)markRestorationComplete; +- (void)reset; +@end +#endif // SHELL_PLATFORM_IOS_FRAMEWORK_SOURCE_FLUTTERRESTORATIONPLUGIN_H_ diff --git a/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.mm b/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.mm new file mode 100644 index 0000000000000..0ff207e3b61db --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.mm @@ -0,0 +1,107 @@ +// 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 "flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.h" + +#import +#import + +#include "flutter/fml/logging.h" + +FLUTTER_ASSERT_NOT_ARC + +@interface FlutterRestorationPlugin () +@property(nonatomic, copy) FlutterResult pendingRequest; +@end + +@implementation FlutterRestorationPlugin { + BOOL _waitForData; + BOOL _restorationEnabled; +} + +- (instancetype)init { + @throw([NSException + exceptionWithName:@"FlutterRestorationPlugin must initWithChannel:restorationEnabled:" + reason:nil + userInfo:nil]); +} + +- (instancetype)initWithChannel:(FlutterMethodChannel*)channel + restorationEnabled:(BOOL)restorationEnabled { + FML_DCHECK(channel) << "channel must be set"; + self = [super init]; + if (self) { + [channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { + [self handleMethodCall:call result:result]; + }]; + _restorationEnabled = restorationEnabled; + _waitForData = restorationEnabled; + } + return self; +} + +- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { + if ([[call method] isEqualToString:@"put"]) { + NSAssert(self.pendingRequest == nil, @"Cannot put data while a get request is pending."); + FlutterStandardTypedData* data = [call arguments]; + self.restorationData = [data data]; + result(nil); + } else if ([[call method] isEqualToString:@"get"]) { + if (!_restorationEnabled || !_waitForData) { + result([self dataForFramework]); + return; + } + NSAssert(self.pendingRequest == nil, @"There can only be one pending request."); + self.pendingRequest = result; + } else { + result(FlutterMethodNotImplemented); + } +} + +- (void)setRestorationData:(NSData*)data { + if (data != _restorationData) { + [_restorationData release]; + _restorationData = [data retain]; + } + _waitForData = NO; + if (self.pendingRequest != nil) { + self.pendingRequest([self dataForFramework]); + self.pendingRequest = nil; + } +} + +- (void)markRestorationComplete { + _waitForData = NO; + if (self.pendingRequest != nil) { + NSAssert(_restorationEnabled, @"No request can be pending when restoration is disabled."); + self.pendingRequest([self dataForFramework]); + self.pendingRequest = nil; + } +} + +- (void)reset { + self.pendingRequest = nil; + self.restorationData = nil; +} + +- (NSDictionary*)dataForFramework { + if (!_restorationEnabled) { + return @{@"enabled" : @NO}; + } + if (self.restorationData == nil) { + return @{@"enabled" : @YES}; + } + return @{ + @"enabled" : @YES, + @"data" : [FlutterStandardTypedData typedDataWithBytes:self.restorationData] + }; +} + +- (void)dealloc { + [_restorationData release]; + [_pendingRequest release]; + [super dealloc]; +} + +@end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterRestorationPluginTest.mm b/shell/platform/darwin/ios/framework/Source/FlutterRestorationPluginTest.mm new file mode 100644 index 0000000000000..ce1e6e55be71d --- /dev/null +++ b/shell/platform/darwin/ios/framework/Source/FlutterRestorationPluginTest.mm @@ -0,0 +1,193 @@ +// 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 "flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.h" + +#import +#import + +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterChannels.h" +#import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" + +FLUTTER_ASSERT_ARC + +@interface FlutterRestorationPlugin () +- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result; +@end + +@interface FlutterRestorationPluginTest : XCTestCase +@end + +@implementation FlutterRestorationPluginTest { + id restorationChannel; +} + +- (void)setUp { + [super setUp]; + restorationChannel = OCMClassMock([FlutterMethodChannel class]); +} + +- (void)tearDown { + [restorationChannel stopMocking]; + + [super tearDown]; +} + +#pragma mark - Tests + +- (void)testRestorationEnabledWaitsForData { + FlutterRestorationPlugin* restorationPlugin = + [[FlutterRestorationPlugin alloc] initWithChannel:restorationChannel restorationEnabled:YES]; + + FlutterMethodCall* methodCall = [FlutterMethodCall methodCallWithMethodName:@"get" arguments:nil]; + __block id capturedResult; + [restorationPlugin handleMethodCall:methodCall + result:^(id _Nullable result) { + capturedResult = result; + }]; + XCTAssertNil(capturedResult); + + NSData* data = [@"testrestortiondata" dataUsingEncoding:NSUTF8StringEncoding]; + [restorationPlugin setRestorationData:data]; + XCTAssertEqual([capturedResult count], 2u); + XCTAssertEqual([capturedResult objectForKey:@"enabled"], @YES); + XCTAssertEqual([[capturedResult objectForKey:@"data"] data], data); +} + +- (void)testRestorationDisabledRespondsRightAway { + FlutterRestorationPlugin* restorationPlugin = + [[FlutterRestorationPlugin alloc] initWithChannel:restorationChannel restorationEnabled:NO]; + + FlutterMethodCall* methodCall = [FlutterMethodCall methodCallWithMethodName:@"get" arguments:nil]; + __block id capturedResult; + [restorationPlugin handleMethodCall:methodCall + result:^(id _Nullable result) { + capturedResult = result; + }]; + XCTAssertEqual([capturedResult count], 1u); + XCTAssertEqual([capturedResult objectForKey:@"enabled"], @NO); +} + +- (void)testRespondsRightAwayWhenDataIsSet { + FlutterRestorationPlugin* restorationPlugin = + [[FlutterRestorationPlugin alloc] initWithChannel:restorationChannel restorationEnabled:YES]; + + NSData* data = [@"testrestortiondata" dataUsingEncoding:NSUTF8StringEncoding]; + [restorationPlugin setRestorationData:data]; + + __block id capturedResult; + FlutterMethodCall* methodCall = [FlutterMethodCall methodCallWithMethodName:@"get" arguments:nil]; + [restorationPlugin handleMethodCall:methodCall + result:^(id _Nullable result) { + capturedResult = result; + }]; + XCTAssertEqual([capturedResult count], 2u); + XCTAssertEqual([capturedResult objectForKey:@"enabled"], @YES); + XCTAssertEqual([[capturedResult objectForKey:@"data"] data], data); +} + +- (void)testRespondsWithNoDataWhenRestorationIsCompletedWithoutData { + FlutterRestorationPlugin* restorationPlugin = + [[FlutterRestorationPlugin alloc] initWithChannel:restorationChannel restorationEnabled:YES]; + + FlutterMethodCall* methodCall = [FlutterMethodCall methodCallWithMethodName:@"get" arguments:nil]; + __block id capturedResult; + [restorationPlugin handleMethodCall:methodCall + result:^(id _Nullable result) { + capturedResult = result; + }]; + XCTAssertNil(capturedResult); + + [restorationPlugin markRestorationComplete]; + XCTAssertEqual([capturedResult count], 1u); + XCTAssertEqual([capturedResult objectForKey:@"enabled"], @YES); +} + +- (void)testRespondsRightAwayWithNoDataWhenRestorationIsCompleted { + FlutterRestorationPlugin* restorationPlugin = + [[FlutterRestorationPlugin alloc] initWithChannel:restorationChannel restorationEnabled:YES]; + + [restorationPlugin markRestorationComplete]; + + __block id capturedResult; + FlutterMethodCall* methodCall = [FlutterMethodCall methodCallWithMethodName:@"get" arguments:nil]; + [restorationPlugin handleMethodCall:methodCall + result:^(id _Nullable result) { + capturedResult = result; + }]; + XCTAssertEqual([capturedResult count], 1u); + XCTAssertEqual([capturedResult objectForKey:@"enabled"], @YES); +} + +- (void)testReturnsDataSetByFramework { + FlutterRestorationPlugin* restorationPlugin = + [[FlutterRestorationPlugin alloc] initWithChannel:restorationChannel restorationEnabled:YES]; + [restorationPlugin markRestorationComplete]; + + NSData* data = [@"testrestortiondata" dataUsingEncoding:NSUTF8StringEncoding]; + FlutterMethodCall* methodCall = [FlutterMethodCall + methodCallWithMethodName:@"put" + arguments:[FlutterStandardTypedData typedDataWithBytes:data]]; + [restorationPlugin handleMethodCall:methodCall + result:^(id _Nullable result) { + XCTAssertNil(result); + }]; + XCTAssertEqual([restorationPlugin restorationData], data); +} + +- (void)testRespondsWithDataSetByFramework { + FlutterRestorationPlugin* restorationPlugin = + [[FlutterRestorationPlugin alloc] initWithChannel:restorationChannel restorationEnabled:YES]; + [restorationPlugin markRestorationComplete]; + + NSData* data = [@"testrestortiondata" dataUsingEncoding:NSUTF8StringEncoding]; + FlutterMethodCall* methodCall = [FlutterMethodCall + methodCallWithMethodName:@"put" + arguments:[FlutterStandardTypedData typedDataWithBytes:data]]; + [restorationPlugin handleMethodCall:methodCall + result:^(id _Nullable result) { + XCTAssertNil(result); + }]; + XCTAssertEqual([restorationPlugin restorationData], data); + + __block id capturedResult; + methodCall = [FlutterMethodCall methodCallWithMethodName:@"get" arguments:nil]; + [restorationPlugin handleMethodCall:methodCall + result:^(id _Nullable result) { + capturedResult = result; + }]; + XCTAssertEqual([capturedResult count], 2u); + XCTAssertEqual([capturedResult objectForKey:@"enabled"], @YES); + XCTAssertEqual([[capturedResult objectForKey:@"data"] data], data); +} + +- (void)testResetClearsData { + FlutterRestorationPlugin* restorationPlugin = + [[FlutterRestorationPlugin alloc] initWithChannel:restorationChannel restorationEnabled:YES]; + [restorationPlugin markRestorationComplete]; + + NSData* data = [@"testrestortiondata" dataUsingEncoding:NSUTF8StringEncoding]; + FlutterMethodCall* methodCall = [FlutterMethodCall + methodCallWithMethodName:@"put" + arguments:[FlutterStandardTypedData typedDataWithBytes:data]]; + [restorationPlugin handleMethodCall:methodCall + result:^(id _Nullable result) { + XCTAssertNil(result); + }]; + XCTAssertEqual([restorationPlugin restorationData], data); + + [restorationPlugin reset]; + XCTAssertNil([restorationPlugin restorationData]); + + __block id capturedResult; + methodCall = [FlutterMethodCall methodCallWithMethodName:@"get" arguments:nil]; + [restorationPlugin handleMethodCall:methodCall + result:^(id _Nullable result) { + capturedResult = result; + }]; + XCTAssertEqual([capturedResult count], 1u); + XCTAssertEqual([capturedResult objectForKey:@"enabled"], @YES); +} + +@end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index a0cffaace53b3..df26adc95048f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -191,7 +191,8 @@ - (void)sharedSetupWithProject:(nullable FlutterDartProject*)project auto engine = fml::scoped_nsobject{[[FlutterEngine alloc] initWithName:@"io.flutter" project:project - allowHeadlessExecution:self.engineAllowHeadlessExecution]}; + allowHeadlessExecution:self.engineAllowHeadlessExecution + restorationEnabled:[self restorationIdentifier] != nil]}; if (!engine) { return; @@ -654,6 +655,7 @@ - (void)viewWillAppear:(BOOL)animated { [self surfaceUpdated:YES]; } [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"]; + [[_engine.get() restorationPlugin] markRestorationComplete]; [super viewWillAppear:animated]; } @@ -1523,4 +1525,20 @@ - (void)scrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) [_engine.get() dispatchPointerDataPacket:std::move(packet)]; } +#pragma mark - State Restoration + +- (void)encodeRestorableStateWithCoder:(NSCoder*)coder { + NSData* restorationData = [[_engine.get() restorationPlugin] restorationData]; + [coder encodeDataObject:restorationData]; +} + +- (void)decodeRestorableStateWithCoder:(NSCoder*)coder { + NSData* restorationData = [coder decodeDataObject]; + [[_engine.get() restorationPlugin] setRestorationData:restorationData]; +} + +- (FlutterRestorationPlugin*)restorationPlugin { + return [_engine.get() restorationPlugin]; +} + @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h index 93584f237b4f0..f51a88df1b1cf 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h @@ -8,6 +8,7 @@ #include "flutter/fml/memory/weak_ptr.h" #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h" +#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterRestorationPlugin.h" namespace flutter { class FlutterPlatformViewsController; @@ -27,6 +28,7 @@ extern NSNotificationName const FlutterViewControllerShowHomeIndicator; @property(nonatomic, readonly) BOOL isPresentingViewController; - (fml::WeakPtr)getWeakPtr; - (std::shared_ptr&)platformViewsController; +- (FlutterRestorationPlugin*)restorationPlugin; @end diff --git a/shell/platform/darwin/ios/platform_view_ios.mm b/shell/platform/darwin/ios/platform_view_ios.mm index 1b48773517bc2..6dfb00ddd546d 100644 --- a/shell/platform/darwin/ios/platform_view_ios.mm +++ b/shell/platform/darwin/ios/platform_view_ios.mm @@ -207,6 +207,7 @@ return; } [owner_controller_.get() platformViewsController]->Reset(); + [[owner_controller_.get() restorationPlugin] reset]; } std::unique_ptr> PlatformViewIOS::ComputePlatformResolvedLocales(