session-ios/Session/Shared/OWSScreenLockUI.m
Morgan Pretty aabf656d89 Finished off the MediaGallery logic
Updated the config message generation for GRDB
Migrated more preferences into GRDB
Added paging to the MediaTileViewController and sorted out the various animations/transitions
Fixed an issue where the 'recipientState' for the 'baseQuery' on the ConversationCell.ViewModel wasn't grouping correctly
Fixed an issue where the MediaZoomAnimationController could fail if the contextual info wasn't available
Fixed an issue where the MediaZoomAnimationController bounce looked odd when returning to the detail screen from the tile screen
Fixed an issue where the MediaZoomAnimationController didn't work for videos
Fixed a bug where the YDB to GRDB migration wasn't properly handling video files
Fixed a number of minor UI bugs with the GalleryRailView
Deleted a bunch of legacy code
2022-05-20 17:58:39 +10:00

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 ([SMKPreferences isScreenSecurityEnabled]) {
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