mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			511 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Objective-C
		
	
			
		
		
	
	
			511 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Objective-C
		
	
| //
 | |
| //  Copyright (c) 2018 Open Whisper Systems. All rights reserved.
 | |
| //
 | |
| 
 | |
| #import "OWSScreenLockUI.h"
 | |
| #import "OWSWindowManager.h"
 | |
| #import "Session-Swift.h"
 | |
| #import <SignalUtilitiesKit/ScreenLockViewController.h>
 | |
| #import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
 | |
| #import <SessionUtilitiesKit/UIView+OWS.h>
 | |
| 
 | |
| NS_ASSUME_NONNULL_BEGIN
 | |
| 
 | |
| @interface OWSScreenLockUI () <ScreenLockViewDelegate>
 | |
| 
 | |
| @property (nonatomic) UIWindow *screenBlockingWindow;
 | |
| @property (nonatomic) ScreenLockViewController *screenBlockingViewController;
 | |
| 
 | |
| // Unlike UIApplication.applicationState, this state reflects the
 | |
| // notifications, i.e. "did become active", "will resign active",
 | |
| // "will enter foreground", "did enter background".
 | |
| //
 | |
| // We want to update our state to reflect these transitions and have
 | |
| // the "update" logic be consistent with "last reported" state. i.e.
 | |
| // when you're responding to "will resign active", we need to behave
 | |
| // as though we're already inactive.
 | |
| //
 | |
| // Secondly, we need to show the screen protection _before_ we become
 | |
| // inactive in order for it to be reflected in the app switcher.
 | |
| @property (nonatomic) BOOL appIsInactiveOrBackground;
 | |
| @property (nonatomic) BOOL appIsInBackground;
 | |
| 
 | |
| @property (nonatomic) BOOL isShowingScreenLockUI;
 | |
| 
 | |
| @property (nonatomic) BOOL didLastUnlockAttemptFail;
 | |
| 
 | |
| // We want to remain in "screen lock" mode while "local auth"
 | |
| // UI is dismissing. So we lazily clear isShowingScreenLockUI
 | |
| // using this property.
 | |
| @property (nonatomic) BOOL shouldClearAuthUIWhenActive;
 | |
| 
 | |
| // Indicates whether or not the user is currently locked out of
 | |
| // the app.  Should only be set if OWSScreenLock.isScreenLockEnabled.
 | |
| //
 | |
| // * The user is locked out by default on app launch.
 | |
| // * The user is also locked out if they spend more than
 | |
| //   "timeout" seconds outside the app.  When the user leaves
 | |
| //   the app, a "countdown" begins.
 | |
| @property (nonatomic) BOOL isScreenLockLocked;
 | |
| 
 | |
| // The "countdown" until screen lock takes effect.
 | |
| @property (nonatomic, nullable) NSDate *screenLockCountdownDate;
 | |
| 
 | |
| @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;
 | |
|     }
 | |
| 
 | |
|     OWSAssertIsOnMainThread();
 | |
| 
 | |
|     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];
 | |
|     [[NSNotificationCenter defaultCenter] addObserver:self
 | |
|                                              selector:@selector(applicationWillEnterForeground:)
 | |
|                                                  name:OWSApplicationWillEnterForegroundNotification
 | |
|                                                object:nil];
 | |
|     [[NSNotificationCenter defaultCenter] addObserver:self
 | |
|                                              selector:@selector(applicationDidEnterBackground:)
 | |
|                                                  name:OWSApplicationDidEnterBackgroundNotification
 | |
|                                                object:nil];
 | |
|     [[NSNotificationCenter defaultCenter] addObserver:self
 | |
|                                              selector:@selector(screenLockDidChange:)
 | |
|                                                  name:OWSScreenLock.ScreenLockDidChange
 | |
|                                                object:nil];
 | |
|     [[NSNotificationCenter defaultCenter] addObserver:self
 | |
|                                              selector:@selector(clockDidChange:)
 | |
|                                                  name:NSSystemClockDidChangeNotification
 | |
|                                                object:nil];
 | |
