session-ios/Signal/src/util/OWSScreenLockUI.m

554 lines
19 KiB
Mathematica
Raw Normal View History

2018-03-21 20:46:23 +01:00
//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
#import "OWSScreenLockUI.h"
#import "Signal-Swift.h"
2018-03-27 21:55:31 +02:00
#import <SignalMessaging/SignalMessaging-Swift.h>
#import <SignalMessaging/UIView+OWS.h>
2018-03-21 20:46:23 +01:00
NS_ASSUME_NONNULL_BEGIN
@interface OWSScreenLockUI ()
2018-03-27 21:55:31 +02:00
@property (nonatomic) UIWindow *screenBlockingWindow;
@property (nonatomic) UIViewController *screenBlockingViewController;
2018-03-27 23:46:05 +02:00
@property (nonatomic) UIView *screenBlockingImageView;
@property (nonatomic) UIView *screenBlockingButton;
@property (nonatomic) NSArray<NSLayoutConstraint *> *screenBlockingConstraints;
@property (nonatomic) NSString *screenBlockingSignature;
2018-03-27 21:55:31 +02:00
2018-03-21 20:46:23 +01:00
// Unlike UIApplication.applicationState, this state is
// updated conservatively, e.g. the flag is cleared during
// "will enter background."
@property (nonatomic) BOOL appIsInactive;
2018-03-22 23:38:31 +01:00
@property (nonatomic) BOOL appIsInBackground;
2018-03-27 21:55:31 +02:00
2018-03-21 20:46:23 +01:00
@property (nonatomic) BOOL isShowingScreenLockUI;
2018-03-27 21:55:31 +02:00
@property (nonatomic) BOOL didLastUnlockAttemptFail;
// We want to remain in "screen lock" mode while "local auth"
// UI is dismissing.
@property (nonatomic) BOOL shouldClearAuthUIWhenActive;
2018-03-21 20:46:23 +01:00
2018-03-27 21:55:31 +02:00
@property (nonatomic, nullable) NSDate *appEnteredBackgroundDate;
@property (nonatomic, nullable) NSDate *appEnteredForegroundDate;
@property (nonatomic, nullable) NSDate *lastUnlockAttemptDate;
@property (nonatomic, nullable) NSDate *lastUnlockSuccessDate;
@property (nonatomic, nullable) NSTimer *inactiveTimer;
2018-03-21 20:46:23 +01:00
@end
#pragma mark -
@implementation OWSScreenLockUI
+ (instancetype)sharedManager
{
static OWSScreenLockUI *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] initDefault];
});
return instance;
}
- (instancetype)initDefault
{
self = [super init];
if (!self) {
return self;
}
[self observeNotifications];
OWSSingletonAssert();
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)observeNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidBecomeActive:)
name:OWSApplicationDidBecomeActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillResignActive:)
name:OWSApplicationWillResignActiveNotification
object:nil];
2018-03-22 23:38:31 +01:00
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillEnterForeground:)
name:OWSApplicationWillEnterForegroundNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationDidEnterBackground:)
name:OWSApplicationDidEnterBackgroundNotification
object:nil];
2018-03-21 20:46:23 +01:00
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(registrationStateDidChange)
name:RegistrationStateDidChangeNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(screenLockDidChange:)
name:OWSScreenLock.ScreenLockDidChange
object:nil];
}
- (void)setupWithRootWindow:(UIWindow *)rootWindow
{
OWSAssertIsOnMainThread();
OWSAssert(rootWindow);
[self prepareScreenProtectionWithRootWindow:rootWindow];
[AppReadiness runNowOrWhenAppIsReady:^{
[self ensureScreenProtection];
}];
}
#pragma mark - Methods
- (void)setAppIsInactive:(BOOL)appIsInactive
{
BOOL didChange = _appIsInactive != appIsInactive;
2018-03-21 20:46:23 +01:00
_appIsInactive = appIsInactive;
if (didChange) {
// If app is inactive for more than N seconds,
// treat this as "entering the background" for the purposes
// of Screen Lock.
if (!appIsInactive) {
[self.inactiveTimer invalidate];
self.inactiveTimer = nil;
} else if (!self.isShowingScreenLockUI) {
[self.inactiveTimer invalidate];
self.inactiveTimer = [NSTimer weakScheduledTimerWithTimeInterval:45.f
target:self
selector:@selector(inactiveTimerDidFire)
userInfo:nil
repeats:NO];
}
}
2018-03-21 20:46:23 +01:00
[self ensureScreenProtection];
}
2018-03-22 23:38:31 +01:00
- (void)setAppIsInBackground:(BOOL)appIsInBackground
{
2018-03-26 20:52:07 +02:00
if (appIsInBackground) {
if (!_appIsInBackground) {
[self markAppAsInBackground];
2018-03-26 20:52:07 +02:00
}
}
2018-03-22 23:38:31 +01:00
_appIsInBackground = appIsInBackground;
[self ensureScreenProtection];
}
- (void)markAppAsInBackground
{
// Record the time when app entered background.
self.appEnteredBackgroundDate = [NSDate new];
self.didLastUnlockAttemptFail = NO;
[self.inactiveTimer invalidate];
self.inactiveTimer = nil;
}
2018-03-21 20:46:23 +01:00
- (void)ensureScreenProtection
{
OWSAssertIsOnMainThread();
if (!AppReadiness.isAppReady) {
[AppReadiness runNowOrWhenAppIsReady:^{
[self ensureScreenProtection];
}];
return;
}
BOOL shouldHaveScreenLock = self.shouldHaveScreenLock;
BOOL shouldHaveScreenProtection = self.shouldHaveScreenProtection;
BOOL shouldShowBlockWindow = shouldHaveScreenProtection || shouldHaveScreenLock;
DDLogVerbose(@"%@, shouldHaveScreenProtection: %d, shouldHaveScreenLock: %d, shouldShowBlockWindow: %d",
self.logTag,
shouldHaveScreenProtection,
shouldHaveScreenLock,
shouldShowBlockWindow);
2018-03-27 19:26:44 +02:00
if (self.screenBlockingWindow.hidden != !shouldShowBlockWindow) {
DDLogInfo(@"%@, %@.", self.logTag, shouldShowBlockWindow ? @"showing block window" : @"hiding block window");
}
2018-03-27 23:46:05 +02:00
[self updateScreenBlockingWindow:shouldShowBlockWindow shouldHaveScreenLock:shouldHaveScreenLock animated:YES];
2018-03-27 21:55:31 +02:00
if (shouldHaveScreenLock && !self.didLastUnlockAttemptFail) {
[self tryToPresentScreenLockUI];
}
}
- (void)tryToPresentScreenLockUI
{
OWSAssertIsOnMainThread();
// If we no longer want to present the screen lock UI, abort.
if (!self.shouldHaveScreenLock) {
return;
}
2018-03-27 21:55:31 +02:00
if (self.didLastUnlockAttemptFail) {
return;
}
if (self.isShowingScreenLockUI) {
return;
}
DDLogInfo(@"%@, try to unlock screen lock", self.logTag);
self.isShowingScreenLockUI = YES;
self.lastUnlockAttemptDate = [NSDate new];
[OWSScreenLock.sharedManager tryToUnlockScreenLockWithSuccess:^{
DDLogInfo(@"%@ unlock screen lock succeeded.", self.logTag);
self.isShowingScreenLockUI = NO;
self.lastUnlockSuccessDate = [NSDate new];
[self ensureScreenProtection];
}
failure:^(NSError *error) {
DDLogInfo(@"%@ unlock screen lock failed.", self.logTag);
2018-03-27 21:55:31 +02:00
[self clearAuthUIWhenActive];
2018-03-27 21:55:31 +02:00
self.didLastUnlockAttemptFail = YES;
[self showScreenLockFailureAlertWithMessage:error.localizedDescription];
}
unexpectedFailure:^(NSError *error) {
DDLogInfo(@"%@ unlock screen lock unexpectedly failed.", self.logTag);
// Local Authentication isn't working properly.
// This isn't covered by the docs or the forums but in practice
// it appears to be effective to retry again after waiting a bit.
dispatch_async(dispatch_get_main_queue(), ^{
[self clearAuthUIWhenActive];
});
}
cancel:^{
DDLogInfo(@"%@ unlock screen lock cancelled.", self.logTag);
2018-03-27 21:55:31 +02:00
[self clearAuthUIWhenActive];
self.didLastUnlockAttemptFail = YES;
// Re-show the unlock UI.
[self ensureScreenProtection];
}];
2018-03-27 23:46:05 +02:00
[self ensureScreenProtection];
}
- (BOOL)shouldHaveScreenProtection
{
// Show 'Screen Protection' if:
//
// * App is inactive and...
// * 'Screen Protection' is enabled.
2018-03-27 21:55:31 +02:00
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
{
2018-03-22 22:28:05 +01:00
if (![TSAccountManager isRegistered]) {
2018-03-21 20:46:23 +01:00
// Don't show 'Screen Lock' if user is not registered.
2018-03-27 21:55:31 +02:00
DDLogVerbose(@"%@ shouldHaveScreenLock NO 1.", self.logTag);
return NO;
2018-03-21 20:46:23 +01:00
} else if (!OWSScreenLock.sharedManager.isScreenLockEnabled) {
// Don't show 'Screen Lock' if 'Screen Lock' isn't enabled.
2018-03-27 21:55:31 +02:00
DDLogVerbose(@"%@ shouldHaveScreenLock NO 2.", self.logTag);
return NO;
2018-03-21 20:46:23 +01:00
} else if (self.hasUnlockedScreenLock) {
// Don't show 'Screen Lock' if 'Screen Lock' has been unlocked.
2018-03-27 21:55:31 +02:00
DDLogVerbose(@"%@ shouldHaveScreenLock NO 3.", self.logTag);
return NO;
2018-03-22 23:38:31 +01:00
} else if (self.appIsInBackground) {
// Don't show 'Screen Lock' if app is in background.
2018-03-27 21:55:31 +02:00
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;
2018-03-22 23:38:31 +01:00
} else if (self.appIsInactive) {
// Don't show 'Screen Lock' if app is inactive.
2018-03-27 21:55:31 +02:00
DDLogVerbose(@"%@ shouldHaveScreenLock NO 5.", self.logTag);
return NO;
2018-03-26 20:52:07 +02:00
} else if (!self.appEnteredBackgroundDate) {
2018-03-22 23:38:31 +01:00
// Show 'Screen Lock' if app has just launched.
2018-03-27 21:55:31 +02:00
DDLogVerbose(@"%@ shouldHaveScreenLock YES 1.", self.logTag);
return YES;
2018-03-21 20:46:23 +01:00
} else {
2018-03-26 20:52:07 +02:00
OWSAssert(self.appEnteredBackgroundDate);
2018-03-21 20:46:23 +01:00
2018-03-26 20:52:07 +02:00
NSTimeInterval screenLockInterval = fabs([self.appEnteredBackgroundDate timeIntervalSinceNow]);
2018-03-21 20:46:23 +01:00
NSTimeInterval screenLockTimeout = OWSScreenLock.sharedManager.screenLockTimeout;
OWSAssert(screenLockInterval >= 0);
OWSAssert(screenLockTimeout >= 0);
2018-03-22 23:38:31 +01:00
if (screenLockInterval < screenLockTimeout) {
2018-03-21 20:46:23 +01:00
// Don't show 'Screen Lock' if 'Screen Lock' timeout hasn't elapsed.
2018-03-27 21:55:31 +02:00
DDLogVerbose(@"%@ shouldHaveScreenLock NO 6.", self.logTag);
return NO;
2018-03-21 20:46:23 +01:00
} else {
// Otherwise, show 'Screen Lock'.
2018-03-27 21:55:31 +02:00
DDLogVerbose(@"%@ shouldHaveScreenLock YES 2.", self.logTag);
return YES;
2018-03-21 20:46:23 +01:00
}
}
}
- (void)showScreenLockFailureAlertWithMessage:(NSString *)message
{
OWSAssertIsOnMainThread();
[OWSAlerts showAlertWithTitle:NSLocalizedString(@"SCREEN_LOCK_UNLOCK_FAILED",
@"Title for alert indicating that screen lock could not be unlocked.")
message:message
buttonTitle:nil
buttonAction:^(UIAlertAction *action) {
// After the alert, re-show the unlock UI.
[self ensureScreenProtection];
}];
}
// 'Screen Blocking' window obscures the app screen:
//
// * In the app switcher.
// * During 'Screen Lock' unlock process.
- (void)prepareScreenProtectionWithRootWindow:(UIWindow *)rootWindow
{
OWSAssertIsOnMainThread();
OWSAssert(rootWindow);
UIWindow *window = [[UIWindow alloc] initWithFrame:rootWindow.bounds];
window.hidden = YES;
window.opaque = YES;
window.windowLevel = CGFLOAT_MAX;
window.backgroundColor = UIColor.ows_materialBlueColor;
2018-03-27 21:55:31 +02:00
UIViewController *viewController = [UIViewController new];
viewController.view.backgroundColor = UIColor.ows_materialBlueColor;
2018-03-27 23:46:05 +02:00
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];
2018-03-27 21:55:31 +02:00
window.rootViewController = viewController;
2018-03-21 20:46:23 +01:00
self.screenBlockingWindow = window;
2018-03-27 21:55:31 +02:00
self.screenBlockingViewController = viewController;
2018-03-27 23:46:05 +02:00
self.screenBlockingImageView = imageView;
self.screenBlockingButton = button;
2018-03-27 21:55:31 +02:00
2018-03-27 23:46:05 +02:00
[self updateScreenBlockingWindow:YES shouldHaveScreenLock:NO animated:NO];
2018-03-27 21:55:31 +02:00
}
2018-03-27 22:16:35 +02:00
// 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.
2018-03-27 23:46:05 +02:00
- (void)updateScreenBlockingWindow:(BOOL)shouldShowBlockWindow
shouldHaveScreenLock:(BOOL)shouldHaveScreenLock
animated:(BOOL)animated
2018-03-27 21:55:31 +02:00
{
OWSAssertIsOnMainThread();
self.screenBlockingWindow.hidden = !shouldShowBlockWindow;
UIView *rootView = self.screenBlockingViewController.view;
2018-03-27 23:46:05 +02:00
[NSLayoutConstraint deactivateConstraints:self.screenBlockingConstraints];
2018-03-27 21:55:31 +02:00
2018-03-27 23:46:05 +02:00
NSMutableArray<NSLayoutConstraint *> *screenBlockingConstraints = [NSMutableArray new];
2018-03-27 21:55:31 +02:00
BOOL shouldShowUnlockButton = (!self.appIsInactive && !self.appIsInBackground && self.didLastUnlockAttemptFail);
DDLogVerbose(@"%@ updateScreenBlockingWindow. shouldShowBlockWindow: %d, shouldHaveScreenLock: %d, "
@"shouldShowUnlockButton: %d.",
self.logTag,
shouldShowBlockWindow,
shouldHaveScreenLock,
shouldShowUnlockButton);
2018-03-27 23:46:05 +02:00
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]];
2018-03-27 21:55:31 +02:00
} else {
2018-03-27 23:46:05 +02:00
[screenBlockingConstraints addObject:[self.screenBlockingImageView autoVCenterInSuperview]];
2018-03-27 21:55:31 +02:00
}
2018-03-27 23:46:05 +02:00
self.screenBlockingConstraints = screenBlockingConstraints;
self.screenBlockingSignature = signature;
if (animated) {
[UIView animateWithDuration:0.35f
animations:^{
[rootView layoutIfNeeded];
}];
} else {
[rootView layoutIfNeeded];
}
2018-03-27 21:55:31 +02:00
}
- (void)showUnlockUI
{
OWSAssertIsOnMainThread();
DDLogInfo(@"showUnlockUI");
self.didLastUnlockAttemptFail = NO;
[self ensureScreenProtection];
2018-03-21 20:46:23 +01:00
}
#pragma mark - Events
- (void)screenLockDidChange:(NSNotification *)notification
{
[self ensureScreenProtection];
}
- (void)registrationStateDidChange
{
OWSAssertIsOnMainThread();
DDLogInfo(@"registrationStateDidChange");
[self ensureScreenProtection];
}
2018-03-27 21:55:31 +02:00
- (void)clearAuthUIWhenActive
{
2018-03-27 22:16:35 +02:00
// For continuity, continue to present blocking screen in "screen lock" mode while
// dismissing the "local auth UI".
2018-03-27 21:55:31 +02:00
if (self.appIsInactive) {
self.shouldClearAuthUIWhenActive = YES;
} else {
self.isShowingScreenLockUI = NO;
2018-03-27 23:46:05 +02:00
[self ensureScreenProtection];
2018-03-27 21:55:31 +02:00
}
}
2018-03-21 20:46:23 +01:00
- (void)applicationDidBecomeActive:(NSNotification *)notification
{
2018-03-27 21:55:31 +02:00
if (self.shouldClearAuthUIWhenActive) {
self.shouldClearAuthUIWhenActive = NO;
self.isShowingScreenLockUI = NO;
}
2018-03-27 23:46:05 +02:00
self.appIsInactive = NO;
2018-03-21 20:46:23 +01:00
}
- (void)applicationWillResignActive:(NSNotification *)notification
{
self.appIsInactive = YES;
}
2018-03-22 23:38:31 +01:00
- (void)applicationWillEnterForeground:(NSNotification *)notification
{
// 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.lastUnlockAttemptDate = nil;
self.lastUnlockSuccessDate = nil;
2018-03-22 23:38:31 +01:00
self.appIsInBackground = NO;
2018-03-27 21:55:31 +02:00
self.appEnteredForegroundDate = [NSDate new];
2018-03-22 23:38:31 +01:00
}
- (void)applicationDidEnterBackground:(NSNotification *)notification
{
self.appIsInBackground = YES;
}
- (void)inactiveTimerDidFire
{
[self markAppAsInBackground];
}
2018-03-21 20:46:23 +01:00
@end
NS_ASSUME_NONNULL_END