Make iOS FlutterViewController stop sending inactive/pause on app lifecycle events when not visible (#12128)

This commit is contained in:
xster 2019-09-17 12:00:46 -07:00 committed by GitHub
parent 709fc6e00c
commit 4159c2bfed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 165 additions and 82 deletions

View File

@ -537,23 +537,32 @@ typedef enum UIAccessibilityContrast : NSInteger {
TRACE_EVENT0("flutter", "applicationBecameActive");
if (_viewportMetrics.physical_width)
[self surfaceUpdated:YES];
[[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.resumed"];
[self goToApplicationLifecycle:@"AppLifecycleState.resumed"];
}
- (void)applicationWillResignActive:(NSNotification*)notification {
TRACE_EVENT0("flutter", "applicationWillResignActive");
[self surfaceUpdated:NO];
[[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"];
[self goToApplicationLifecycle:@"AppLifecycleState.inactive"];
}
- (void)applicationDidEnterBackground:(NSNotification*)notification {
TRACE_EVENT0("flutter", "applicationDidEnterBackground");
[[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.paused"];
[self goToApplicationLifecycle:@"AppLifecycleState.paused"];
}
- (void)applicationWillEnterForeground:(NSNotification*)notification {
TRACE_EVENT0("flutter", "applicationWillEnterForeground");
[[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"];
[self goToApplicationLifecycle:@"AppLifecycleState.inactive"];
}
// Make this transition only while this current view controller is visible.
- (void)goToApplicationLifecycle:(nonnull NSString*)state {
// Accessing self.view will create the view. Check whether the view is organically loaded
// first before checking whether the view is attached to window.
if (self.isViewLoaded && self.view.window) {
[[_engine.get() lifecycleChannel] sendMessage:state];
}
}
#pragma mark - Touch event handling

View File

@ -7,7 +7,7 @@
@implementation ScreenBeforeFlutter
FlutterEngine* _engine;
@synthesize engine = _engine;
- (id)initWithEngineRunCompletion:(void (^)(void))engineRunCompletion {
self = [super init];

View File

@ -6,6 +6,28 @@
#import <XCTest/XCTest.h>
#import "ScreenBeforeFlutter.h"
@interface XCAppLifecycleTestExpectation : XCTestExpectation
- (instancetype)initForLifecycle:(NSString*)expectedLifecycle forStep:(NSString*)step;
@property(nonatomic, readonly, copy) NSString* expectedLifecycle;
@end
@implementation XCAppLifecycleTestExpectation
@synthesize expectedLifecycle = _expectedLifecycle;
- (instancetype)initForLifecycle:(NSString*)expectedLifecycle forStep:(NSString*)step {
// The step is here because the callbacks into the handler which checks these expectations isn't
// synchronous with the executions in the test, so it's hard to find the cause in the test
// otherwise.
self = [super initWithDescription:[NSString stringWithFormat:@"Expected state %@ during step %@",
expectedLifecycle, step]];
_expectedLifecycle = [expectedLifecycle copy];
return self;
}
@end
@interface AppLifecycleTests : XCTestCase
@end
@ -16,7 +38,7 @@
self.continueAfterFailure = NO;
}
- (void)testLifecycleChannel {
- (void)testDismissedFlutterViewControllerNotRespondingToApplicationLifecycle {
XCTestExpectation* engineStartedExpectation = [self expectationWithDescription:@"Engine started"];
// Let the engine finish booting (at the end of which the channels are properly set-up) before
@ -32,121 +54,173 @@
FlutterEngine* engine = rootVC.engine;
NSMutableArray* lifecycleExpectations = [NSMutableArray arrayWithCapacity:10];
NSMutableArray* lifecycleEvents = [NSMutableArray arrayWithCapacity:10];
[lifecycleExpectations addObject:[[XCTestExpectation alloc]
initWithDescription:@"A loading FlutterViewController goes "
@"through AppLifecycleState.inactive"]];
[lifecycleExpectations
addObject:[[XCTestExpectation alloc]
initWithDescription:
@"A loading FlutterViewController goes through AppLifecycleState.resumed"]];
// Expected sequence from showing the FlutterViewController is inactive and resumed.
[lifecycleExpectations addObjectsFromArray:@[
[[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.inactive"
forStep:@"showing a FlutterViewController"],
[[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.resumed"
forStep:@"showing a FlutterViewController"]
]];
// Holding onto this FlutterViewController is consequential here. Since a released
// FlutterViewController wouldn't keep listening to the application lifecycle events and produce
// false positives for the application lifecycle tests further below.
FlutterViewController* flutterVC = [rootVC showFlutter];
[engine.lifecycleChannel setMessageHandler:^(id message, FlutterReply callback) {
if (lifecycleExpectations.count == 0) {
XCTFail(@"Unexpected lifecycle transition: %@", message);
return;
}
[lifecycleEvents addObject:message];
[[lifecycleExpectations objectAtIndex:0] fulfill];
XCAppLifecycleTestExpectation* nextExpectation = [lifecycleExpectations objectAtIndex:0];
if (![[nextExpectation expectedLifecycle] isEqualToString:message]) {
XCTFail(@"Expected lifecycle %@ but instead received %@", [nextExpectation expectedLifecycle],
message);
return;
}
[nextExpectation fulfill];
[lifecycleExpectations removeObjectAtIndex:0];
}];
[self waitForExpectations:lifecycleExpectations timeout:5];
// Expected sequence from showing the FlutterViewController is inactive and resumed.
NSArray* expectedStates = @[ @"AppLifecycleState.inactive", @"AppLifecycleState.resumed" ];
XCTAssertEqualObjects(lifecycleEvents, expectedStates,
@"AppLifecycleState transitions while presenting not as expected");
// The expectations list isn't dequeued by the message handler yet.
[self waitForExpectations:lifecycleExpectations timeout:5 enforceOrder:YES];
// Now dismiss the FlutterViewController again and expect another inactive and paused.
[lifecycleExpectations
addObject:[[XCTestExpectation alloc]
initWithDescription:@"A dismissed FlutterViewController goes through "
@"AppLifecycleState.inactive"]];
[lifecycleExpectations
addObject:[[XCTestExpectation alloc]
initWithDescription:@"A dismissed FlutterViewController goes through "
@"AppLifecycleState.paused"]];
[lifecycleExpectations addObjectsFromArray:@[
[[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.inactive"
forStep:@"dismissing a FlutterViewController"],
[[XCAppLifecycleTestExpectation alloc]
initForLifecycle:@"AppLifecycleState.paused"
forStep:@"dismissing a FlutterViewController"]
]];
[flutterVC dismissViewControllerAnimated:NO completion:nil];
[self waitForExpectations:lifecycleExpectations timeout:5];
expectedStates = @[
@"AppLifecycleState.inactive", @"AppLifecycleState.resumed", @"AppLifecycleState.inactive",
@"AppLifecycleState.paused"
];
XCTAssertEqualObjects(lifecycleEvents, expectedStates,
@"AppLifecycleState transitions while dismissing not as expected");
[self waitForExpectations:lifecycleExpectations timeout:5 enforceOrder:YES];
// Now put the app in the background (while the engine is still running) and bring it back to
// the foreground. Granted, we're not winning any awards for hyper-realism but at least we're
// checking that we aren't observing the UIApplication notifications and double registering
// for AppLifecycleState events.
// However the production is temporarily wrong. https://github.com/flutter/flutter/issues/37226.
// It will be fixed in a next PR that removes the wrong asserts.
[lifecycleExpectations
addObject:
[[XCTestExpectation alloc]
initWithDescription:@"Current implementation sends another AppLifecycleState event"]];
// These operations are synchronous so if they trigger any lifecycle events, they should trigger
// failures in the message handler immediately.
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationWillResignActiveNotification
object:nil];
[lifecycleExpectations
addObject:
[[XCTestExpectation alloc]
initWithDescription:@"Current implementation sends another AppLifecycleState event"]];
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidEnterBackgroundNotification
object:nil];
[lifecycleExpectations
addObject:
[[XCTestExpectation alloc]
initWithDescription:@"Current implementation sends another AppLifecycleState event"]];
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationWillEnterForegroundNotification
object:nil];
[lifecycleExpectations
addObject:
[[XCTestExpectation alloc]
initWithDescription:@"Current implementation sends another AppLifecycleState event"]];
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidBecomeActiveNotification
object:nil];
// There's no timing latch for our semi-fake background-foreground cycle so launch the
// FlutterViewController again to check the complete event list again.
[lifecycleExpectations addObject:[[XCTestExpectation alloc]
initWithDescription:@"A second FlutterViewController goes "
@"through AppLifecycleState.inactive"]];
[lifecycleExpectations
addObject:[[XCTestExpectation alloc]
initWithDescription:
@"A second FlutterViewController goes through AppLifecycleState.resumed"]];
// Expect only lifecycle events from showing the FlutterViewController again, not from any
// backgrounding/foregrounding.
[lifecycleExpectations addObjectsFromArray:@[
[[XCAppLifecycleTestExpectation alloc]
initForLifecycle:@"AppLifecycleState.inactive"
forStep:@"showing a FlutterViewController a second time after backgrounding"],
[[XCAppLifecycleTestExpectation alloc]
initForLifecycle:@"AppLifecycleState.resumed"
forStep:@"showing a FlutterViewController a second time after backgrounding"]
]];
flutterVC = [rootVC showFlutter];
[self waitForExpectations:lifecycleExpectations timeout:5];
expectedStates = @[
@"AppLifecycleState.inactive", @"AppLifecycleState.resumed", @"AppLifecycleState.inactive",
@"AppLifecycleState.paused",
// The production code currently misbehaves. https://github.com/flutter/flutter/issues/37226.
// It will be fixed in a next PR that removes the wrong asserts.
@"AppLifecycleState.inactive", @"AppLifecycleState.paused", @"AppLifecycleState.inactive",
@"AppLifecycleState.resumed",
// We only added 2 from re-launching the FlutterViewController
// and none from the background-foreground cycle.
@"AppLifecycleState.inactive", @"AppLifecycleState.resumed"
];
XCTAssertEqualObjects(
lifecycleEvents, expectedStates,
@"AppLifecycleState transitions while presenting a second time not as expected");
[self waitForExpectations:lifecycleExpectations timeout:5 enforceOrder:YES];
// Dismantle.
[engine.lifecycleChannel setMessageHandler:nil];
[flutterVC dismissViewControllerAnimated:NO completion:nil];
flutterVC = nil;
[engine setViewController:nil];
[rootVC dismissViewControllerAnimated:NO completion:nil];
rootVC = nil;
}
- (void)testVisibleFlutterViewControllerRespondsToApplicationLifecycle {
XCTestExpectation* engineStartedExpectation = [self expectationWithDescription:@"Engine started"];
// Let the engine finish booting (at the end of which the channels are properly set-up) before
// moving onto the next step of showing the next view controller.
ScreenBeforeFlutter* rootVC = [[ScreenBeforeFlutter alloc] initWithEngineRunCompletion:^void() {
[engineStartedExpectation fulfill];
}];
[self waitForExpectationsWithTimeout:5 handler:nil];
UIApplication* application = UIApplication.sharedApplication;
application.delegate.window.rootViewController = rootVC;
FlutterEngine* engine = rootVC.engine;
NSMutableArray* lifecycleExpectations = [NSMutableArray arrayWithCapacity:10];
// Expected sequence from showing the FlutterViewController is inactive and resumed.
[lifecycleExpectations addObjectsFromArray:@[
[[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.inactive"
forStep:@"showing a FlutterViewController"],
[[XCAppLifecycleTestExpectation alloc] initForLifecycle:@"AppLifecycleState.resumed"
forStep:@"showing a FlutterViewController"]
]];
FlutterViewController* flutterVC = [rootVC showFlutter];
[engine.lifecycleChannel setMessageHandler:^(id message, FlutterReply callback) {
if (lifecycleExpectations.count == 0) {
XCTFail(@"Unexpected lifecycle transition: %@", message);
return;
}
XCAppLifecycleTestExpectation* nextExpectation = [lifecycleExpectations objectAtIndex:0];
if (![[nextExpectation expectedLifecycle] isEqualToString:message]) {
XCTFail(@"Expected lifecycle %@ but instead received %@", [nextExpectation expectedLifecycle],
message);
return;
}
[nextExpectation fulfill];
[lifecycleExpectations removeObjectAtIndex:0];
}];
[self waitForExpectations:lifecycleExpectations timeout:5];
// Now put the FlutterViewController into background.
[lifecycleExpectations addObjectsFromArray:@[
[[XCAppLifecycleTestExpectation alloc]
initForLifecycle:@"AppLifecycleState.inactive"
forStep:@"putting FlutterViewController to the background"],
[[XCAppLifecycleTestExpectation alloc]
initForLifecycle:@"AppLifecycleState.paused"
forStep:@"putting FlutterViewController to the background"]
]];
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationWillResignActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidEnterBackgroundNotification
object:nil];
[self waitForExpectations:lifecycleExpectations timeout:5];
// Now restore to foreground
[lifecycleExpectations addObjectsFromArray:@[
[[XCAppLifecycleTestExpectation alloc]
initForLifecycle:@"AppLifecycleState.inactive"
forStep:@"putting FlutterViewController back to foreground"],
[[XCAppLifecycleTestExpectation alloc]
initForLifecycle:@"AppLifecycleState.resumed"
forStep:@"putting FlutterViewController back to foreground"]
]];
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationWillEnterForegroundNotification
object:nil];
[[NSNotificationCenter defaultCenter]
postNotificationName:UIApplicationDidBecomeActiveNotification
object:nil];
[self waitForExpectations:lifecycleExpectations timeout:5];
// Dismantle.
[engine.lifecycleChannel setMessageHandler:nil];
[flutterVC dismissViewControllerAnimated:NO completion:nil];
[engine setViewController:nil];
}
@end