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
|
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
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;
|
|
|
|
|
2018-03-21 20:46:23 +01:00
|
|
|
@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-04-12 18:23:11 +02:00
|
|
|
// 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;
|
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"
|
2018-04-12 18:23:11 +02:00
|
|
|
// UI is dismissing. So we lazily clear isShowingScreenLockUI
|
|
|
|
// using this property.
|
2018-03-27 21:55:31 +02:00
|
|
|
@property (nonatomic) BOOL shouldClearAuthUIWhenActive;
|
2018-03-21 20:46:23 +01:00
|
|
|
|
2018-04-06 18:12:49 +02:00
|
|
|
// Indicates whether or not the user is currently locked out of
|
2018-04-12 18:23:11 +02:00
|
|
|
// the app. Should only be set if OWSScreenLock.isScreenLockEnabled.
|
2018-04-06 18:12:49 +02:00
|
|
|
//
|
2018-04-12 18:23:11 +02:00
|
|
|
// * The user is locked out by default on app launch.
|
2018-04-06 18:12:49 +02:00
|
|
|
// * 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.
|
2018-04-12 18:23:11 +02:00
|
|
|
@property (nonatomic) BOOL isScreenLockLocked;
|
2018-03-28 18:01:23 +02:00
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
// The "countdown" until screen lock takes effect.
|
2018-04-06 18:00:09 +02:00
|
|
|
@property (nonatomic, nullable) NSDate *screenLockCountdownDate;
|
2018-04-06 18:12:49 +02:00
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
@property (nonatomic) UIWindow *rootWindow;
|
2018-04-06 18:12:49 +02:00
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
@property (nonatomic, nullable) UIResponder *rootWindowResponder;
|
2018-04-06 18:00:09 +02:00
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
|
|
|
|
_appIsInactiveOrBackground = [UIApplication sharedApplication].applicationState != UIApplicationStateActive;
|
|
|
|
|
2018-03-21 20:46:23 +01:00
|
|
|
[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(screenLockDidChange:)
|
|
|
|
name:OWSScreenLock.ScreenLockDidChange
|
|
|
|
object:nil];
|
2018-04-06 18:00:09 +02:00
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self
|
2018-04-12 18:23:11 +02:00
|
|
|
selector:@selector(clockDidChange:)
|
|
|
|
name:NSSystemClockDidChangeNotification
|
2018-04-06 18:00:09 +02:00
|
|
|
object:nil];
|
2018-03-21 20:46:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)setupWithRootWindow:(UIWindow *)rootWindow
|
|
|
|
{
|
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
OWSAssert(rootWindow);
|
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
self.rootWindow = rootWindow;
|
|
|
|
|
2018-03-21 20:46:23 +01:00
|
|
|
[self prepareScreenProtectionWithRootWindow:rootWindow];
|
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
// Initialize the screen lock state.
|
|
|
|
//
|
|
|
|
// It's not safe to access OWSScreenLock.isScreenLockEnabled
|
|
|
|
// until the app is ready.
|
2018-03-21 20:46:23 +01:00
|
|
|
[AppReadiness runNowOrWhenAppIsReady:^{
|
2018-04-12 18:23:11 +02:00
|
|
|
self.isScreenLockLocked = OWSScreenLock.sharedManager.isScreenLockEnabled;
|
|
|
|
|
|
|
|
[self ensureUI];
|
2018-03-21 20:46:23 +01:00
|
|
|
}];
|
|
|
|
}
|
|
|
|
|
|
|
|
#pragma mark - Methods
|
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
- (void)tryToActivateScreenLockBasedOnCountdown
|
2018-03-21 20:46:23 +01:00
|
|
|
{
|
2018-04-12 18:23:11 +02:00
|
|
|
OWSAssert(!self.appIsInBackground);
|
|
|
|
OWSAssertIsOnMainThread();
|
2018-04-06 18:00:09 +02:00
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
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`.
|
2018-04-06 18:00:09 +02:00
|
|
|
DDLogVerbose(@"%@ tryToActivateScreenLockUponBecomingActive NO 0", self.logTag);
|
|
|
|
return;
|
|
|
|
}
|
2018-04-12 18:23:11 +02:00
|
|
|
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;
|
|
|
|
}
|
2018-04-06 18:00:09 +02:00
|
|
|
if (!self.screenLockCountdownDate) {
|
|
|
|
// We became inactive, but never started a countdown.
|
2018-04-12 18:23:11 +02:00
|
|
|
DDLogVerbose(@"%@ tryToActivateScreenLockUponBecomingActive NO 3", self.logTag);
|
2018-04-06 18:00:09 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
NSTimeInterval countdownInterval = fabs([self.screenLockCountdownDate timeIntervalSinceNow]);
|
|
|
|
OWSAssert(countdownInterval >= 0);
|
|
|
|
NSTimeInterval screenLockTimeout = OWSScreenLock.sharedManager.screenLockTimeout;
|
|
|
|
OWSAssert(screenLockTimeout >= 0);
|
|
|
|
if (countdownInterval >= screenLockTimeout) {
|
2018-04-12 18:23:11 +02:00
|
|
|
self.isScreenLockLocked = YES;
|
|
|
|
|
|
|
|
DDLogVerbose(@"%@ tryToActivateScreenLockUponBecomingActive YES 4 (%0.3f >= %0.3f)",
|
2018-04-06 18:00:09 +02:00
|
|
|
self.logTag,
|
|
|
|
countdownInterval,
|
|
|
|
screenLockTimeout);
|
|
|
|
} else {
|
2018-04-12 18:23:11 +02:00
|
|
|
DDLogVerbose(@"%@ tryToActivateScreenLockUponBecomingActive NO 5 (%0.3f < %0.3f)",
|
2018-04-06 18:00:09 +02:00
|
|
|
self.logTag,
|
|
|
|
countdownInterval,
|
|
|
|
screenLockTimeout);
|
|
|
|
}
|
|
|
|
}
|
2018-03-28 18:01:23 +02:00
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
// Setter for property indicating that the app is either
|
|
|
|
// inactive or in the background, e.g. not "foreground and active."
|
|
|
|
- (void)setAppIsInactiveOrBackground:(BOOL)appIsInactiveOrBackground
|
2018-04-06 18:00:09 +02:00
|
|
|
{
|
2018-04-12 18:23:11 +02:00
|
|
|
OWSAssertIsOnMainThread();
|
2018-03-21 20:46:23 +01:00
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
_appIsInactiveOrBackground = appIsInactiveOrBackground;
|
2018-04-06 18:00:09 +02:00
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
if (appIsInactiveOrBackground) {
|
|
|
|
if (!self.isShowingScreenLockUI) {
|
|
|
|
[self startScreenLockCountdownIfNecessary];
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
[self tryToActivateScreenLockBasedOnCountdown];
|
|
|
|
|
|
|
|
DDLogInfo(@"%@ setAppIsInactiveOrBackground clear screenLockCountdownDate.", self.logTag);
|
2018-04-06 18:00:09 +02:00
|
|
|
self.screenLockCountdownDate = nil;
|
2018-03-28 18:01:23 +02:00
|
|
|
}
|
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
[self ensureUI];
|
2018-03-21 20:46:23 +01:00
|
|
|
}
|
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
// Setter for property indicating that the app is in the background.
|
|
|
|
// If true, by definition the app is not active.
|
2018-03-22 23:38:31 +01:00
|
|
|
- (void)setAppIsInBackground:(BOOL)appIsInBackground
|
|
|
|
{
|
2018-04-12 18:23:11 +02:00
|
|
|
OWSAssertIsOnMainThread();
|
2018-03-26 20:52:07 +02:00
|
|
|
|
2018-03-22 23:38:31 +01:00
|
|
|
_appIsInBackground = appIsInBackground;
|
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
if (self.appIsInBackground) {
|
|
|
|
[self startScreenLockCountdownIfNecessary];
|
|
|
|
} else {
|
|
|
|
[self tryToActivateScreenLockBasedOnCountdown];
|
|
|
|
}
|
|
|
|
|
|
|
|
[self ensureUI];
|
2018-03-22 23:38:31 +01:00
|
|
|
}
|
|
|
|
|
2018-04-06 18:00:09 +02:00
|
|
|
- (void)startScreenLockCountdownIfNecessary
|
2018-03-28 18:01:23 +02:00
|
|
|
{
|
2018-04-12 18:23:11 +02:00
|
|
|
DDLogVerbose(@"%@ startScreenLockCountdownIfNecessary: %d", self.logTag, self.screenLockCountdownDate != nil);
|
|
|
|
|
2018-04-06 18:00:09 +02:00
|
|
|
if (!self.screenLockCountdownDate) {
|
2018-04-12 18:23:11 +02:00
|
|
|
DDLogInfo(@"%@ startScreenLockCountdown.", self.logTag);
|
2018-04-06 18:00:09 +02:00
|
|
|
self.screenLockCountdownDate = [NSDate new];
|
2018-04-04 16:07:40 +02:00
|
|
|
}
|
2018-03-28 18:01:23 +02:00
|
|
|
|
|
|
|
self.didLastUnlockAttemptFail = NO;
|
|
|
|
}
|
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
// Ensure that:
|
|
|
|
//
|
|
|
|
// * The blocking window has the correct state.
|
|
|
|
// * That we show the "iOS auth UI to unlock" if necessary.
|
|
|
|
- (void)ensureUI
|
2018-03-21 20:46:23 +01:00
|
|
|
{
|
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
|
|
|
|
if (!AppReadiness.isAppReady) {
|
|
|
|
[AppReadiness runNowOrWhenAppIsReady:^{
|
2018-04-12 18:23:11 +02:00
|
|
|
[self ensureUI];
|
2018-03-21 20:46:23 +01:00
|
|
|
}];
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
ScreenLockUIState desiredUIState = self.desiredUIState;
|
2018-03-24 17:30:35 +01:00
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
DDLogVerbose(@"%@, ensureUI: %@", self.logTag, NSStringForScreenLockUIState(desiredUIState));
|
|
|
|
|
|
|
|
[self updateScreenBlockingWindow:desiredUIState animated:YES];
|
2018-03-24 17:30:35 +01:00
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
// Show the "iOS auth UI to unlock" if necessary.
|
|
|
|
if (desiredUIState == ScreenLockUIStateScreenLock && !self.didLastUnlockAttemptFail) {
|
|
|
|
[self tryToPresentAuthUIToUnlockScreenLock];
|
2018-03-24 17:30:35 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
- (void)tryToPresentAuthUIToUnlockScreenLock
|
2018-03-24 17:30:35 +01:00
|
|
|
{
|
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
if (self.isShowingScreenLockUI) {
|
|
|
|
// We're already showing the auth UI; abort.
|
2018-03-27 21:55:31 +02:00
|
|
|
return;
|
|
|
|
}
|
2018-04-12 18:23:11 +02:00
|
|
|
if (self.appIsInactiveOrBackground) {
|
|
|
|
// Never show the auth UI unless active.
|
2018-03-24 17:30:35 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2018-03-27 18:58:21 +02:00
|
|
|
DDLogInfo(@"%@, try to unlock screen lock", self.logTag);
|
2018-03-24 17:30:35 +01:00
|
|
|
|
|
|
|
self.isShowingScreenLockUI = YES;
|
|
|
|
|
|
|
|
[OWSScreenLock.sharedManager tryToUnlockScreenLockWithSuccess:^{
|
|
|
|
DDLogInfo(@"%@ unlock screen lock succeeded.", self.logTag);
|
2018-04-06 18:00:09 +02:00
|
|
|
|
2018-03-24 17:30:35 +01:00
|
|
|
self.isShowingScreenLockUI = NO;
|
2018-04-06 18:00:09 +02:00
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
self.isScreenLockLocked = NO;
|
2018-04-06 18:00:09 +02:00
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
[self ensureUI];
|
2018-03-24 17:30:35 +01:00
|
|
|
}
|
|
|
|
failure:^(NSError *error) {
|
|
|
|
DDLogInfo(@"%@ unlock screen lock failed.", self.logTag);
|
|
|
|
|
2018-03-27 21:55:31 +02:00
|
|
|
[self clearAuthUIWhenActive];
|
2018-03-27 18:58:21 +02:00
|
|
|
|
2018-03-27 21:55:31 +02:00
|
|
|
self.didLastUnlockAttemptFail = YES;
|
|
|
|
|
|
|
|
[self showScreenLockFailureAlertWithMessage:error.localizedDescription];
|
2018-03-27 18:58:21 +02:00
|
|
|
}
|
2018-03-28 18:32:57 +02:00
|
|
|
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];
|
|
|
|
});
|
|
|
|
}
|
2018-03-24 17:30:35 +01:00
|
|
|
cancel:^{
|
|
|
|
DDLogInfo(@"%@ unlock screen lock cancelled.", self.logTag);
|
2018-03-27 21:55:31 +02:00
|
|
|
|
|
|
|
[self clearAuthUIWhenActive];
|
|
|
|
|
|
|
|
self.didLastUnlockAttemptFail = YES;
|
2018-03-24 17:30:35 +01:00
|
|
|
|
|
|
|
// Re-show the unlock UI.
|
2018-04-12 18:23:11 +02:00
|
|
|
[self ensureUI];
|
2018-03-24 17:30:35 +01:00
|
|
|
}];
|
2018-03-27 23:46:05 +02:00
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
[self ensureUI];
|
2018-03-24 17:30:35 +01:00
|
|
|
}
|
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
// Determines what the state of the app should be.
|
|
|
|
- (ScreenLockUIState)desiredUIState
|
2018-03-24 17:30:35 +01:00
|
|
|
{
|
2018-04-12 18:23:11 +02:00
|
|
|
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;
|
|
|
|
}
|
2018-03-27 21:55:31 +02:00
|
|
|
}
|
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
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;
|
2018-03-21 20:46:23 +01:00
|
|
|
} else {
|
2018-04-12 18:23:11 +02:00
|
|
|
DDLogVerbose(@"%@ desiredUIState: none 5.", self.logTag);
|
|
|
|
return ScreenLockUIStateNone;
|
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.
|
2018-04-12 18:23:11 +02:00
|
|
|
[self ensureUI];
|
2018-04-16 23:39:13 +02:00
|
|
|
}
|
|
|
|
fromViewController:self.screenBlockingViewController];
|
2018-03-21 20:46:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// '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];
|
2018-04-12 18:23:11 +02:00
|
|
|
window.hidden = NO;
|
|
|
|
window.windowLevel = UIWindowLevel_Background;
|
2018-03-21 20:46:23 +01:00
|
|
|
window.opaque = YES;
|
|
|
|
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-04-12 18:23:11 +02:00
|
|
|
// Default to screen protection until we know otherwise.
|
|
|
|
[self updateScreenBlockingWindow:ScreenLockUIStateNone 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-04-12 18:23:11 +02:00
|
|
|
- (void)updateScreenBlockingWindow:(ScreenLockUIState)desiredUIState animated:(BOOL)animated
|
2018-03-27 21:55:31 +02:00
|
|
|
{
|
|
|
|
OWSAssertIsOnMainThread();
|
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
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;
|
|
|
|
}
|
2018-03-27 21:55:31 +02:00
|
|
|
|
|
|
|
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
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
BOOL shouldHaveScreenLock = desiredUIState == ScreenLockUIStateScreenLock;
|
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();
|
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
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);
|
2018-03-27 21:55:31 +02:00
|
|
|
|
|
|
|
self.didLastUnlockAttemptFail = NO;
|
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
[self ensureUI];
|
2018-03-21 20:46:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
#pragma mark - Events
|
|
|
|
|
|
|
|
- (void)screenLockDidChange:(NSNotification *)notification
|
|
|
|
{
|
2018-04-12 18:23:11 +02:00
|
|
|
[self ensureUI];
|
2018-03-21 20:46:23 +01:00
|
|
|
}
|
|
|
|
|
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-04-12 18:23:11 +02:00
|
|
|
if (self.appIsInactiveOrBackground) {
|
2018-03-27 21:55:31 +02:00
|
|
|
self.shouldClearAuthUIWhenActive = YES;
|
|
|
|
} else {
|
|
|
|
self.isShowingScreenLockUI = NO;
|
2018-04-12 18:23:11 +02:00
|
|
|
[self ensureUI];
|
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
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
self.appIsInactiveOrBackground = NO;
|
2018-03-21 20:46:23 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
- (void)applicationWillResignActive:(NSNotification *)notification
|
|
|
|
{
|
2018-04-12 18:23:11 +02:00
|
|
|
self.appIsInactiveOrBackground = YES;
|
2018-03-21 20:46:23 +01:00
|
|
|
}
|
|
|
|
|
2018-03-22 23:38:31 +01:00
|
|
|
- (void)applicationWillEnterForeground:(NSNotification *)notification
|
|
|
|
{
|
|
|
|
self.appIsInBackground = NO;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)applicationDidEnterBackground:(NSNotification *)notification
|
|
|
|
{
|
|
|
|
self.appIsInBackground = YES;
|
|
|
|
}
|
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
// Whenever the device date/time is edited by the user,
|
|
|
|
// trigger screen lock immediately if enabled.
|
|
|
|
- (void)clockDidChange:(NSNotification *)notification
|
2018-03-28 18:01:23 +02:00
|
|
|
{
|
2018-04-12 18:23:11 +02:00
|
|
|
DDLogInfo(@"%@ clock did change", self.logTag);
|
2018-04-06 18:00:09 +02:00
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
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;
|
2018-04-06 18:00:09 +02:00
|
|
|
}
|
2018-04-12 18:23:11 +02:00
|
|
|
self.isScreenLockLocked = OWSScreenLock.sharedManager.isScreenLockEnabled;
|
2018-04-06 18:00:09 +02:00
|
|
|
|
2018-04-12 18:23:11 +02:00
|
|
|
// NOTE: this notifications fires _before_ applicationDidBecomeActive,
|
|
|
|
// which is desirable. Don't assume that though; call ensureUI
|
|
|
|
// just in case it's necessary.
|
|
|
|
[self ensureUI];
|
2018-03-28 18:01:23 +02:00
|
|
|
}
|
|
|
|
|
2018-03-21 20:46:23 +01:00
|
|
|
@end
|
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_END
|