From c85e5b39b575574d52af5e3079f5274eda0cd612 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 27 Mar 2018 15:55:31 -0400 Subject: [PATCH 1/4] Fix more edge cases in Screen Lock. --- Signal/src/util/OWSScreenLock.swift | 21 +- Signal/src/util/OWSScreenLockUI.m | 235 +++++++++++++----- .../translations/en.lproj/Localizable.strings | 12 +- 3 files changed, 187 insertions(+), 81 deletions(-) diff --git a/Signal/src/util/OWSScreenLock.swift b/Signal/src/util/OWSScreenLock.swift index 9db55613b..fdb95fe51 100644 --- a/Signal/src/util/OWSScreenLock.swift +++ b/Signal/src/util/OWSScreenLock.swift @@ -11,7 +11,6 @@ import LocalAuthentication case success case cancel case failure(error:String) - case unexpectedFailure(error:String) } @objc public let screenLockTimeoutDefault = 15 * kMinuteInterval @@ -123,8 +122,6 @@ import LocalAuthentication 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) @@ -147,8 +144,6 @@ import LocalAuthentication 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) @@ -160,7 +155,6 @@ import LocalAuthentication @objc public func tryToUnlockScreenLock(success: @escaping (() -> Void), failure: @escaping ((Error) -> Void), - unexpectedFailure: @escaping ((Error) -> Void), cancel: @escaping (() -> Void)) { guard !ignoreUnlockUntilActive else { DispatchQueue.main.async { @@ -177,8 +171,6 @@ import LocalAuthentication switch outcome { case .failure(let error): failure(self.authenticationError(errorDescription: error)) - case .unexpectedFailure(let error): - unexpectedFailure(self.authenticationError(errorDescription: error)) case .success: success() case .cancel: @@ -223,7 +215,7 @@ import LocalAuthentication case .success: owsFail("\(self.logTag) local authentication unexpected success") completion(.failure(error:defaultErrorDescription)) - case .cancel, .failure, .unexpectedFailure: + case .cancel, .failure: completion(outcome) } return @@ -243,7 +235,7 @@ import LocalAuthentication case .success: owsFail("\(self.logTag) local authentication unexpected success") completion(.failure(error:defaultErrorDescription)) - case .cancel, .failure, .unexpectedFailure: + case .cancel, .failure: completion(outcome) } } @@ -304,10 +296,10 @@ import LocalAuthentication comment: "Indicates that Touch ID/Face ID/Phone Passcode is 'locked out' on this device due to authentication failures.")) case .invalidContext: owsFail("\(self.logTag) context not valid.") - return .unexpectedFailure(error:defaultErrorDescription) + return .failure(error:defaultErrorDescription) case .notInteractive: owsFail("\(self.logTag) context not interactive.") - return .unexpectedFailure(error:defaultErrorDescription) + return .failure(error:defaultErrorDescription) } } return .failure(error:defaultErrorDescription) @@ -323,7 +315,10 @@ import LocalAuthentication private func screenLockContext() -> LAContext { let context = LAContext() - context.touchIDAuthenticationAllowableReuseDuration = TimeInterval(screenLockTimeout()) + // If user has set any non-zero timeout, recycle biometric auth + // in the same period as our normal screen lock timeout. + context.touchIDAuthenticationAllowableReuseDuration = TimeInterval(min(10.0, screenLockTimeout())) + if #available(iOS 11.0, *) { assert(!context.interactionNotAllowed) } diff --git a/Signal/src/util/OWSScreenLockUI.m b/Signal/src/util/OWSScreenLockUI.m index 2f4fcf70b..7a64e344f 100644 --- a/Signal/src/util/OWSScreenLockUI.m +++ b/Signal/src/util/OWSScreenLockUI.m @@ -4,22 +4,33 @@ #import "OWSScreenLockUI.h" #import "Signal-Swift.h" +#import +#import NS_ASSUME_NONNULL_BEGIN @interface OWSScreenLockUI () +@property (nonatomic) UIWindow *screenBlockingWindow; +@property (nonatomic) UIViewController *screenBlockingViewController; + // Unlike UIApplication.applicationState, this state is // updated conservatively, e.g. the flag is cleared during // "will enter background." @property (nonatomic) BOOL appIsInactive; @property (nonatomic) BOOL appIsInBackground; -@property (nonatomic, nullable) NSDate *appEnteredBackgroundDate; -@property (nonatomic) UIWindow *screenBlockingWindow; -@property (nonatomic) BOOL hasUnlockedScreenLock; + @property (nonatomic) BOOL isShowingScreenLockUI; +@property (nonatomic) BOOL didLastUnlockAttemptFail; + +// We want to remain in "screen lock" mode while "local auth" +// UI is dismissing. +@property (nonatomic) BOOL shouldClearAuthUIWhenActive; @property (nonatomic, nullable) NSTimer *screenLockUITimer; + +@property (nonatomic, nullable) NSDate *appEnteredBackgroundDate; +@property (nonatomic, nullable) NSDate *appEnteredForegroundDate; @property (nonatomic, nullable) NSDate *lastUnlockAttemptDate; @property (nonatomic, nullable) NSDate *lastUnlockSuccessDate; @@ -112,11 +123,10 @@ NS_ASSUME_NONNULL_BEGIN { if (appIsInBackground) { if (!_appIsInBackground) { - // Whenever app enters background, clear this state. - self.hasUnlockedScreenLock = NO; - // Record the time when app entered background. self.appEnteredBackgroundDate = [NSDate new]; + + self.didLastUnlockAttemptFail = NO; } } @@ -148,42 +158,13 @@ NS_ASSUME_NONNULL_BEGIN if (self.screenBlockingWindow.hidden != !shouldShowBlockWindow) { DDLogInfo(@"%@, %@.", self.logTag, shouldShowBlockWindow ? @"showing block window" : @"hiding block window"); } - self.screenBlockingWindow.hidden = !shouldShowBlockWindow; + [self updateScreenBlockingWindow:shouldShowBlockWindow shouldHaveScreenLock:shouldHaveScreenLock]; [self.screenLockUITimer invalidate]; self.screenLockUITimer = nil; - if (shouldHaveScreenLock) { - // In pincode-only mode (e.g. device pincode is set - // but Touch ID/Face ID are not configured), the pincode - // unlock UI is fullscreen and the app becomes inactive. - // Hitting the home button will cancel the authentication - // UI but not send the app to the background. Therefore, - // to send the "locked" app to the background, you need - // to tap the home button twice. - // - // Therefore, if our last unlock attempt failed or was - // cancelled, wait a couple of second before re-presenting - // the "unlock screen lock UI" so that users have a chance - // to hit home button again. - BOOL shouldDelayScreenLockUI = YES; - if (!self.lastUnlockAttemptDate) { - shouldDelayScreenLockUI = NO; - } else if (self.lastUnlockAttemptDate && self.lastUnlockSuccessDate && - [self.lastUnlockSuccessDate isAfterDate:self.lastUnlockAttemptDate]) { - shouldDelayScreenLockUI = NO; - } - - if (shouldDelayScreenLockUI) { - DDLogVerbose(@"%@, Delaying Screen Lock UI.", self.logTag); - self.screenLockUITimer = [NSTimer weakScheduledTimerWithTimeInterval:1.25f - target:self - selector:@selector(tryToPresentScreenLockUI) - userInfo:nil - repeats:NO]; - } else { - [self tryToPresentScreenLockUI]; - } + if (shouldHaveScreenLock && !self.didLastUnlockAttemptFail) { + [self tryToPresentScreenLockUI]; } } @@ -198,6 +179,9 @@ NS_ASSUME_NONNULL_BEGIN if (!self.shouldHaveScreenLock) { return; } + if (self.didLastUnlockAttemptFail) { + return; + } if (self.isShowingScreenLockUI) { return; } @@ -210,27 +194,24 @@ NS_ASSUME_NONNULL_BEGIN [OWSScreenLock.sharedManager tryToUnlockScreenLockWithSuccess:^{ DDLogInfo(@"%@ unlock screen lock succeeded.", self.logTag); self.isShowingScreenLockUI = NO; - self.hasUnlockedScreenLock = YES; self.lastUnlockSuccessDate = [NSDate new]; [self ensureScreenProtection]; } failure:^(NSError *error) { DDLogInfo(@"%@ unlock screen lock failed.", self.logTag); - self.isShowingScreenLockUI = NO; - [self showScreenLockFailureAlertWithMessage:error.localizedDescription]; - } - unexpectedFailure:^(NSError *error) { - DDLogInfo(@"%@ unlock screen lock unexpectedly failed.", self.logTag); - self.isShowingScreenLockUI = NO; + [self clearAuthUIWhenActive]; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self ensureScreenProtection]; - }); + self.didLastUnlockAttemptFail = YES; + + [self showScreenLockFailureAlertWithMessage:error.localizedDescription]; } cancel:^{ DDLogInfo(@"%@ unlock screen lock cancelled.", self.logTag); - self.isShowingScreenLockUI = NO; + + [self clearAuthUIWhenActive]; + + self.didLastUnlockAttemptFail = YES; // Re-show the unlock UI. [self ensureScreenProtection]; @@ -243,26 +224,57 @@ NS_ASSUME_NONNULL_BEGIN // // * App is inactive and... // * 'Screen Protection' is enabled. - BOOL shouldHaveScreenProtection = (self.appIsInactive && Environment.preferences.screenSecurityIsEnabled); - return shouldHaveScreenProtection; + if (!self.appIsInactive) { + return NO; + } else if (!Environment.preferences.screenSecurityIsEnabled) { + return NO; + } else { + return YES; + } +} + +- (BOOL)hasUnlockedScreenLock +{ + if (!self.lastUnlockSuccessDate) { + return NO; + } else if (!self.appEnteredBackgroundDate) { + return YES; + } else { + return [self.lastUnlockSuccessDate isAfterDate:self.appEnteredBackgroundDate]; + } } - (BOOL)shouldHaveScreenLock { - BOOL shouldHaveScreenLock = NO; 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.hasUnlockedScreenLock) { // Don't show 'Screen Lock' if 'Screen Lock' has been unlocked. + DDLogVerbose(@"%@ shouldHaveScreenLock NO 3.", 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; } else if (!self.appEnteredBackgroundDate) { // Show 'Screen Lock' if app has just launched. - shouldHaveScreenLock = YES; + DDLogVerbose(@"%@ shouldHaveScreenLock YES 1.", self.logTag); + return YES; } else { OWSAssert(self.appEnteredBackgroundDate); @@ -272,13 +284,14 @@ NS_ASSUME_NONNULL_BEGIN OWSAssert(screenLockTimeout >= 0); if (screenLockInterval < screenLockTimeout) { // Don't show 'Screen Lock' if 'Screen Lock' timeout hasn't elapsed. - shouldHaveScreenLock = NO; + DDLogVerbose(@"%@ shouldHaveScreenLock NO 6.", self.logTag); + return NO; } else { // Otherwise, show 'Screen Lock'. - shouldHaveScreenLock = YES; + DDLogVerbose(@"%@ shouldHaveScreenLock YES 2.", self.logTag); + return YES; } } - return shouldHaveScreenLock; } - (void)showScreenLockFailureAlertWithMessage:(NSString *)message @@ -307,13 +320,102 @@ NS_ASSUME_NONNULL_BEGIN UIWindow *window = [[UIWindow alloc] initWithFrame:rootWindow.bounds]; window.hidden = YES; window.opaque = YES; - window.userInteractionEnabled = NO; window.windowLevel = CGFLOAT_MAX; window.backgroundColor = UIColor.ows_materialBlueColor; - window.rootViewController = - [[UIStoryboard storyboardWithName:@"Launch Screen" bundle:nil] instantiateInitialViewController]; + + UIViewController *viewController = [UIViewController new]; + viewController.view.backgroundColor = UIColor.ows_materialBlueColor; + + window.rootViewController = viewController; self.screenBlockingWindow = window; + self.screenBlockingViewController = viewController; + + [self updateScreenBlockingWindow:YES shouldHaveScreenLock:NO]; +} + +- (void)updateScreenBlockingWindow:(BOOL)shouldShowBlockWindow shouldHaveScreenLock:(BOOL)shouldHaveScreenLock +{ + OWSAssertIsOnMainThread(); + + self.screenBlockingWindow.hidden = !shouldShowBlockWindow; + + UIView *rootView = self.screenBlockingViewController.view; + for (UIView *subview in rootView.subviews) { + [subview removeFromSuperview]; + } + + UIView *edgesView = [UIView containerView]; + [rootView addSubview:edgesView]; + [edgesView autoHCenterInSuperview]; + [edgesView autoPinEdgeToSuperviewEdge:ALEdgeTop]; + [edgesView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + + UIView *containerView = [UIView containerView]; + [edgesView addSubview:containerView]; + [containerView autoVCenterInSuperview]; + [containerView autoPinWidthToSuperviewWithMargin:20.f]; + + UIImage *image = [UIImage imageNamed:@"logoSignal"]; + UIImageView *imageView = [UIImageView new]; + imageView.image = image; + [containerView addSubview:imageView]; + [imageView autoPinTopToSuperview]; + [imageView autoHCenterInSuperview]; + + const CGSize screenSize = UIScreen.mainScreen.bounds.size; + const CGFloat shortScreenDimension = MIN(screenSize.width, screenSize.height); + const CGFloat imageSize = round(shortScreenDimension / 3.f); + [imageView autoSetDimension:ALDimensionWidth toSize:imageSize]; + [imageView autoSetDimension:ALDimensionHeight toSize:imageSize]; + + BOOL shouldShowUnlockButton = (!self.appIsInactive && !self.appIsInBackground && self.didLastUnlockAttemptFail); + + DDLogVerbose(@"%@ updateScreenBlockingWindow. shouldShowBlockWindow: %d, shouldHaveScreenLock: %d, " + @"shouldShowUnlockButton: %d.", + self.logTag, + shouldShowBlockWindow, + shouldHaveScreenLock, + shouldShowUnlockButton); + + if (shouldHaveScreenLock) { + const CGFloat kButtonHeight = 40.f; + OWSFlatButton *button = + [OWSFlatButton buttonWithTitle:NSLocalizedString(@"SCREEN_LOCK_UNLOCK_SIGNAL", + @"Label for button on lock screen that lets users unlock Signal.") + font:[OWSFlatButton fontForHeight:kButtonHeight] + titleColor:[UIColor ows_materialBlueColor] + backgroundColor:[UIColor whiteColor] + target:self + selector:@selector(showUnlockUI)]; + [containerView addSubview:button]; + [button autoSetDimension:ALDimensionHeight toSize:kButtonHeight]; + const CGFloat kVSpacing = 80.f; + [button autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:imageView withOffset:kVSpacing]; + // For symmetry, use equal padding below so that "unlock" button is visually centered + // (under the local auth UI alert) and the Signal logo is moved upwards, not blocked by + // the local auth UI alert. + const CGFloat kBottomPadding = imageSize + kVSpacing; + [button autoPinBottomToSuperviewWithMargin:kBottomPadding]; + [button autoPinLeadingAndTrailingToSuperview]; + + button.hidden = !shouldShowUnlockButton; + } else { + [imageView autoPinBottomToSuperview]; + } + + [rootView layoutIfNeeded]; +} + +- (void)showUnlockUI +{ + OWSAssertIsOnMainThread(); + + DDLogInfo(@"showUnlockUI"); + + self.didLastUnlockAttemptFail = NO; + + [self ensureScreenProtection]; } #pragma mark - Events @@ -332,9 +434,23 @@ NS_ASSUME_NONNULL_BEGIN [self ensureScreenProtection]; } +- (void)clearAuthUIWhenActive +{ + if (self.appIsInactive) { + self.shouldClearAuthUIWhenActive = YES; + } else { + self.isShowingScreenLockUI = NO; + } +} + - (void)applicationDidBecomeActive:(NSNotification *)notification { self.appIsInactive = NO; + + if (self.shouldClearAuthUIWhenActive) { + self.shouldClearAuthUIWhenActive = NO; + self.isShowingScreenLockUI = NO; + } } - (void)applicationWillResignActive:(NSNotification *)notification @@ -353,6 +469,7 @@ NS_ASSUME_NONNULL_BEGIN self.lastUnlockSuccessDate = nil; self.appIsInBackground = NO; + self.appEnteredForegroundDate = [NSDate new]; } - (void)applicationDidEnterBackground:(NSNotification *)notification diff --git a/Signal/translations/en.lproj/Localizable.strings b/Signal/translations/en.lproj/Localizable.strings index 69dfde5cb..5c5fcc419 100644 --- a/Signal/translations/en.lproj/Localizable.strings +++ b/Signal/translations/en.lproj/Localizable.strings @@ -1585,6 +1585,9 @@ /* Title for alert indicating that screen lock could not be unlocked. */ "SCREEN_LOCK_UNLOCK_FAILED" = "Authentication Failed"; +/* Label for button on lock screen that lets users unlock Signal. */ +"SCREEN_LOCK_UNLOCK_SIGNAL" = "Unlock Signal"; + /* No comment provided by engineer. */ "SEARCH_BYNAMEORNUMBER_PLACEHOLDER_TEXT" = "Search by name or number"; @@ -1786,15 +1789,6 @@ /* An explanation of the 'read receipts' setting. */ "SETTINGS_READ_RECEIPTS_SECTION_FOOTER" = "See and share when messages have been read. This setting is optional and applies to all conversations."; -/* Remove metadata table cell label */ -"SETTINGS_REMOVE_METADATA" = "Remove Media Metadata"; - -/* Remove metadata section footer */ -"SETTINGS_REMOVE_METADATA_DETAIL" = "Remove user-identifying metadata and GPS information when sending image and video messages."; - -/* Remove metadata section header */ -"SETTINGS_REMOVE_METADATA_TITLE" = "Metadata"; - /* Label for the 'screen lock activity timeout' setting of the privacy settings. */ "SETTINGS_SCREEN_LOCK_ACTIVITY_TIMEOUT" = "Screen Lock Timeout"; From 16af07842509f86cff045bf50e7fdeb9c575b7f1 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 27 Mar 2018 16:10:20 -0400 Subject: [PATCH 2/4] Fix more edge cases in Screen Lock. --- Signal/src/util/OWSScreenLockUI.m | 41 +++++++++++++------------------ 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/Signal/src/util/OWSScreenLockUI.m b/Signal/src/util/OWSScreenLockUI.m index 7a64e344f..efb94fc4b 100644 --- a/Signal/src/util/OWSScreenLockUI.m +++ b/Signal/src/util/OWSScreenLockUI.m @@ -345,23 +345,9 @@ NS_ASSUME_NONNULL_BEGIN [subview removeFromSuperview]; } - UIView *edgesView = [UIView containerView]; - [rootView addSubview:edgesView]; - [edgesView autoHCenterInSuperview]; - [edgesView autoPinEdgeToSuperviewEdge:ALEdgeTop]; - [edgesView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; - - UIView *containerView = [UIView containerView]; - [edgesView addSubview:containerView]; - [containerView autoVCenterInSuperview]; - [containerView autoPinWidthToSuperviewWithMargin:20.f]; - UIImage *image = [UIImage imageNamed:@"logoSignal"]; UIImageView *imageView = [UIImageView new]; imageView.image = image; - [containerView addSubview:imageView]; - [imageView autoPinTopToSuperview]; - [imageView autoHCenterInSuperview]; const CGSize screenSize = UIScreen.mainScreen.bounds.size; const CGFloat shortScreenDimension = MIN(screenSize.width, screenSize.height); @@ -379,6 +365,11 @@ NS_ASSUME_NONNULL_BEGIN shouldShowUnlockButton); if (shouldHaveScreenLock) { + const CGFloat kVMargin = 50.f; + [rootView addSubview:imageView]; + [imageView autoHCenterInSuperview]; + [imageView autoPinTopToSuperviewWithMargin:kVMargin]; + const CGFloat kButtonHeight = 40.f; OWSFlatButton *button = [OWSFlatButton buttonWithTitle:NSLocalizedString(@"SCREEN_LOCK_UNLOCK_SIGNAL", @@ -388,20 +379,22 @@ NS_ASSUME_NONNULL_BEGIN backgroundColor:[UIColor whiteColor] target:self selector:@selector(showUnlockUI)]; - [containerView addSubview:button]; + [rootView addSubview:button]; + [button autoVCenterInSuperview]; [button autoSetDimension:ALDimensionHeight toSize:kButtonHeight]; - const CGFloat kVSpacing = 80.f; - [button autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:imageView withOffset:kVSpacing]; - // For symmetry, use equal padding below so that "unlock" button is visually centered - // (under the local auth UI alert) and the Signal logo is moved upwards, not blocked by - // the local auth UI alert. - const CGFloat kBottomPadding = imageSize + kVSpacing; - [button autoPinBottomToSuperviewWithMargin:kBottomPadding]; - [button autoPinLeadingAndTrailingToSuperview]; + [button autoPinLeadingToSuperviewWithMargin:50.f]; + [button autoPinTrailingToSuperviewWithMargin:50.f]; button.hidden = !shouldShowUnlockButton; } else { - [imageView autoPinBottomToSuperview]; + UIView *edgesView = [UIView containerView]; + [rootView addSubview:edgesView]; + [edgesView autoPinEdgeToSuperviewEdge:ALEdgeTop]; + [edgesView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + [edgesView autoPinWidthToSuperview]; + + [edgesView addSubview:imageView]; + [imageView autoCenterInSuperview]; } [rootView layoutIfNeeded]; From 930d89242d9e838b157447737063a110426d52bd Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 27 Mar 2018 16:16:35 -0400 Subject: [PATCH 3/4] Clean up ahead of PR. --- Signal/src/util/OWSScreenLockUI.m | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/Signal/src/util/OWSScreenLockUI.m b/Signal/src/util/OWSScreenLockUI.m index efb94fc4b..eec61e972 100644 --- a/Signal/src/util/OWSScreenLockUI.m +++ b/Signal/src/util/OWSScreenLockUI.m @@ -27,8 +27,6 @@ NS_ASSUME_NONNULL_BEGIN // UI is dismissing. @property (nonatomic) BOOL shouldClearAuthUIWhenActive; -@property (nonatomic, nullable) NSTimer *screenLockUITimer; - @property (nonatomic, nullable) NSDate *appEnteredBackgroundDate; @property (nonatomic, nullable) NSDate *appEnteredForegroundDate; @property (nonatomic, nullable) NSDate *lastUnlockAttemptDate; @@ -160,9 +158,6 @@ NS_ASSUME_NONNULL_BEGIN } [self updateScreenBlockingWindow:shouldShowBlockWindow shouldHaveScreenLock:shouldHaveScreenLock]; - [self.screenLockUITimer invalidate]; - self.screenLockUITimer = nil; - if (shouldHaveScreenLock && !self.didLastUnlockAttemptFail) { [self tryToPresentScreenLockUI]; } @@ -172,9 +167,6 @@ NS_ASSUME_NONNULL_BEGIN { OWSAssertIsOnMainThread(); - [self.screenLockUITimer invalidate]; - self.screenLockUITimer = nil; - // If we no longer want to present the screen lock UI, abort. if (!self.shouldHaveScreenLock) { return; @@ -334,6 +326,13 @@ NS_ASSUME_NONNULL_BEGIN [self updateScreenBlockingWindow:YES shouldHaveScreenLock:NO]; } +// The "screen blocking" window has three possible states: +// +// * "Just a logo". Used when app is launching and in app switcher. Must match the "Launch Screen" +// storyboard pixel-for-pixel. +// * "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 { OWSAssertIsOnMainThread(); @@ -429,6 +428,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)clearAuthUIWhenActive { + // For continuity, continue to present blocking screen in "screen lock" mode while + // dismissing the "local auth UI". if (self.appIsInactive) { self.shouldClearAuthUIWhenActive = YES; } else { @@ -456,8 +457,6 @@ NS_ASSUME_NONNULL_BEGIN // Clear the "delay Screen Lock UI" state; we don't want any // delays when presenting the "unlock screen lock UI" after // returning from background. - [self.screenLockUITimer invalidate]; - self.screenLockUITimer = nil; self.lastUnlockAttemptDate = nil; self.lastUnlockSuccessDate = nil; From 72b602c3d8cc91a6789c2e7c3d5542e7dd036d72 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Tue, 27 Mar 2018 17:46:05 -0400 Subject: [PATCH 4/4] Respond to CR. --- Signal/src/util/OWSScreenLock.swift | 3 +- Signal/src/util/OWSScreenLockUI.m | 129 ++++++++++++++++++---------- 2 files changed, 84 insertions(+), 48 deletions(-) diff --git a/Signal/src/util/OWSScreenLock.swift b/Signal/src/util/OWSScreenLock.swift index fdb95fe51..d8ae13f3b 100644 --- a/Signal/src/util/OWSScreenLock.swift +++ b/Signal/src/util/OWSScreenLock.swift @@ -316,7 +316,8 @@ import LocalAuthentication let context = LAContext() // If user has set any non-zero timeout, recycle biometric auth - // in the same period as our normal screen lock timeout. + // in the same period as our normal screen lock timeout, up to + // max of 10 seconds. context.touchIDAuthenticationAllowableReuseDuration = TimeInterval(min(10.0, screenLockTimeout())) if #available(iOS 11.0, *) { diff --git a/Signal/src/util/OWSScreenLockUI.m b/Signal/src/util/OWSScreenLockUI.m index eec61e972..41e24c563 100644 --- a/Signal/src/util/OWSScreenLockUI.m +++ b/Signal/src/util/OWSScreenLockUI.m @@ -13,6 +13,10 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) UIWindow *screenBlockingWindow; @property (nonatomic) UIViewController *screenBlockingViewController; +@property (nonatomic) UIView *screenBlockingImageView; +@property (nonatomic) UIView *screenBlockingButton; +@property (nonatomic) NSArray *screenBlockingConstraints; +@property (nonatomic) NSString *screenBlockingSignature; // Unlike UIApplication.applicationState, this state is // updated conservatively, e.g. the flag is cleared during @@ -156,7 +160,7 @@ NS_ASSUME_NONNULL_BEGIN if (self.screenBlockingWindow.hidden != !shouldShowBlockWindow) { DDLogInfo(@"%@, %@.", self.logTag, shouldShowBlockWindow ? @"showing block window" : @"hiding block window"); } - [self updateScreenBlockingWindow:shouldShowBlockWindow shouldHaveScreenLock:shouldHaveScreenLock]; + [self updateScreenBlockingWindow:shouldShowBlockWindow shouldHaveScreenLock:shouldHaveScreenLock animated:YES]; if (shouldHaveScreenLock && !self.didLastUnlockAttemptFail) { [self tryToPresentScreenLockUI]; @@ -208,6 +212,8 @@ NS_ASSUME_NONNULL_BEGIN // Re-show the unlock UI. [self ensureScreenProtection]; }]; + + [self ensureScreenProtection]; } - (BOOL)shouldHaveScreenProtection @@ -318,12 +324,52 @@ NS_ASSUME_NONNULL_BEGIN UIViewController *viewController = [UIViewController new]; viewController.view.backgroundColor = UIColor.ows_materialBlueColor; + + UIView *rootView = viewController.view; + + UIView *edgesView = [UIView containerView]; + [rootView addSubview:edgesView]; + [edgesView autoPinEdgeToSuperviewEdge:ALEdgeTop]; + [edgesView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; + [edgesView autoPinWidthToSuperview]; + + UIImage *image = [UIImage imageNamed:@"logoSignal"]; + UIImageView *imageView = [UIImageView new]; + imageView.image = image; + [edgesView addSubview:imageView]; + [imageView autoHCenterInSuperview]; + + const CGSize screenSize = UIScreen.mainScreen.bounds.size; + const CGFloat shortScreenDimension = MIN(screenSize.width, screenSize.height); + const CGFloat imageSize = round(shortScreenDimension / 3.f); + [imageView autoSetDimension:ALDimensionWidth toSize:imageSize]; + [imageView autoSetDimension:ALDimensionHeight toSize:imageSize]; + + const CGFloat kButtonHeight = 40.f; + OWSFlatButton *button = + [OWSFlatButton buttonWithTitle:NSLocalizedString(@"SCREEN_LOCK_UNLOCK_SIGNAL", + @"Label for button on lock screen that lets users unlock Signal.") + font:[OWSFlatButton fontForHeight:kButtonHeight] + titleColor:[UIColor ows_materialBlueColor] + backgroundColor:[UIColor whiteColor] + target:self + selector:@selector(showUnlockUI)]; + [edgesView addSubview:button]; + + [button autoSetDimension:ALDimensionHeight toSize:kButtonHeight]; + [button autoPinLeadingToSuperviewWithMargin:50.f]; + [button autoPinTrailingToSuperviewWithMargin:50.f]; + const CGFloat kVMargin = 65.f; + [button autoPinBottomToSuperviewWithMargin:kVMargin]; + window.rootViewController = viewController; self.screenBlockingWindow = window; self.screenBlockingViewController = viewController; + self.screenBlockingImageView = imageView; + self.screenBlockingButton = button; - [self updateScreenBlockingWindow:YES shouldHaveScreenLock:NO]; + [self updateScreenBlockingWindow:YES shouldHaveScreenLock:NO animated:NO]; } // The "screen blocking" window has three possible states: @@ -333,26 +379,19 @@ 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 +- (void)updateScreenBlockingWindow:(BOOL)shouldShowBlockWindow + shouldHaveScreenLock:(BOOL)shouldHaveScreenLock + animated:(BOOL)animated { OWSAssertIsOnMainThread(); self.screenBlockingWindow.hidden = !shouldShowBlockWindow; UIView *rootView = self.screenBlockingViewController.view; - for (UIView *subview in rootView.subviews) { - [subview removeFromSuperview]; - } - UIImage *image = [UIImage imageNamed:@"logoSignal"]; - UIImageView *imageView = [UIImageView new]; - imageView.image = image; + [NSLayoutConstraint deactivateConstraints:self.screenBlockingConstraints]; - const CGSize screenSize = UIScreen.mainScreen.bounds.size; - const CGFloat shortScreenDimension = MIN(screenSize.width, screenSize.height); - const CGFloat imageSize = round(shortScreenDimension / 3.f); - [imageView autoSetDimension:ALDimensionWidth toSize:imageSize]; - [imageView autoSetDimension:ALDimensionHeight toSize:imageSize]; + NSMutableArray *screenBlockingConstraints = [NSMutableArray new]; BOOL shouldShowUnlockButton = (!self.appIsInactive && !self.appIsInBackground && self.didLastUnlockAttemptFail); @@ -363,40 +402,35 @@ NS_ASSUME_NONNULL_BEGIN shouldHaveScreenLock, shouldShowUnlockButton); - if (shouldHaveScreenLock) { - const CGFloat kVMargin = 50.f; - [rootView addSubview:imageView]; - [imageView autoHCenterInSuperview]; - [imageView autoPinTopToSuperviewWithMargin:kVMargin]; - - const CGFloat kButtonHeight = 40.f; - OWSFlatButton *button = - [OWSFlatButton buttonWithTitle:NSLocalizedString(@"SCREEN_LOCK_UNLOCK_SIGNAL", - @"Label for button on lock screen that lets users unlock Signal.") - font:[OWSFlatButton fontForHeight:kButtonHeight] - titleColor:[UIColor ows_materialBlueColor] - backgroundColor:[UIColor whiteColor] - target:self - selector:@selector(showUnlockUI)]; - [rootView addSubview:button]; - [button autoVCenterInSuperview]; - [button autoSetDimension:ALDimensionHeight toSize:kButtonHeight]; - [button autoPinLeadingToSuperviewWithMargin:50.f]; - [button autoPinTrailingToSuperviewWithMargin:50.f]; - - button.hidden = !shouldShowUnlockButton; + 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. + return; + } + + self.screenBlockingButton.hidden = !shouldHaveScreenLock; + + if (self.isShowingScreenLockUI) { + const CGFloat kVMargin = 60.f; + [screenBlockingConstraints addObject:[self.screenBlockingImageView autoPinEdge:ALEdgeTop + toEdge:ALEdgeTop + ofView:rootView + withOffset:kVMargin]]; } else { - UIView *edgesView = [UIView containerView]; - [rootView addSubview:edgesView]; - [edgesView autoPinEdgeToSuperviewEdge:ALEdgeTop]; - [edgesView autoPinEdgeToSuperviewEdge:ALEdgeBottom]; - [edgesView autoPinWidthToSuperview]; - - [edgesView addSubview:imageView]; - [imageView autoCenterInSuperview]; + [screenBlockingConstraints addObject:[self.screenBlockingImageView autoVCenterInSuperview]]; } - [rootView layoutIfNeeded]; + self.screenBlockingConstraints = screenBlockingConstraints; + self.screenBlockingSignature = signature; + + if (animated) { + [UIView animateWithDuration:0.35f + animations:^{ + [rootView layoutIfNeeded]; + }]; + } else { + [rootView layoutIfNeeded]; + } } - (void)showUnlockUI @@ -434,17 +468,18 @@ NS_ASSUME_NONNULL_BEGIN self.shouldClearAuthUIWhenActive = YES; } else { self.isShowingScreenLockUI = NO; + [self ensureScreenProtection]; } } - (void)applicationDidBecomeActive:(NSNotification *)notification { - self.appIsInactive = NO; - if (self.shouldClearAuthUIWhenActive) { self.shouldClearAuthUIWhenActive = NO; self.isShowingScreenLockUI = NO; } + + self.appIsInactive = NO; } - (void)applicationWillResignActive:(NSNotification *)notification