| }
 | |
| 
 | |
| - (void)setupWithRootWindow:(UIWindow *)rootWindow
 | |
| {
 | |
|     OWSAssertIsOnMainThread();
 | |
|     OWSAssertDebug(rootWindow);
 | |
| 
 | |
|     [self createScreenBlockingWindowWithRootWindow:rootWindow];
 | |
|     OWSAssertDebug(self.screenBlockingWindow);
 | |
| }
 | |
| 
 | |
| - (void)startObserving
 | |
| {
 | |
|     _appIsInactiveOrBackground = [UIApplication sharedApplication].applicationState != UIApplicationStateActive;
 | |
| 
 | |
|     [self observeNotifications];
 | |
| 
 | |
|     // Hide the screen blocking window until "app is ready" to
 | |
|     // avoid blocking the loading view.
 | |
|     [self updateScreenBlockingWindow:ScreenLockUIStateNone animated:NO];
 | |
| 
 | |
|     // Initialize the screen lock state.
 | |
|     //
 | |
|     // It's not safe to access OWSScreenLock.isScreenLockEnabled
 | |
|     // until the app is ready.
 | |
|     [AppReadiness runNowOrWhenAppWillBecomeReady:^{
 | |
|         self.isScreenLockLocked = OWSScreenLock.sharedManager.isScreenLockEnabled;
 | |
|         
 | |
|         [self ensureUI];
 | |
|     }];
 | |
| }
 | |
| 
 | |
| #pragma mark - Methods
 | |
| 
 | |
| - (void)tryToActivateScreenLockBasedOnCountdown
 | |
