Fix more edge cases in Screen Lock.

This commit is contained in:
Matthew Chen 2018-03-27 15:55:31 -04:00
parent 8bcf62258a
commit c85e5b39b5
3 changed files with 187 additions and 81 deletions

View File

@ -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)
}

View File

@ -4,22 +4,33 @@
#import "OWSScreenLockUI.h"
#import "Signal-Swift.h"
#import <SignalMessaging/SignalMessaging-Swift.h>
#import <SignalMessaging/UIView+OWS.h>
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 clearAuthUIWhenActive];
self.didLastUnlockAttemptFail = YES;
[self showScreenLockFailureAlertWithMessage:error.localizedDescription];
}
unexpectedFailure:^(NSError *error) {
DDLogInfo(@"%@ unlock screen lock unexpectedly failed.", self.logTag);
self.isShowingScreenLockUI = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self ensureScreenProtection];
});
}
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

View File

@ -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";