From 39de96ac261b89542767b4f971f9d91dac2f2cce Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Mon, 28 Jan 2019 11:48:49 -0500 Subject: [PATCH] Re-enable landscape orientation; fix 'double activation' issue. --- Signal/src/AppDelegate.m | 87 +++++++++++++++++++ .../ViewControllers/OWSViewController.m | 2 +- 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index a8bf0367c..20f6f059f 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -68,6 +68,34 @@ static NSTimeInterval launchStartedAt; @property (nonatomic) BOOL areVersionMigrationsComplete; @property (nonatomic) BOOL didAppLaunchFail; +// Signal iOS uses multiple "key" windows, e.g. the screen lock window. +// We usually switch "key" windows while becoming active. At the same +// time, we often change the state of our orientation mask. +// +// For reasons unknown, this confuses iOS and leads to very strange +// behavior, e.g.: +// +// * Multiple activation of the app returning from the background, e.g. +// transitions from "background, inactive" -> "foreground, inactive" +// -> "foreground, active" -> "foreground, inactive" -> +// "foreground, active". +// * Multiple (sometimes incomplete) orientation changes while becoming +// active. +// * The side effects of orientation changes (e.g. safe area insets) +// being left in a bad state. +// +// The solution: +// +// * Lock app in portrait unless "foreground, active". +// * Don't "unlock" until the app has been "foreground, active" +// for a short duration (to allow activation process to safely complete). +// * After unlocking, try to rotate to the current device orientation. +// +// The user experience is reasonable: if the user activates the app +// while in landscape, the user sees a rotation animation. +@property (nonatomic) BOOL isLandscapeEnabled; +@property (nonatomic, nullable) NSTimer *landscapeTimer; + @end #pragma mark - @@ -152,11 +180,15 @@ static NSTimeInterval launchStartedAt; - (void)applicationDidEnterBackground:(UIApplication *)application { OWSLogWarn(@"applicationDidEnterBackground."); + [self disableLandscape]; + [DDLog flushLog]; } - (void)applicationWillEnterForeground:(UIApplication *)application { OWSLogWarn(@"applicationWillEnterForeground."); + + [self disableLandscape]; } - (void)applicationDidReceiveMemoryWarning:(UIApplication *)application @@ -619,6 +651,8 @@ static NSTimeInterval launchStartedAt; // be called _before_ we become active. [self clearAllNotificationsAndRestoreBadgeCount]; + [self enableLandscapeAfterDelay]; + OWSLogInfo(@"applicationDidBecomeActive completed."); } @@ -732,6 +766,8 @@ static NSTimeInterval launchStartedAt; OWSLogWarn(@"applicationWillResignActive."); + [self disableLandscape]; + [DDLog flushLog]; } @@ -948,6 +984,16 @@ static NSTimeInterval launchStartedAt; - (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(nullable UIWindow *)window { + // See comments on isLandscapeEnabled property. + if (!self.isLandscapeEnabled) { + return UIInterfaceOrientationMaskPortrait; + } + // This clause shouldn't be necessary, but it's nice to + // be explicit about our invariants. + if (!self.hasInitialRootViewController) { + return UIInterfaceOrientationMaskPortrait; + } + if (self.windowManager.hasCall) { // The call-banner window is only suitable for portrait display return UIInterfaceOrientationMaskPortrait; @@ -979,6 +1025,47 @@ static NSTimeInterval launchStartedAt; return UIInterfaceOrientationMaskAllButUpsideDown; } +// See comments on isLandscapeEnabled property. +- (void)disableLandscape +{ + OWSAssertIsOnMainThread(); + + BOOL wasEnabled = self.isLandscapeEnabled; + self.isLandscapeEnabled = NO; + [self.landscapeTimer invalidate]; + self.landscapeTimer = nil; + + if (wasEnabled) { + [UIViewController attemptRotationToDeviceOrientation]; + } +} + +// See comments on isLandscapeEnabled property. +- (void)enableLandscape +{ + OWSAssertIsOnMainThread(); + + self.isLandscapeEnabled = YES; + [self.landscapeTimer invalidate]; + self.landscapeTimer = nil; + + [UIViewController attemptRotationToDeviceOrientation]; +} + +// See comments on isLandscapeEnabled property. +- (void)enableLandscapeAfterDelay +{ + OWSAssertIsOnMainThread(); + + [self disableLandscape]; + NSTimeInterval delay = 0.35f; + self.landscapeTimer = [NSTimer weakScheduledTimerWithTimeInterval:delay + target:self + selector:@selector(enableLandscape) + userInfo:nil + repeats:NO]; +} + #pragma mark Push Notifications Delegate Methods - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { diff --git a/SignalMessaging/ViewControllers/OWSViewController.m b/SignalMessaging/ViewControllers/OWSViewController.m index e76c2918e..b4d8a06ec 100644 --- a/SignalMessaging/ViewControllers/OWSViewController.m +++ b/SignalMessaging/ViewControllers/OWSViewController.m @@ -10,7 +10,7 @@ NS_ASSUME_NONNULL_BEGIN BOOL IsLandscapeOrientationEnabled(void) { - return NO; + return YES; } UIInterfaceOrientationMask DefaultUIInterfaceOrientationMask(void)