| {
 | |
|     OWSAssertDebug(!self.appIsInBackground);
 | |
|     OWSAssertIsOnMainThread();
 | |
| 
 | |
|     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`.
 | |
|         OWSLogVerbose(@"tryToActivateScreenLockUponBecomingActive NO 0");
 | |
|         return;
 | |
|     }
 | |
|     if (!OWSScreenLock.sharedManager.isScreenLockEnabled) {
 | |
|         // Screen lock is not enabled.
 | |
|         OWSLogVerbose(@"tryToActivateScreenLockUponBecomingActive NO 1");
 | |
|         return;
 | |
|     }
 | |
|     if (self.isScreenLockLocked) {
 | |
|         // Screen lock is already activated.
 | |
|         OWSLogVerbose(@"tryToActivateScreenLockUponBecomingActive NO 2");
 | |
|         return;
 | |
|     }
 | |
|     if (!self.screenLockCountdownDate) {
 | |
|         // We became inactive, but never started a countdown.
 | |
|         OWSLogVerbose(@"tryToActivateScreenLockUponBecomingActive NO 3");
 | |
|         return;
 | |
|     }
 | |
|     NSTimeInterval countdownInterval = fabs([self.screenLockCountdownDate timeIntervalSinceNow]);
 | |
|     OWSAssertDebug(countdownInterval >= 0);
 | |
|     NSTimeInterval screenLockTimeout = OWSScreenLock.sharedManager.screenLockTimeout;
 | |
|     OWSAssertDebug(screenLockTimeout >= 0);
 | |
|     if (countdownInterval >= screenLockTimeout) {
 | |
|         self.isScreenLockLocked = YES;
 | |
| 
 | |
|         OWSLogVerbose(
 | |
|             @"tryToActivateScreenLockUponBecomingActive YES 4 (%0.3f >= %0.3f)", countdownInterval, screenLockTimeout);
 | |
|     } else {
 | |
|         OWSLogVerbose(
 | |
|             @"tryToActivateScreenLockUponBecomingActive NO 5 (%0.3f < %0.3f)", countdownInterval, screenLockTimeout);
 | |
|     }
 | |
| }
 | |
| 
 | |
| // Setter for property indicating that the app is either
 | |
| // inactive or in the background, e.g. not "foreground and active."
 | |
| - (void)setAppIsInactiveOrBackground:(BOOL)appIsInactiveOrBackground
 | |
| {
 | |
|     OWSAssertIsOnMainThread();
 | |
| 
 | |
|     _appIsInactiveOrBackground = appIsInactiveOrBackground;
 | |
| 
 | |
|     if (appIsInactiveOrBackground) {
 | |
|         if (!self.isShowingScreenLockUI) {
 | |
|             [self startScreenLockCountdownIfNecessary];
 | |
|         }
 | |
|     } else {
 | |
|         [self tryToActivateScreenLockBasedOnCountdown];
 | |
| 
 | |
|         OWSLogInfo(@"setAppIsInactiveOrBackground clear screenLockCountdownDate.");
 | |
|         self.screenLockCountdownDate = nil;
 | |
|     }
 | |
| 
 | |
|     [self ensureUI];
 | |
| }
 | |
| 
 | |
| // Setter for property indicating that the app is in the background.
 | |
| // If true, by definition the app is not active.
 | |
| - (void)setAppIsInBackground:(BOOL)appIsInBackground
 | |
| {
 | |
|     OWSAssertIsOnMainThread();
 | |
| 
 | |
|     _appIsInBackground = appIsInBackground;
 | |
| 
 | |
|     if (self.appIsInBackground) {
 | |
|         [self startScreenLockCountdownIfNecessary];
 | |
|     } else {
 | |
|         [self tryToActivateScreenLockBasedOnCountdown];
 | |
|     }
 | |
| 
 | |
|     [self ensureUI];
 | |
| }
 | |
| 
 | |
| - (void)startScreenLockCountdownIfNecessary
 | |
| {
 | |
|     OWSLogVerbose(@"startScreenLockCountdownIfNecessary: %d", self.screenLockCountdownDate != nil);
 | |
| 
 | |
|     if (!self.screenLockCountdownDate) {
 | |
|         OWSLogInfo(@"startScreenLockCountdown.");
 | |
|         self.screenLockCountdownDate = [NSDate new];
 | |
|     }
 | |
| 
 | |
|     self.didLastUnlockAttemptFail = NO;
 | |
| }
 | |
| 
 | |
| // Ensure that:
 | |
| //
 | |
| // * The blocking window has the correct state.
 | |
| // * That we show the "iOS auth UI to unlock" if necessary.
 | |
| - (void)ensureUI
 | |
| {
 | |
|     OWSAssertIsOnMainThread();
 | |
| 
 | |
|     if (!AppReadiness.isAppReady) {
 | |
|         [AppReadiness runNowOrWhenAppWillBecomeReady:^{
 | |
|             [self ensureUI];
 | |
|         }];
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     ScreenLockUIState desiredUIState = self.desiredUIState;
 | |
| 
 | |
|     OWSLogVerbose(@"ensureUI: %@", NSStringForScreenLockUIState(desiredUIState));
 | |
| 
 | |
|     [self updateScreenBlockingWindow:desiredUIState animated:YES];
 | |
| 
 | |
|     // Show the "iOS auth UI to unlock" if necessary.
 | |
|     if (desiredUIState == ScreenLockUIStateScreenLock && !self.didLastUnlockAttemptFail) {
 | |
|         [self tryToPresentAuthUIToUnlockScreenLock];
 | |
|     }
 | |
| }
 | |
| 
 | |
| - (void)tryToPresentAuthUIToUnlockScreenLock
 | |
| {
 | |
|     OWSAssertIsOnMainThread();
 | |
| 
 | |
|     if (self.isShowingScreenLockUI) {
 | |
|         // We're already showing the auth UI; abort.
 | |
|         return;
 | |
|     }
 | |
|     if (self.appIsInactiveOrBackground) {
 | |
|         // Never show the auth UI unless active.
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     OWSLogInfo(@"try to unlock screen lock");
 | |
| 
 | |
|     self.isShowingScreenLockUI = YES;
 | |
| 
 | |
|     [OWSScreenLock.sharedManager
 | |
|         tryToUnlockScreenLockWithSuccess:^{
 | |
|             OWSLogInfo(@"unlock screen lock succeeded.");
 | |
| 
 | |
|             self.isShowingScreenLockUI = NO;
 | |
| 
 | |
|             self.isScreenLockLocked = NO;
 | |
| 
 | |
|             [self ensureUI];
 | |
|         }
 | |
|         failure:^(NSError *error) {
 | |
|             OWSLogInfo(@"unlock screen lock failed.");
 | |
| 
 | |
|             [self clearAuthUIWhenActive];
 | |
| 
 | |
|             self.didLastUnlockAttemptFail = YES;
 | |
| 
 | |
|             [self showScreenLockFailureAlertWithMessage:error.localizedDescription];
 | |
|         }
 | |
|         unexpectedFailure:^(NSError *error) {
 | |
|             OWSLogInfo(@"unlock screen lock unexpectedly failed.");
 | |
| 
 | |
|             // 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:^{
 | |
|             OWSLogInfo(@"unlock screen lock cancelled.");
 | |
| 
 | |
|             [self clearAuthUIWhenActive];
 | |
| 
 | |
|             self.didLastUnlockAttemptFail = YES;
 | |
| 
 | |
|             // Re-show the unlock UI.
 | |
|             [self ensureUI];
 | |
|         }];
 | |
| 
 | |
|     [self ensureUI];
 | |
| }
 | |
| 
 | |
| // Determines what the state of the app should be.
 | |
| - (ScreenLockUIState)desiredUIState
 | |
| {
 | |
|     if (self.isScreenLockLocked) {
 | |
|         if (self.appIsInactiveOrBackground) {
 | |
|             OWSLogVerbose(@"desiredUIState: screen protection 1.");
 | |
|             return ScreenLockUIStateScreenProtection;
 | |
|         } else {
 | |
|             OWSLogVerbose(@"desiredUIState: screen lock 2.");
 | |
|             return ScreenLockUIStateScreenLock;
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     if (!self.appIsInactiveOrBackground) {
 | |
|         // App is inactive or background.
 | |
|         OWSLogVerbose(@"desiredUIState: none 3.");
 | |
|         return ScreenLockUIStateNone;
 | |
|     }
 | |
|     
 | |
|     if (Environment.shared.isRequestingPermission) {
 | |
|         return ScreenLockUIStateNone;
 | |
|     }
 | |
| 
 | |
|     if (Environment.shared.preferences.screenSecurityIsEnabled) {
 | |
|         OWSLogVerbose(@"desiredUIState: screen protection 4.");
 | |
|         return ScreenLockUIStateScreenProtection;
 | |
|     } else {
 | |
|         OWSLogVerbose(@"desiredUIState: none 5.");
 | |
|         return ScreenLockUIStateNone;
 | |
|     }
 | |
| }
 | |
| 
 | |
| - (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, update the UI.
 | |
|                          [self ensureUI];
 | |
|                      }
 | |
|                fromViewController:self.screenBlockingWindow.rootViewController];
 | |
| }
 | |
| 
 | |
| // 'Screen Blocking' window obscures the app screen:
 | |
| //
 | |
| // * In the app switcher.
 | |
| // * During 'Screen Lock' unlock process.
 | |
| - (void)createScreenBlockingWindowWithRootWindow:(UIWindow *)rootWindow
 | |
| {
 | |
|     OWSAssertIsOnMainThread();
 | |
|     OWSAssertDebug(rootWindow);
 | |
| 
 | |
|     UIWindow *window = [[UIWindow alloc] initWithFrame:rootWindow.bounds];
 | |
|     window.hidden = NO;
 | |
|     window.windowLevel = UIWindowLevel_Background;
 | |
|     window.opaque = YES;
 | |
|     window.backgroundColor = UIColor.ows_materialBlueColor;
 | |
| 
 | |
|     ScreenLockViewController *viewController = [ScreenLockViewController new];
 | |
|     viewController.delegate = self;
 | |
|     window.rootViewController = viewController;
 | |
| 
 | |
|     self.screenBlockingWindow = window;
 | |
|     self.screenBlockingViewController = viewController;
 | |
| }
 | |
| 
 | |
| // 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:(ScreenLockUIState)desiredUIState animated:(BOOL)animated
 | |
| {
 | |
|     OWSAssertIsOnMainThread();
 | |
| 
 | |
|     BOOL shouldShowBlockWindow = desiredUIState != ScreenLockUIStateNone;
 | |
| 
 | |
|     [OWSWindowManager.sharedManager setIsScreenBlockActive:shouldShowBlockWindow];
 | |
| 
 | |
|     [self.screenBlockingViewController updateUIWithState:desiredUIState
 | |
|                                              isLogoAtTop:self.isShowingScreenLockUI
 | |
|                                                 animated:animated];
 | |
| }
 | |
| 
 | |
| #pragma mark - Events
 | |
| 
 | |
| - (void)screenLockDidChange:(NSNotification *)notification
 | |
| {
 | |
|     [self ensureUI];
 | |
| }
 | |
| 
 | |
| - (void)clearAuthUIWhenActive
 | |
| {
 | |
|     // For continuity, continue to present blocking screen in "screen lock" mode while
 | |
|     // dismissing the "local auth UI".
 | |
|     if (self.appIsInactiveOrBackground) {
 | |
|         self.shouldClearAuthUIWhenActive = YES;
 | |
|     } else {
 | |
|         self.isShowingScreenLockUI = NO;
 | |
|         [self ensureUI];
 | |
|     }
 | |
| }
 | |
| 
 | |
| - (void)applicationDidBecomeActive:(NSNotification *)notification
 | |
| {
 | |
|     if (self.shouldClearAuthUIWhenActive) {
 | |
|         self.shouldClearAuthUIWhenActive = NO;
 | |
|         self.isShowingScreenLockUI = NO;
 | |
|     }
 | |
| 
 | |
|     self.appIsInactiveOrBackground = NO;
 | |
| }
 | |
| 
 | |
| - (void)applicationWillResignActive:(NSNotification *)notification
 | |
| {
 | |
|     self.appIsInactiveOrBackground = YES;
 | |
| }
 | |
| 
 | |
| - (void)applicationWillEnterForeground:(NSNotification *)notification
 | |
| {
 | |
|     self.appIsInBackground = NO;
 | |
| }
 | |
| 
 | |
| - (void)applicationDidEnterBackground:(NSNotification *)notification
 | |
| {
 | |
|     self.appIsInBackground = YES;
 | |
| }
 | |
| 
 | |
| // Whenever the device date/time is edited by the user,
 | |
| // trigger screen lock immediately if enabled.
 | |
| - (void)clockDidChange:(NSNotification *)notification
 | |
| {
 | |
|     OWSLogInfo(@"clock did change");
 | |
| 
 | |
|     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`.
 | |
|         OWSLogVerbose(@"clockDidChange 0");
 | |
|         return;
 | |
|     }
 | |
|     self.isScreenLockLocked = OWSScreenLock.sharedManager.isScreenLockEnabled;
 | |
| 
 | |
|     // NOTE: this notifications fires _before_ applicationDidBecomeActive,
 | |
|     // which is desirable.  Don't assume that though; call ensureUI
 | |
|     // just in case it's necessary.
 | |
|     [self ensureUI];
 | |
| }
 | |
| 
 | |
| #pragma mark - ScreenLockViewDelegate
 | |
| 
 | |
| - (void)unlockButtonWasTapped
 | |
| {
 | |
|     OWSAssertIsOnMainThread();
 | |
| 
 | |
|     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;
 | |
|     }
 | |
| 
 | |
|     OWSLogInfo(@"unlockButtonWasTapped");
 | |
| 
 | |
|     self.didLastUnlockAttemptFail = NO;
 | |
| 
 | |
|     [self ensureUI];
 | |
| }
 | |
| 
 | |
| @end
 | |
| 
 | |
| NS_ASSUME_NONNULL_END
 |