diff --git a/Signal/src/ViewControllers/AppSettings/PrivacySettingsTableViewController.m b/Signal/src/ViewControllers/AppSettings/PrivacySettingsTableViewController.m index 12af5dbd5..96cda65f7 100644 --- a/Signal/src/ViewControllers/AppSettings/PrivacySettingsTableViewController.m +++ b/Signal/src/ViewControllers/AppSettings/PrivacySettingsTableViewController.m @@ -299,28 +299,7 @@ NS_ASSUME_NONNULL_BEGIN DDLogInfo(@"%@ trying to set is screen lock enabled: %@", self.logTag, @(shouldBeEnabled)); - __weak typeof(self) weakSelf = self; - if (shouldBeEnabled) { - [OWSScreenLock.sharedManager tryToEnableScreenLockWithCompletion:^(NSError *_Nullable error) { - [weakSelf updateTableContents]; - - if (error) { - [OWSAlerts showAlertWithTitle:NSLocalizedString(@"SCREEN_LOCK_ENABLE_FAILED", - @"Title for alert indicating that screen lock could not be enabled.") - message:error.localizedDescription]; - } - }]; - } else { - [OWSScreenLock.sharedManager tryToDisableScreenLockWithCompletion:^(NSError *_Nullable error) { - [weakSelf updateTableContents]; - - if (error) { - [OWSAlerts showAlertWithTitle:NSLocalizedString(@"SCREEN_LOCK_DISABLE_FAILED", - @"Title for alert indicating that screen lock could not be disabled.") - message:error.localizedDescription]; - } - }]; - } + [OWSScreenLock.sharedManager setIsScreenLockEnabled:shouldBeEnabled]; } - (void)screenLockDidChange:(NSNotification *)notification diff --git a/Signal/src/util/OWSScreenLock.swift b/Signal/src/util/OWSScreenLock.swift index f549a39e4..7de46a978 100644 --- a/Signal/src/util/OWSScreenLock.swift +++ b/Signal/src/util/OWSScreenLock.swift @@ -24,7 +24,6 @@ import LocalAuthentication 0 ] - @objc public static let ScreenLockWasEnabled = Notification.Name("ScreenLockWasEnabled") @objc public static let ScreenLockDidChange = Notification.Name("ScreenLockDidChange") let primaryStorage: OWSPrimaryStorage @@ -34,10 +33,6 @@ import LocalAuthentication private let OWSScreenLock_Key_IsScreenLockEnabled = "OWSScreenLock_Key_IsScreenLockEnabled" private let OWSScreenLock_Key_ScreenLockTimeoutSeconds = "OWSScreenLock_Key_ScreenLockTimeoutSeconds" - // We don't want the verification process itself to trigger unlock verification. - // Passcode-code only authentication process deactivates the app. - private var ignoreUnlockUntilActive = false - // We temporarily resign any first responder while the Screen Lock is presented. weak var firstResponderBeforeLockscreen: UIResponder? @@ -53,21 +48,6 @@ import LocalAuthentication super.init() SwiftSingletons.register(self) - - NotificationCenter.default.addObserver(self, - selector: #selector(didBecomeActive), - name: NSNotification.Name.OWSApplicationDidBecomeActive, - object: nil) - } - - deinit { - NotificationCenter.default.removeObserver(self) - } - - func didBecomeActive() { - AssertIsOnMainThread() - - ignoreUnlockUntilActive = false } // MARK: - Properties @@ -83,17 +63,13 @@ import LocalAuthentication return self.dbConnection.bool(forKey: OWSScreenLock_Key_IsScreenLockEnabled, inCollection: OWSScreenLock_Collection, defaultValue: false) } - private func setIsScreenLockEnabled(value: Bool) { + @objc + public func setIsScreenLockEnabled(_ value: Bool) { AssertIsOnMainThread() assert(OWSStorage.isStorageReady()) - let isEnabling = value && !isScreenLockEnabled() - self.dbConnection.setBool(value, forKey: OWSScreenLock_Key_IsScreenLockEnabled, inCollection: OWSScreenLock_Collection) - if isEnabling { - NotificationCenter.default.postNotificationNameAsync(OWSScreenLock.ScreenLockWasEnabled, object: nil) - } NotificationCenter.default.postNotificationNameAsync(OWSScreenLock.ScreenLockDidChange, object: nil) } @@ -119,74 +95,18 @@ import LocalAuthentication // MARK: - Methods - // On failure, completion is called with an error argument. - // On success or cancel, completion is called with nil argument. - // Success and cancel can be differentiated by consulting - // isScreenLockEnabled. - @objc public func tryToEnableScreenLock(completion: @escaping ((Error?) -> Void)) { - tryToVerifyLocalAuthentication(localizedReason: NSLocalizedString("SCREEN_LOCK_REASON_ENABLE_SCREEN_LOCK", - comment: "Description of how and why Signal iOS uses Touch ID/Face ID/Phone Passcode to enable 'screen lock'."), - completion: { (outcome: OWSScreenLockOutcome) in - AssertIsOnMainThread() - - switch outcome { - case .failure(let error): - completion(self.authenticationError(errorDescription: error)) - case .unexpectedFailure(let error): - completion(self.authenticationError(errorDescription: error)) - case .success: - self.setIsScreenLockEnabled(value: true) - completion(nil) - case .cancel: - completion(nil) - } - }) - } - - // On failure, completion is called with an error argument. - // On success or cancel, completion is called with nil argument. - // Success and cancel can be differentiated by consulting - // isScreenLockEnabled. - @objc public func tryToDisableScreenLock(completion: @escaping ((Error?) -> Void)) { - tryToVerifyLocalAuthentication(localizedReason: NSLocalizedString("SCREEN_LOCK_REASON_DISABLE_SCREEN_LOCK", - comment: "Description of how and why Signal iOS uses Touch ID/Face ID/Phone Passcode to disable 'screen lock'."), - completion: { (outcome: OWSScreenLockOutcome) in - AssertIsOnMainThread() - - switch outcome { - case .failure(let error): - completion(self.authenticationError(errorDescription: error)) - case .unexpectedFailure(let error): - completion(self.authenticationError(errorDescription: error)) - case .success: - self.setIsScreenLockEnabled(value: false) - completion(nil) - case .cancel: - completion(nil) - } - }) - } - @objc public func tryToUnlockScreenLock(success: @escaping (() -> Void), failure: @escaping ((Error) -> Void), unexpectedFailure: @escaping ((Error) -> Void), cancel: @escaping (() -> Void)) { - guard !ignoreUnlockUntilActive else { - DispatchQueue.main.async { - success() - } + guard CurrentAppContext().isMainAppAndActive else { + owsFail("\(self.logTag) \(#function) Unexpected request for 'screen lock' unlock UI while app is inactive.") + cancel() return } - // A popped keyboard breaks our layout and obscures the unlock button. - if let firstResponder = UIResponder.currentFirstResponder() { - Logger.debug("\(self.logTag) in \(#function) resigning first responder: \(firstResponder)") - firstResponder.resignFirstResponder() - self.firstResponderBeforeLockscreen = firstResponder - } - tryToVerifyLocalAuthentication(localizedReason: NSLocalizedString("SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK", - comment: "Description of how and why Signal iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'."), + comment: "Description of how and why Signal iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'."), completion: { (outcome: OWSScreenLockOutcome) in AssertIsOnMainThread() @@ -196,16 +116,6 @@ import LocalAuthentication case .unexpectedFailure(let error): unexpectedFailure(self.authenticationError(errorDescription: error)) case .success: - // It's important we restore first responder status once the user completes - // In some cases, (RegistrationLock Reminder) it just puts the keyboard back where - // the user needs it, saving them a tap. - // But in the case of an inputAccessoryView, like the ConversationViewController, - // failing to restore firstResponder could make the input toolbar disappear until - if let firstResponder = self.firstResponderBeforeLockscreen { - Logger.debug("\(self.logTag) in \(#function) regaining first responder: \(firstResponder)") - firstResponder.becomeFirstResponder() - self.firstResponderBeforeLockscreen = nil - } success() case .cancel: cancel() @@ -255,8 +165,6 @@ import LocalAuthentication return } - // Use ignoreUnlockUntilActive to suppress unlock verifications. - ignoreUnlockUntilActive = true context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: localizedReason) { success, evaluateError in if success { @@ -349,10 +257,8 @@ import LocalAuthentication private func screenLockContext() -> LAContext { let context = LAContext() - // If user has set any non-zero timeout, recycle biometric auth - // in the same period as our normal screen lock timeout, up to - // max of 10 seconds. - context.touchIDAuthenticationAllowableReuseDuration = TimeInterval(min(10.0, screenLockTimeout())) + // Never recycle biometric auth. + context.touchIDAuthenticationAllowableReuseDuration = TimeInterval(0) if #available(iOS 11.0, *) { assert(!context.interactionNotAllowed) diff --git a/Signal/src/util/OWSScreenLockUI.m b/Signal/src/util/OWSScreenLockUI.m index 2d5d3232a..16205ba6e 100644 --- a/Signal/src/util/OWSScreenLockUI.m +++ b/Signal/src/util/OWSScreenLockUI.m @@ -9,6 +9,29 @@ NS_ASSUME_NONNULL_BEGIN +typedef NS_ENUM(NSUInteger, ScreenLockUIState) { + ScreenLockUIStateNone, + // Shown while app is inactive or background, if enabled. + ScreenLockUIStateScreenProtection, + // Shown while app is active, if enabled. + ScreenLockUIStateScreenLock, +}; + +NSString *NSStringForScreenLockUIState(ScreenLockUIState value); +NSString *NSStringForScreenLockUIState(ScreenLockUIState value) +{ + switch (value) { + case ScreenLockUIStateNone: + return @"ScreenLockUIStateNone"; + case ScreenLockUIStateScreenProtection: + return @"ScreenLockUIStateScreenProtection"; + case ScreenLockUIStateScreenLock: + return @"ScreenLockUIStateScreenLock"; + } +} + +const UIWindowLevel UIWindowLevel_Background = -1.f; + @interface OWSScreenLockUI () @property (nonatomic) UIWindow *screenBlockingWindow; @@ -18,35 +41,43 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) NSArray *screenBlockingConstraints; @property (nonatomic) NSString *screenBlockingSignature; -// Unlike UIApplication.applicationState, this state is -// updated conservatively, e.g. the flag is cleared during -// "will enter background." -@property (nonatomic) BOOL appIsInactive; +// Unlike UIApplication.applicationState, this state reflects the +// notifications, i.e. "did become active", "will resign active", +// "will enter foreground", "did enter background". +// +// We want to update our state to reflect these transitions and have +// the "update" logic be consistent with "last reported" state. i.e. +// when you're responding to "will resign active", we need to behave +// as though we're already inactive. +// +// Secondly, we need to show the screen protection _before_ we become +// inactive in order for it to be reflected in the app switcher. +@property (nonatomic) BOOL appIsInactiveOrBackground; @property (nonatomic) BOOL appIsInBackground; @property (nonatomic) BOOL isShowingScreenLockUI; @property (nonatomic) BOOL didLastUnlockAttemptFail; // We want to remain in "screen lock" mode while "local auth" -// UI is dismissing. +// UI is dismissing. So we lazily clear isShowingScreenLockUI +// using this property. @property (nonatomic) BOOL shouldClearAuthUIWhenActive; // Indicates whether or not the user is currently locked out of -// the app. Only applies if OWSScreenLock.isScreenLockEnabled. +// the app. Should only be set if OWSScreenLock.isScreenLockEnabled. // -// * The user is locked out out by default on app launch. +// * The user is locked out by default on app launch. // * The user is also locked out if they spend more than // "timeout" seconds outside the app. When the user leaves // the app, a "countdown" begins. -@property (nonatomic) BOOL isScreenLockUnlocked; +@property (nonatomic) BOOL isScreenLockLocked; +// The "countdown" until screen lock takes effect. @property (nonatomic, nullable) NSDate *screenLockCountdownDate; -// We normally start the "countdown" when the app enters the background, -// But we also want to start the "countdown" if the app is inactive for -// more than N seconds. -@property (nonatomic, nullable) NSTimer *inactiveTimer; +@property (nonatomic) UIWindow *rootWindow; +@property (nonatomic, nullable) UIResponder *rootWindowResponder; @end @@ -72,6 +103,10 @@ NS_ASSUME_NONNULL_BEGIN return self; } + OWSAssertIsOnMainThread(); + + _appIsInactiveOrBackground = [UIApplication sharedApplication].applicationState != UIApplicationStateActive; + [self observeNotifications]; OWSSingletonAssert(); @@ -102,17 +137,13 @@ NS_ASSUME_NONNULL_BEGIN selector:@selector(applicationDidEnterBackground:) name:OWSApplicationDidEnterBackgroundNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(registrationStateDidChange) - name:RegistrationStateDidChangeNotification - object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(screenLockDidChange:) name:OWSScreenLock.ScreenLockDidChange object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(screenLockWasEnabled:) - name:OWSScreenLock.ScreenLockWasEnabled + selector:@selector(clockDidChange:) + name:NSSystemClockDidChangeNotification object:nil]; } @@ -121,27 +152,50 @@ NS_ASSUME_NONNULL_BEGIN OWSAssertIsOnMainThread(); OWSAssert(rootWindow); + self.rootWindow = rootWindow; + [self prepareScreenProtectionWithRootWindow:rootWindow]; + // Initialize the screen lock state. + // + // It's not safe to access OWSScreenLock.isScreenLockEnabled + // until the app is ready. [AppReadiness runNowOrWhenAppIsReady:^{ - [self ensureScreenProtection]; + self.isScreenLockLocked = OWSScreenLock.sharedManager.isScreenLockEnabled; + + [self ensureUI]; }]; } #pragma mark - Methods -- (void)tryToActivateScreenLockUponBecomingActive +- (void)tryToActivateScreenLockBasedOnCountdown { - OWSAssert(!self.appIsInactive); + OWSAssert(!self.appIsInBackground); + OWSAssertIsOnMainThread(); - if (!self.isScreenLockUnlocked) { - // Screen lock is already activated. + if (!AppReadiness.isAppReady) { + // It's not safe to access OWSScreenLock.isScreenLockEnabled + // until the app is ready. + // + // We don't need to try to lock the screen lock; + // It will be initialized by `setupWithRootWindow`. DDLogVerbose(@"%@ tryToActivateScreenLockUponBecomingActive NO 0", self.logTag); return; } + if (!OWSScreenLock.sharedManager.isScreenLockEnabled) { + // Screen lock is not enabled. + DDLogVerbose(@"%@ tryToActivateScreenLockUponBecomingActive NO 1", self.logTag); + return; + } + if (self.isScreenLockLocked) { + // Screen lock is already activated. + DDLogVerbose(@"%@ tryToActivateScreenLockUponBecomingActive NO 2", self.logTag); + return; + } if (!self.screenLockCountdownDate) { // We became inactive, but never started a countdown. - DDLogVerbose(@"%@ tryToActivateScreenLockUponBecomingActive NO 1", self.logTag); + DDLogVerbose(@"%@ tryToActivateScreenLockUponBecomingActive NO 3", self.logTag); return; } NSTimeInterval countdownInterval = fabs([self.screenLockCountdownDate timeIntervalSinceNow]); @@ -149,99 +203,108 @@ NS_ASSUME_NONNULL_BEGIN NSTimeInterval screenLockTimeout = OWSScreenLock.sharedManager.screenLockTimeout; OWSAssert(screenLockTimeout >= 0); if (countdownInterval >= screenLockTimeout) { - self.isScreenLockUnlocked = NO; - DDLogVerbose(@"%@ tryToActivateScreenLockUponBecomingActive YES 1 (%0.3f >= %0.3f)", + self.isScreenLockLocked = YES; + + DDLogVerbose(@"%@ tryToActivateScreenLockUponBecomingActive YES 4 (%0.3f >= %0.3f)", self.logTag, countdownInterval, screenLockTimeout); } else { - DDLogVerbose(@"%@ tryToActivateScreenLockUponBecomingActive NO 2 (%0.3f < %0.3f)", + DDLogVerbose(@"%@ tryToActivateScreenLockUponBecomingActive NO 5 (%0.3f < %0.3f)", self.logTag, countdownInterval, screenLockTimeout); } } -- (void)setAppIsInactive:(BOOL)appIsInactive +// Setter for property indicating that the app is either +// inactive or in the background, e.g. not "foreground and active." +- (void)setAppIsInactiveOrBackground:(BOOL)appIsInactiveOrBackground { - _appIsInactive = appIsInactive; + OWSAssertIsOnMainThread(); - if (!appIsInactive) { - [self tryToActivateScreenLockUponBecomingActive]; + _appIsInactiveOrBackground = appIsInactiveOrBackground; + if (appIsInactiveOrBackground) { + if (!self.isShowingScreenLockUI) { + [self startScreenLockCountdownIfNecessary]; + } + } else { + [self tryToActivateScreenLockBasedOnCountdown]; + + DDLogInfo(@"%@ setAppIsInactiveOrBackground clear screenLockCountdownDate.", self.logTag); self.screenLockCountdownDate = nil; } - [self startInactiveTimerIfNecessary]; - - [self ensureScreenProtection]; + [self ensureUI]; } +// Setter for property indicating that the app is in the background. +// If true, by definition the app is not active. - (void)setAppIsInBackground:(BOOL)appIsInBackground { - if (appIsInBackground && !_appIsInBackground) { - [self startScreenLockCountdownIfNecessary]; - } + OWSAssertIsOnMainThread(); _appIsInBackground = appIsInBackground; - [self ensureScreenProtection]; + if (self.appIsInBackground) { + [self startScreenLockCountdownIfNecessary]; + } else { + [self tryToActivateScreenLockBasedOnCountdown]; + } + + [self ensureUI]; } - (void)startScreenLockCountdownIfNecessary { + DDLogVerbose(@"%@ startScreenLockCountdownIfNecessary: %d", self.logTag, self.screenLockCountdownDate != nil); + if (!self.screenLockCountdownDate) { - DDLogVerbose(@"%@ startScreenLockCountdownIfNecessary.", self.logTag); + DDLogInfo(@"%@ startScreenLockCountdown.", self.logTag); self.screenLockCountdownDate = [NSDate new]; } self.didLastUnlockAttemptFail = NO; - - [self clearInactiveTimer]; } -- (void)ensureScreenProtection +// Ensure that: +// +// * The blocking window has the correct state. +// * That we show the "iOS auth UI to unlock" if necessary. +- (void)ensureUI { OWSAssertIsOnMainThread(); if (!AppReadiness.isAppReady) { [AppReadiness runNowOrWhenAppIsReady:^{ - [self ensureScreenProtection]; + [self ensureUI]; }]; return; } - BOOL shouldHaveScreenLock = self.shouldHaveScreenLock; - BOOL shouldHaveScreenProtection = self.shouldHaveScreenProtection; + ScreenLockUIState desiredUIState = self.desiredUIState; - BOOL shouldShowBlockWindow = shouldHaveScreenProtection || shouldHaveScreenLock; - DDLogVerbose(@"%@, shouldHaveScreenProtection: %d, shouldHaveScreenLock: %d, shouldShowBlockWindow: %d", - self.logTag, - shouldHaveScreenProtection, - shouldHaveScreenLock, - shouldShowBlockWindow); - if (self.screenBlockingWindow.hidden != !shouldShowBlockWindow) { - DDLogInfo(@"%@, %@.", self.logTag, shouldShowBlockWindow ? @"showing block window" : @"hiding block window"); - } - [self updateScreenBlockingWindow:shouldShowBlockWindow shouldHaveScreenLock:shouldHaveScreenLock animated:YES]; + DDLogVerbose(@"%@, ensureUI: %@", self.logTag, NSStringForScreenLockUIState(desiredUIState)); + + [self updateScreenBlockingWindow:desiredUIState animated:YES]; - if (shouldHaveScreenLock && !self.didLastUnlockAttemptFail) { - [self tryToPresentScreenLockUI]; + // Show the "iOS auth UI to unlock" if necessary. + if (desiredUIState == ScreenLockUIStateScreenLock && !self.didLastUnlockAttemptFail) { + [self tryToPresentAuthUIToUnlockScreenLock]; } } -- (void)tryToPresentScreenLockUI +- (void)tryToPresentAuthUIToUnlockScreenLock { OWSAssertIsOnMainThread(); - // If we no longer want to present the screen lock UI, abort. - if (!self.shouldHaveScreenLock) { - return; - } - if (self.didLastUnlockAttemptFail) { + if (self.isShowingScreenLockUI) { + // We're already showing the auth UI; abort. return; } - if (self.isShowingScreenLockUI) { + if (self.appIsInactiveOrBackground) { + // Never show the auth UI unless active. return; } @@ -254,9 +317,9 @@ NS_ASSUME_NONNULL_BEGIN self.isShowingScreenLockUI = NO; - self.isScreenLockUnlocked = YES; + self.isScreenLockLocked = NO; - [self ensureScreenProtection]; + [self ensureUI]; } failure:^(NSError *error) { DDLogInfo(@"%@ unlock screen lock failed.", self.logTag); @@ -285,54 +348,37 @@ NS_ASSUME_NONNULL_BEGIN self.didLastUnlockAttemptFail = YES; // Re-show the unlock UI. - [self ensureScreenProtection]; + [self ensureUI]; }]; - [self ensureScreenProtection]; + [self ensureUI]; } -- (BOOL)shouldHaveScreenProtection +// Determines what the state of the app should be. +- (ScreenLockUIState)desiredUIState { - // Show 'Screen Protection' if: - // - // * App is inactive and... - // * 'Screen Protection' is enabled. - if (!self.appIsInactive) { - return NO; - } else if (!Environment.preferences.screenSecurityIsEnabled) { - return NO; - } else { - return YES; + if (self.isScreenLockLocked) { + if (self.appIsInactiveOrBackground) { + DDLogVerbose(@"%@ desiredUIState: screen protection 1.", self.logTag); + return ScreenLockUIStateScreenProtection; + } else { + DDLogVerbose(@"%@ desiredUIState: screen lock 2.", self.logTag); + return ScreenLockUIStateScreenLock; + } } -} -- (BOOL)shouldHaveScreenLock -{ - if (![TSAccountManager isRegistered]) { - // Don't show 'Screen Lock' if user is not registered. - DDLogVerbose(@"%@ shouldHaveScreenLock NO 1.", self.logTag); - return NO; - } else if (!OWSScreenLock.sharedManager.isScreenLockEnabled) { - // Don't show 'Screen Lock' if 'Screen Lock' isn't enabled. - DDLogVerbose(@"%@ shouldHaveScreenLock NO 2.", self.logTag); - return NO; - } else if (self.appIsInBackground) { - // Don't show 'Screen Lock' if app is in background. - DDLogVerbose(@"%@ shouldHaveScreenLock NO 4.", self.logTag); - return NO; - } else if (self.isShowingScreenLockUI) { - // Maintain blocking window in 'screen lock' mode while we're - // showing the 'Unlock Screen Lock' UI. - DDLogVerbose(@"%@ shouldHaveScreenLock YES 0.", self.logTag); - return YES; - } else if (self.appIsInactive) { - // Don't show 'Screen Lock' if app is inactive. - DDLogVerbose(@"%@ shouldHaveScreenLock NO 5.", self.logTag); - return NO; + if (!self.appIsInactiveOrBackground) { + // App is inactive or background. + DDLogVerbose(@"%@ desiredUIState: none 3.", self.logTag); + return ScreenLockUIStateNone; + } + + if (Environment.preferences.screenSecurityIsEnabled) { + DDLogVerbose(@"%@ desiredUIState: screen protection 4.", self.logTag); + return ScreenLockUIStateScreenProtection; } else { - BOOL shouldHaveScreenLock = !self.isScreenLockUnlocked; - DDLogVerbose(@"%@ shouldHaveScreenLock ? %d.", self.logTag, shouldHaveScreenLock); - return shouldHaveScreenLock; + DDLogVerbose(@"%@ desiredUIState: none 5.", self.logTag); + return ScreenLockUIStateNone; } } @@ -346,7 +392,7 @@ NS_ASSUME_NONNULL_BEGIN buttonTitle:nil buttonAction:^(UIAlertAction *action) { // After the alert, re-show the unlock UI. - [self ensureScreenProtection]; + [self ensureUI]; }]; } @@ -360,15 +406,14 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(rootWindow); UIWindow *window = [[UIWindow alloc] initWithFrame:rootWindow.bounds]; - window.hidden = YES; + window.hidden = NO; + window.windowLevel = UIWindowLevel_Background; window.opaque = YES; - window.windowLevel = CGFLOAT_MAX; window.backgroundColor = UIColor.ows_materialBlueColor; UIViewController *viewController = [UIViewController new]; viewController.view.backgroundColor = UIColor.ows_materialBlueColor; - UIView *rootView = viewController.view; UIView *edgesView = [UIView containerView]; @@ -413,7 +458,8 @@ NS_ASSUME_NONNULL_BEGIN self.screenBlockingImageView = imageView; self.screenBlockingButton = button; - [self updateScreenBlockingWindow:YES shouldHaveScreenLock:NO animated:NO]; + // Default to screen protection until we know otherwise. + [self updateScreenBlockingWindow:ScreenLockUIStateNone animated:NO]; } // The "screen blocking" window has three possible states: @@ -423,13 +469,48 @@ NS_ASSUME_NONNULL_BEGIN // * "Screen Lock, local auth UI presented". Move the Signal logo so that it is visible. // * "Screen Lock, local auth UI not presented". Move the Signal logo so that it is visible, // show "unlock" button. -- (void)updateScreenBlockingWindow:(BOOL)shouldShowBlockWindow - shouldHaveScreenLock:(BOOL)shouldHaveScreenLock - animated:(BOOL)animated +- (void)updateScreenBlockingWindow:(ScreenLockUIState)desiredUIState animated:(BOOL)animated { OWSAssertIsOnMainThread(); - self.screenBlockingWindow.hidden = !shouldShowBlockWindow; + BOOL shouldShowBlockWindow = desiredUIState != ScreenLockUIStateNone; + if (self.rootWindow.hidden != shouldShowBlockWindow) { + DDLogInfo(@"%@, %@.", self.logTag, shouldShowBlockWindow ? @"showing block window" : @"hiding block window"); + } + + // When we show the block window, try to capture the first responder of + // the root window before it is hidden. + // + // When we hide the root window, its first responder will resign. + if (shouldShowBlockWindow && !self.rootWindow.hidden) { + self.rootWindowResponder = [UIResponder currentFirstResponder]; + DDLogInfo(@"%@ trying to capture self.rootWindowResponder: %@", self.logTag, self.rootWindowResponder); + } + + // * Show/hide the app's root window as necessary. + // * Never hide the blocking window (that can lead to bad frames). + // Instead, manipulate its window level to move it in front of + // or behind the root window. + if (shouldShowBlockWindow) { + // Show the blocking window in front of the status bar. + self.screenBlockingWindow.windowLevel = UIWindowLevelStatusBar + 1; + self.rootWindow.hidden = YES; + } else { + self.screenBlockingWindow.windowLevel = UIWindowLevel_Background; + [self.rootWindow makeKeyAndVisible]; + + // When we hide the block window, try to restore the first + // responder of the root window. + // + // It's important we restore first responder status once the user completes + // In some cases, (RegistrationLock Reminder) it just puts the keyboard back where + // the user needs it, saving them a tap. + // But in the case of an inputAccessoryView, like the ConversationViewController, + // failing to restore firstResponder could hide the input toolbar. + DDLogInfo(@"%@ trying to restore self.rootWindowResponder: %@", self.logTag, self.rootWindowResponder); + [self.rootWindowResponder becomeFirstResponder]; + self.rootWindowResponder = nil; + } UIView *rootView = self.screenBlockingViewController.view; @@ -437,15 +518,7 @@ NS_ASSUME_NONNULL_BEGIN NSMutableArray *screenBlockingConstraints = [NSMutableArray new]; - BOOL shouldShowUnlockButton = (!self.appIsInactive && !self.appIsInBackground && self.didLastUnlockAttemptFail); - - DDLogVerbose(@"%@ updateScreenBlockingWindow. shouldShowBlockWindow: %d, shouldHaveScreenLock: %d, " - @"shouldShowUnlockButton: %d.", - self.logTag, - shouldShowBlockWindow, - shouldHaveScreenLock, - shouldShowUnlockButton); - + BOOL shouldHaveScreenLock = desiredUIState == ScreenLockUIStateScreenLock; NSString *signature = [NSString stringWithFormat:@"%d %d", shouldHaveScreenLock, self.isShowingScreenLockUI]; if ([NSObject isNullableObject:self.screenBlockingSignature equalTo:signature]) { // Skip redundant work to avoid interfering with ongoing animations. @@ -481,48 +554,35 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssertIsOnMainThread(); - DDLogInfo(@"showUnlockUI"); + if (self.appIsInactiveOrBackground) { + // This button can be pressed while the app is inactive + // for a brief window while the iOS auth UI is dismissing. + return; + } + + DDLogInfo(@"%@ unlockButtonTapped", self.logTag); self.didLastUnlockAttemptFail = NO; - [self ensureScreenProtection]; + [self ensureUI]; } #pragma mark - Events - (void)screenLockDidChange:(NSNotification *)notification { - [self ensureScreenProtection]; -} - -- (void)screenLockWasEnabled:(NSNotification *)notification -{ - // When we enable screen lock, consider that an unlock. - self.isScreenLockUnlocked = YES; - - DDLogVerbose(@"%@ screenLockWasEnabled", self.logTag); - - [self ensureScreenProtection]; -} - -- (void)registrationStateDidChange -{ - OWSAssertIsOnMainThread(); - - DDLogInfo(@"registrationStateDidChange"); - - [self ensureScreenProtection]; + [self ensureUI]; } - (void)clearAuthUIWhenActive { // For continuity, continue to present blocking screen in "screen lock" mode while // dismissing the "local auth UI". - if (self.appIsInactive) { + if (self.appIsInactiveOrBackground) { self.shouldClearAuthUIWhenActive = YES; } else { self.isShowingScreenLockUI = NO; - [self ensureScreenProtection]; + [self ensureUI]; } } @@ -533,12 +593,12 @@ NS_ASSUME_NONNULL_BEGIN self.isShowingScreenLockUI = NO; } - self.appIsInactive = NO; + self.appIsInactiveOrBackground = NO; } - (void)applicationWillResignActive:(NSNotification *)notification { - self.appIsInactive = YES; + self.appIsInactiveOrBackground = YES; } - (void)applicationWillEnterForeground:(NSNotification *)notification @@ -551,29 +611,27 @@ NS_ASSUME_NONNULL_BEGIN self.appIsInBackground = YES; } -#pragma mark - Inactive Timer - -- (void)inactiveTimerDidFire +// Whenever the device date/time is edited by the user, +// trigger screen lock immediately if enabled. +- (void)clockDidChange:(NSNotification *)notification { - [self startScreenLockCountdownIfNecessary]; -} + DDLogInfo(@"%@ clock did change", self.logTag); -- (void)startInactiveTimerIfNecessary -{ - if (self.appIsInactive && !self.isShowingScreenLockUI && !self.inactiveTimer) { - [self.inactiveTimer invalidate]; - self.inactiveTimer = [NSTimer weakScheduledTimerWithTimeInterval:45.f - target:self - selector:@selector(inactiveTimerDidFire) - userInfo:nil - repeats:NO]; + if (!AppReadiness.isAppReady) { + // It's not safe to access OWSScreenLock.isScreenLockEnabled + // until the app is ready. + // + // We don't need to try to lock the screen lock; + // It will be initialized by `setupWithRootWindow`. + DDLogVerbose(@"%@ clockDidChange 0", self.logTag); + return; } -} + self.isScreenLockLocked = OWSScreenLock.sharedManager.isScreenLockEnabled; -- (void)clearInactiveTimer -{ - [self.inactiveTimer invalidate]; - self.inactiveTimer = nil; + // NOTE: this notifications fires _before_ applicationDidBecomeActive, + // which is desirable. Don't assume that though; call ensureUI + // just in case it's necessary. + [self ensureUI]; } @end