diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist
index 2a7c8e24f3..b04d376f9c 100644
--- a/Signal/Signal-Info.plist
+++ b/Signal/Signal-Info.plist
@@ -148,6 +148,8 @@
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
UIViewControllerBasedStatusBarAppearance
diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m
index a8bf0367c1..20f6f059fd 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 e76c2918ec..b4d8a06ec9 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)