Merge pull request #377 from mpretty-cyro/feature/refactor-screen-lock

Cleaned up some of the Screen Lock behaviours
pull/1061/head
Morgan Pretty 3 weeks ago committed by GitHub
commit 43952c8386
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -312,7 +312,6 @@
C38EF24D255B6D67007E1867 /* UIView+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF240255B6D67007E1867 /* UIView+OWS.swift */; };
C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF241255B6D67007E1867 /* Collection+OWS.swift */; };
C38EF2B3255B6D9C007E1867 /* UIViewController+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */; };
C38EF30C255B6DBF007E1867 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2E2255B6DB9007E1867 /* ScreenLock.swift */; };
C38EF32E255B6DBF007E1867 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF304255B6DBE007E1867 /* ImageCache.swift */; };
C38EF363255B6DCC007E1867 /* ModalActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF349255B6DC7007E1867 /* ModalActivityIndicatorViewController.swift */; };
C38EF372255B6DCC007E1867 /* MediaMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF358255B6DCC007E1867 /* MediaMessageView.swift */; };
@ -685,8 +684,6 @@
FD4C53AF2CC1D62E003B10F4 /* _021_ReworkRecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD4C53AE2CC1D61E003B10F4 /* _021_ReworkRecipientState.swift */; };
FD52090328B4680F006098F6 /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090228B4680F006098F6 /* RadioButton.swift */; };
FD52090528B4915F006098F6 /* PrivacySettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */; };
FD52090928B59411006098F6 /* ScreenLockUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090828B59411006098F6 /* ScreenLockUI.swift */; };
FD52090B28B59BB4006098F6 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */; };
FD559DF52A7368CB00C7C62A /* DispatchQueue+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */; };
FD5931A72A8DA5DA0040147D /* SQLInterpolation+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */; };
FD5931AB2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */; };
@ -708,6 +705,9 @@
FD65318B2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; };
FD65318C2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; };
FD65318D2AA025C500DFEEAA /* TestDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6531892AA025C500DFEEAA /* TestDependencies.swift */; };
FD6673FD2D77F54600041530 /* ScreenLockViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */; };
FD6673FF2D77F9C100041530 /* ScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD6673FE2D77F9BE00041530 /* ScreenLock.swift */; };
FD6674002D77F9FD00041530 /* ScreenLockWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD52090828B59411006098F6 /* ScreenLockWindow.swift */; };
FD6673F62D7021E700041530 /* SessionUtil in Frameworks */ = {isa = PBXBuildFile; productRef = FD6673F52D7021E700041530 /* SessionUtil */; };
FD6673F82D7021F200041530 /* SessionUtil in Frameworks */ = {isa = PBXBuildFile; productRef = FD6673F72D7021F200041530 /* SessionUtil */; };
FD6673FA2D7021F800041530 /* SessionUtil in Frameworks */ = {isa = PBXBuildFile; productRef = FD6673F92D7021F800041530 /* SessionUtil */; };
@ -1579,7 +1579,6 @@
C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PlaceholderIcon.swift; path = SessionUIKit/Components/PlaceholderIcon.swift; sourceTree = SOURCE_ROOT; };
C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProfilePictureView.swift; path = SessionUIKit/Components/ProfilePictureView.swift; sourceTree = SOURCE_ROOT; };
C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "UIViewController+Utilities.swift"; path = "SignalUtilitiesKit/Utilities/UIViewController+Utilities.swift"; sourceTree = SOURCE_ROOT; };
C38EF2E2255B6DB9007E1867 /* ScreenLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ScreenLock.swift; path = "SignalUtilitiesKit/Screen Lock/ScreenLock.swift"; sourceTree = SOURCE_ROOT; };
C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProximityMonitoringManager.swift; path = SessionMessagingKit/Utilities/ProximityMonitoringManager.swift; sourceTree = SOURCE_ROOT; };
C38EF2EF255B6DBB007E1867 /* Weak.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Weak.swift; path = SessionUtilitiesKit/General/Weak.swift; sourceTree = SOURCE_ROOT; };
C38EF2F5255B6DBC007E1867 /* OWSAudioPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSAudioPlayer.h; path = SessionMessagingKit/Utilities/OWSAudioPlayer.h; sourceTree = SOURCE_ROOT; };
@ -1899,8 +1898,7 @@
FD52090228B4680F006098F6 /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = "<group>"; };
FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsViewModel.swift; sourceTree = "<group>"; };
FD52090628B49738006098F6 /* ConfirmationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationModal.swift; sourceTree = "<group>"; };
FD52090828B59411006098F6 /* ScreenLockUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockUI.swift; sourceTree = "<group>"; };
FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = "<group>"; };
FD52090828B59411006098F6 /* ScreenLockWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockWindow.swift; sourceTree = "<group>"; };
FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Utilities.swift"; sourceTree = "<group>"; };
FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SQLInterpolation+Utilities.swift"; sourceTree = "<group>"; };
FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScopeAdapter+Utilities.swift"; sourceTree = "<group>"; };
@ -1918,6 +1916,8 @@
FD61FCF82D308CC5005752DE /* GroupMemberSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberSpec.swift; sourceTree = "<group>"; };
FD61FCFA2D34A5DE005752DE /* _007_SplitSnodeReceivedMessageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _007_SplitSnodeReceivedMessageInfo.swift; sourceTree = "<group>"; };
FD6531892AA025C500DFEEAA /* TestDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDependencies.swift; sourceTree = "<group>"; };
FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = "<group>"; };
FD6673FE2D77F9BE00041530 /* ScreenLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLock.swift; sourceTree = "<group>"; };
FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SessionSnodeKit.xctestplan; sourceTree = "<group>"; };
FD66CB2B2BF344C600268FAB /* SessionMessageKit.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SessionMessageKit.xctestplan; sourceTree = "<group>"; };
FD6A38F02C2A66B100762359 /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = "<group>"; };
@ -2818,6 +2818,7 @@
C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */,
FD848B9228420164000E298B /* UnicodeScalar+Utilities.swift */,
C38EF2EF255B6DBB007E1867 /* Weak.swift */,
FD6673FE2D77F9BE00041530 /* ScreenLock.swift */,
FD5D201D27B0D87C00FEA984 /* SessionId.swift */,
7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */,
7B0EFDED274F598600FFAAE7 /* TimestampUtils.swift */,
@ -2856,7 +2857,7 @@
FD71162128D983ED00B47552 /* QRCodeScanningViewController.swift */,
9422569E2C23F8FE00C0FDBF /* ScanQRCodeScreen.swift */,
B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */,
FD52090828B59411006098F6 /* ScreenLockUI.swift */,
FD52090828B59411006098F6 /* ScreenLockWindow.swift */,
FD37EA0828AA2D27003AE748 /* SessionTableViewModel.swift */,
FD10AF0B2AF32B9A007709E5 /* SessionListViewModel.swift */,
FD37EA0628AA2CCA003AE748 /* SessionTableViewController.swift */,
@ -3114,6 +3115,7 @@
FD52090628B49738006098F6 /* ConfirmationModal.swift */,
C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */,
C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */,
FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */,
FD0150572CA27DEE005B08A1 /* ScrollableLabel.swift */,
FD71165A28E6DDBC00B47552 /* StyledNavigationController.swift */,
FD0B77AF29B69A65009169BA /* TopBannerController.swift */,
@ -3128,7 +3130,6 @@
children = (
C33FD9B7255A54A300E217F9 /* Meta */,
C36096ED25AD20FD008B62B2 /* Media Viewing & Editing */,
C36096EE25AD21BC008B62B2 /* Screen Lock */,
C3851CD225624B060061EEB0 /* Shared Views */,
C360970125AD22D3008B62B2 /* Shared View Controllers */,
C3CA3B11255CF17200F4C6D4 /* Utilities */,
@ -3312,15 +3313,6 @@
path = "Media Viewing & Editing";
sourceTree = "<group>";
};
C36096EE25AD21BC008B62B2 /* Screen Lock */ = {
isa = PBXGroup;
children = (
C38EF2E2255B6DB9007E1867 /* ScreenLock.swift */,
FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */,
);
path = "Screen Lock";
sourceTree = "<group>";
};
C360970125AD22D3008B62B2 /* Shared View Controllers */ = {
isa = PBXGroup;
children = (
@ -5812,6 +5804,7 @@
FD37E9F628A5F106003AE748 /* Configuration.swift in Sources */,
FD2272E62C351378004D8A6C /* SUIKImageFormat.swift in Sources */,
7BF8D1FB2A70AF57005F1D6E /* SwiftUI+Theme.swift in Sources */,
FD6673FD2D77F54600041530 /* ScreenLockViewController.swift in Sources */,
FDE754BE2C9BA16C002A2623 /* UIButtonConfiguration+Utilities.swift in Sources */,
FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */,
C331FFE02558FB0000070591 /* SearchBar.swift in Sources */,
@ -5847,7 +5840,6 @@
C38EF389255B6DD2007E1867 /* AttachmentTextView.swift in Sources */,
C38EF3FF255B6DF7007E1867 /* TappableView.swift in Sources */,
C38EF3C2255B6DE7007E1867 /* ImageEditorPaletteView.swift in Sources */,
C38EF30C255B6DBF007E1867 /* ScreenLock.swift in Sources */,
C38EF363255B6DCC007E1867 /* ModalActivityIndicatorViewController.swift in Sources */,
C38EF3C1255B6DE7007E1867 /* ImageEditorBrushViewController.swift in Sources */,
C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */,
@ -5872,7 +5864,6 @@
C38EF3FA255B6DF7007E1867 /* DirectionalPanGestureRecognizer.swift in Sources */,
C38EF3BB255B6DE7007E1867 /* ImageEditorStrokeItem.swift in Sources */,
C38EF3C0255B6DE7007E1867 /* ImageEditorCropViewController.swift in Sources */,
FD52090B28B59BB4006098F6 /* ScreenLockViewController.swift in Sources */,
C38EF3BD255B6DE7007E1867 /* ImageEditorTransform.swift in Sources */,
C33FDC58255A582000E217F9 /* ReverseDispatchQueue.swift in Sources */,
C38EF3F9255B6DF7007E1867 /* OWSLayerView.swift in Sources */,
@ -6028,6 +6019,7 @@
FDB7400B28EB99A70094D718 /* TimeInterval+Utilities.swift in Sources */,
FD09797D27FBDB2000936362 /* Notification+Utilities.swift in Sources */,
FDC6D7602862B3F600B04575 /* Dependencies.swift in Sources */,
FD6673FF2D77F9C100041530 /* ScreenLock.swift in Sources */,
FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */,
FD848B9328420164000E298B /* UnicodeScalar+Utilities.swift in Sources */,
FDE754CE2C9BAF37002A2623 /* ImageFormat.swift in Sources */,
@ -6340,7 +6332,6 @@
files = (
FD97B2422A3FEBF30027DD57 /* UnreadMarkerCell.swift in Sources */,
FD0606C32BCE13ED00C3816E /* MessageRequestFooterView.swift in Sources */,
FD52090928B59411006098F6 /* ScreenLockUI.swift in Sources */,
7B2561C429874851005C086C /* SessionCarouselView+Info.swift in Sources */,
FDF2220B2818F38D000A4995 /* SessionApp.swift in Sources */,
FD7115EB28C5D78E00B47552 /* ThreadSettingsViewModel.swift in Sources */,
@ -6451,6 +6442,7 @@
FDE754B72C9B96BB002A2623 /* WebRTCSession+MessageHandling.swift in Sources */,
FD37EA1928AC5CCA003AE748 /* NotificationSoundViewModel.swift in Sources */,
7BAFA75A2AAEA281001DA43E /* LinkPreviewView_SwiftUI.swift in Sources */,
FD6674002D77F9FD00041530 /* ScreenLockWindow.swift in Sources */,
FD71163E28E2C82900B47552 /* SessionCell.swift in Sources */,
9422569C2C23F8F000C0FDBF /* QRCodeScreen.swift in Sources */,
4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */,

@ -97,13 +97,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
// Note: Intentionally dispatching sync as we want to wait for these to complete before
// continuing
DispatchQueue.main.sync {
ScreenLockUI.shared.setupWithRootWindow(rootWindow: mainWindow, using: dependencies)
dependencies[singleton: .screenLock].setupWithRootWindow(rootWindow: mainWindow)
OWSWindowManager.shared().setup(
withRootWindow: mainWindow,
screenBlockingWindow: ScreenLockUI.shared.screenBlockingWindow,
screenBlockingWindow: dependencies[singleton: .screenLock].window,
backgroundWindowLevel: .background
)
ScreenLockUI.shared.startObserving()
}
},
migrationProgressChanged: { [weak self] progress, minEstimatedTotalTime in

@ -1,122 +1,79 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import LocalAuthentication
import SessionUIKit
import SessionMessagingKit
import SessionUtilitiesKit
import SignalUtilitiesKit
class ScreenLockUI {
public static let shared: ScreenLockUI = ScreenLockUI()
private var dependencies: Dependencies?
public lazy var screenBlockingWindow: UIWindow = {
let result: UIWindow = UIWindow()
result.isHidden = false
result.windowLevel = .background
result.isOpaque = true
result.themeBackgroundColorForced = .theme(.classicDark, color: .backgroundPrimary)
result.rootViewController = self.screenBlockingViewController
return result
}()
private lazy var screenBlockingViewController: ScreenLockViewController = {
let result: ScreenLockViewController = ScreenLockViewController { [weak self] in
guard self?.appIsInactiveOrBackground == false else {
// This button can be pressed while the app is inactive
// for a brief window while the iOS auth UI is dismissing.
return
}
// MARK: - Singleton
Log.info("unlockButtonWasTapped")
public extension Singleton {
static let screenLock: SingletonConfig<ScreenLockWindow> = Dependencies.create(
identifier: "screenLock",
createInstance: { dependencies in ScreenLockWindow(using: dependencies) }
)
}
self?.didLastUnlockAttemptFail = false
self?.ensureUI()
}
return result
}()
/// Obscures the app screen:
///
/// * In the app switcher.
/// * During 'Screen Lock' unlock process.
public class ScreenLockWindow {
private let dependencies: Dependencies
/// 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.
/// Indicates whether or not the user is currently locked out of the app. Should only be set if `db[.isScreenLockEnabled]`.
///
/// Secondly, we need to show the screen protection _before_ we become inactive in order for it to be reflected in the
/// app switcher.
private var appIsInactiveOrBackground: Bool = false {
didSet {
if self.appIsInactiveOrBackground {
if !self.isShowingScreenLockUI {
self.didLastUnlockAttemptFail = false
self.tryToActivateScreenLockBasedOnCountdown()
}
}
else if !self.didUnlockJustSucceed {
self.tryToActivateScreenLockBasedOnCountdown()
}
self.didUnlockJustSucceed = false
self.ensureUI()
}
}
private var appIsInBackground: Bool = false {
didSet {
self.didUnlockJustSucceed = false
self.tryToActivateScreenLockBasedOnCountdown()
self.ensureUI()
}
}
/// * The user is locked out by default on app launch.
/// * The user is also locked out if the app is sent to the background
@ThreadSafe private var isScreenLockLocked: Bool = false
private var isShowingScreenLockUI: Bool = false
private var didUnlockJustSucceed: Bool = false
private var didLastUnlockAttemptFail: Bool = false
/// We want to remain in "screen lock" mode while "local auth" UI is dismissing. So we lazily clear isShowingScreenLockUI
/// using this property.
private var shouldClearAuthUIWhenActive: Bool = false
/// Indicates whether or not the user is currently locked out of the app. Should only be set if db[.isScreenLockEnabled].
///
/// * The user is locked out by default on app launch.
/// * The user is also locked out if the app is sent to the background
@ThreadSafe private var isScreenLockLocked: Bool = false
// Determines what the state of the app should be.
private var desiredUIState: ScreenLockViewController.State {
if isScreenLockLocked {
if appIsInactiveOrBackground {
Log.verbose("desiredUIState: screen protection 1.")
return .protection
}
Log.verbose("desiredUIState: screen lock 2.")
return (isShowingScreenLockUI ? .protection : .lock)
}
if !self.appIsInactiveOrBackground {
// App is inactive or background.
Log.verbose("desiredUIState: none 3.");
return .none;
}
// MARK: - UI
public lazy var window: UIWindow = {
let result: UIWindow = UIWindow()
result.isHidden = false
result.windowLevel = .background
result.isOpaque = true
result.themeBackgroundColorForced = .theme(.classicDark, color: .backgroundPrimary)
result.rootViewController = self.viewController
if SessionEnvironment.shared?.isRequestingPermission == true {
return .none;
return result
}()
private lazy var viewController: ScreenLockViewController = ScreenLockViewController { [weak self, dependencies] in
guard dependencies[singleton: .appContext].isAppForegroundAndActive else {
// This button can be pressed while the app is inactive
// for a brief window while the iOS auth UI is dismissing.
return
}
Log.verbose("desiredUIState: screen protection 4.")
return .protection;
Log.info(.screenLock, "unlockButtonWasTapped")
self?.didLastUnlockAttemptFail = false
self?.ensureUI()
}
// MARK: - Lifecycle
init(using dependencies: Dependencies) {
self.dependencies = dependencies
}
deinit {
NotificationCenter.default.removeObserver(self)
}
// MARK: - Observations
private func observeNotifications() {
NotificationCenter.default.addObserver(
self,
@ -150,56 +107,72 @@ class ScreenLockUI {
)
}
public func setupWithRootWindow(rootWindow: UIWindow, using dependencies: Dependencies) {
self.dependencies = dependencies
self.screenBlockingWindow.frame = rootWindow.bounds
}
public func startObserving() {
self.appIsInactiveOrBackground = (UIApplication.shared.applicationState != .active)
public func setupWithRootWindow(rootWindow: UIWindow) {
self.window.frame = rootWindow.bounds
self.observeNotifications()
// Hide the screen blocking window until "app is ready" to
// avoid blocking the loading view.
/// Hide the screen blocking window until "app is ready" to avoid blocking the loading view
updateScreenBlockingWindow(state: .none, animated: false)
// Initialize the screen lock state.
//
// It's not safe to access OWSScreenLock.isScreenLockEnabled
// until the app is ready.
dependencies?[singleton: .appReadiness].runNowOrWhenAppWillBecomeReady { [weak self, dependencies] in
DispatchQueue.global(qos: .background).async {
self?.isScreenLockLocked = (dependencies?[singleton: .storage, key: .isScreenLockEnabled] == true)
DispatchQueue.main.async {
self?.ensureUI()
}
/// Initialize the screen lock state.
///
/// It's not safe to access `isScreenLockEnabled` in `storage` until the app is ready
dependencies[singleton: .appReadiness].runNowOrWhenAppWillBecomeReady { [weak self, dependencies] in
self?.isScreenLockLocked = (dependencies[singleton: .storage, key: .isScreenLockEnabled] == true)
switch Thread.isMainThread {
case true: self?.ensureUI()
case false: DispatchQueue.main.async { self?.ensureUI() }
}
}
}
// MARK: - Functions
private func determineDesiredUIState() -> ScreenLockViewController.State {
if isScreenLockLocked {
if dependencies[singleton: .appContext].isNotInForeground {
Log.verbose(.screenLock, "App not in foreground, desiredUIState is: protection.")
return .protection
}
Log.verbose(.screenLock, "App in foreground and locked, desiredUIState is: \(isShowingScreenLockUI ? "protection" : "lock").")
return (isShowingScreenLockUI ? .protection : .lock)
}
if dependencies[singleton: .appContext].isAppForegroundAndActive {
// App is inactive or background.
Log.verbose(.screenLock, "App in foreground and not locked, desiredUIState is: none.")
return .none;
}
if SessionEnvironment.shared?.isRequestingPermission == true {
Log.verbose(.screenLock, "App requesting permissions and not locked, desiredUIState is: none.")
return .none;
}
Log.verbose(.screenLock, "desiredUIState is: protection.")
return .protection;
}
private func tryToActivateScreenLockBasedOnCountdown() {
guard dependencies?[singleton: .appReadiness].isAppReady == true else {
// 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`.
Log.verbose("tryToActivateScreenLockUponBecomingActive NO 0")
guard dependencies[singleton: .appReadiness].isAppReady else {
/// It's not safe to access `isScreenLockEnabled` in `storage` until the app is ready
///
/// We don't need to try to lock the screen lock;
/// It will be initialized by `setupWithRootWindow`
Log.verbose(.screenLock, "tryToActivateScreenLockUponBecomingActive NO 0")
return
}
guard dependencies?[singleton: .storage, key: .isScreenLockEnabled] == true else {
// Screen lock is not enabled.
Log.verbose("tryToActivateScreenLockUponBecomingActive NO 1")
return;
guard dependencies[singleton: .storage, key: .isScreenLockEnabled] else {
/// Screen lock is not enabled.
Log.verbose(.screenLock, "tryToActivateScreenLockUponBecomingActive NO 1")
return
}
guard !isScreenLockLocked else {
// Screen lock is already activated.
Log.verbose("tryToActivateScreenLockUponBecomingActive NO 2")
return;
/// Screen lock is already activated.
Log.verbose(.screenLock, "tryToActivateScreenLockUponBecomingActive NO 2")
return
}
self.isScreenLockLocked = true
@ -210,49 +183,52 @@ class ScreenLockUI {
/// * The blocking window has the correct state.
/// * That we show the "iOS auth UI to unlock" if necessary.
private func ensureUI() {
guard dependencies?[singleton: .appReadiness].isAppReady == true else {
dependencies?[singleton: .appReadiness].runNowOrWhenAppWillBecomeReady { [weak self] in
guard dependencies[singleton: .appReadiness].isAppReady else {
dependencies[singleton: .appReadiness].runNowOrWhenAppWillBecomeReady { [weak self] in
self?.ensureUI()
}
return
}
let desiredUIState: ScreenLockViewController.State = self.desiredUIState
Log.verbose("ensureUI: \(desiredUIState)")
let desiredUIState: ScreenLockViewController.State = determineDesiredUIState()
Log.verbose(.screenLock, "ensureUI: \(desiredUIState)")
// Show the "iOS auth UI to unlock" if necessary.
/// Show the "iOS auth UI to unlock" if necessary.
if desiredUIState == .lock && !didLastUnlockAttemptFail {
tryToPresentAuthUIToUnlockScreenLock()
}
// Note: We want to regenerate the 'desiredUIState' as if we are about to show the
// 'unlock screen' UI then we shouldn't show the "unlock" button
updateScreenBlockingWindow(state: self.desiredUIState, animated: true)
/// Note: We want to regenerate the `desiredUIState` as if we are about to show the "unlock screen" UI then we
/// shouldn't show the "unlock" button
updateScreenBlockingWindow(state: determineDesiredUIState(), animated: true)
}
private func tryToPresentAuthUIToUnlockScreenLock() {
guard !isShowingScreenLockUI else { return } // We're already showing the auth UI; abort
guard !appIsInactiveOrBackground else { return } // Never show the auth UI unless active
/// If we're already showing the auth UI; or the app isn't active then don't do anything
guard
!isShowingScreenLockUI,
dependencies[singleton: .appContext].isAppForegroundAndActive
else { return }
Log.info("try to unlock screen lock")
Log.info(.screenLock, "Try to unlock screen lock")
isShowingScreenLockUI = true
ScreenLock.shared.tryToUnlockScreenLock(
ScreenLock.tryToUnlockScreenLock(
success: { [weak self] in
Log.info("unlock screen lock succeeded.")
Log.info(.screenLock, "Unlock screen lock succeeded")
self?.isShowingScreenLockUI = false
self?.isScreenLockLocked = false
self?.didUnlockJustSucceed = true
self?.ensureUI()
},
failure: { [weak self] error in
Log.info("unlock screen lock failed.")
Log.info(.screenLock, "Unlock screen lock failed")
self?.clearAuthUIWhenActive()
self?.didLastUnlockAttemptFail = true
self?.showScreenLockFailureAlert(message: "\(error)")
},
unexpectedFailure: { [weak self] error in
Log.info("unlock screen lock unexpectedly failed.")
Log.warn(.screenLock, "Unlock screen lock unexpectedly failed")
// Local Authentication isn't working properly.
// This isn't covered by the docs or the forums but in practice
@ -262,7 +238,7 @@ class ScreenLockUI {
}
},
cancel: { [weak self] in
Log.info("unlock screen lock cancelled.")
Log.info(.screenLock, "Unlock screen lock cancelled")
self?.clearAuthUIWhenActive()
self?.didLastUnlockAttemptFail = true
@ -277,7 +253,7 @@ class ScreenLockUI {
private func showScreenLockFailureAlert(message: String) {
let modal: ConfirmationModal = ConfirmationModal(
targetView: screenBlockingWindow.rootViewController?.view,
targetView: viewController.view,
info: ConfirmationModal.Info(
title: "authenticateFailed".localized(),
body: .text(message),
@ -286,36 +262,7 @@ class ScreenLockUI {
afterClosed: { [weak self] in self?.ensureUI() } // After the alert, update the UI
)
)
screenBlockingWindow.rootViewController?.present(modal, animated: true)
}
/// 'Screen Blocking' window obscures the app screen:
///
/// * In the app switcher.
/// * During 'Screen Lock' unlock process.
private func createScreenBlockingWindow(rootWindow: UIWindow) {
let window: UIWindow = UIWindow(frame: rootWindow.bounds)
window.isHidden = false
window.windowLevel = .background
window.isOpaque = true
window.themeBackgroundColorForced = .theme(.classicDark, color: .backgroundPrimary)
let viewController: ScreenLockViewController = ScreenLockViewController { [weak self] in
guard self?.appIsInactiveOrBackground == false else {
// This button can be pressed while the app is inactive
// for a brief window while the iOS auth UI is dismissing.
return
}
Log.info("unlockButtonWasTapped")
self?.didLastUnlockAttemptFail = false
self?.ensureUI()
}
window.rootViewController = viewController
self.screenBlockingWindow = window
self.screenBlockingViewController = viewController
viewController.present(modal, animated: true)
}
/// The "screen blocking" window has three possible states:
@ -327,7 +274,7 @@ class ScreenLockUI {
let shouldShowBlockWindow: Bool = (state != .none)
OWSWindowManager.shared().isScreenBlockActive = shouldShowBlockWindow
self.screenBlockingViewController.updateUI(state: state, animated: animated)
self.viewController.updateUI(state: state, animated: animated)
}
// MARK: - Events
@ -335,7 +282,7 @@ class ScreenLockUI {
private func clearAuthUIWhenActive() {
// For continuity, continue to present blocking screen in "screen lock" mode while
// dismissing the "local auth UI".
if self.appIsInactiveOrBackground {
if !dependencies[singleton: .appContext].isAppForegroundAndActive {
self.shouldClearAuthUIWhenActive = true
}
else {
@ -345,42 +292,61 @@ class ScreenLockUI {
}
@objc private func applicationDidBecomeActive() {
if self.shouldClearAuthUIWhenActive {
self.shouldClearAuthUIWhenActive = false
self.isShowingScreenLockUI = false
if shouldClearAuthUIWhenActive {
shouldClearAuthUIWhenActive = false
isShowingScreenLockUI = false
}
if !didUnlockJustSucceed {
tryToActivateScreenLockBasedOnCountdown()
}
self.appIsInactiveOrBackground = false
didUnlockJustSucceed = false
ensureUI()
}
/// When the OS shows the TouchID/FaceID/Pin UI the application will resign active (and we don't want to re-authenticate if we are
/// already locked)
///
/// Secondly, we need to show the screen protection _before_ we become inactive in order for it to be reflected in the app switcher
@objc private func applicationWillResignActive() {
self.appIsInactiveOrBackground = true
if !isShowingScreenLockUI {
didLastUnlockAttemptFail = false
tryToActivateScreenLockBasedOnCountdown()
}
didUnlockJustSucceed = false
ensureUI()
}
@objc private func applicationWillEnterForeground() {
self.appIsInBackground = false
didUnlockJustSucceed = false
tryToActivateScreenLockBasedOnCountdown()
ensureUI()
}
@objc private func applicationDidEnterBackground() {
self.appIsInBackground = true
didUnlockJustSucceed = false
tryToActivateScreenLockBasedOnCountdown()
ensureUI()
}
/// Whenever the device date/time is edited by the user, trigger screen lock immediately if enabled.
@objc private func clockDidChange() {
Log.info("clock did change")
Log.info(.screenLock, "clock did change")
guard dependencies?[singleton: .appReadiness].isAppReady == true else {
guard dependencies[singleton: .appReadiness].isAppReady == true else {
// 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`.
Log.verbose("clockDidChange 0")
Log.verbose(.screenLock, "clockDidChange 0")
return;
}
DispatchQueue.global(qos: .background).async { [dependencies] in
self.isScreenLockLocked = (dependencies?[singleton: .storage, key: .isScreenLockEnabled] == true)
self.isScreenLockLocked = (dependencies[singleton: .storage, key: .isScreenLockEnabled] == true)
DispatchQueue.main.async {
// NOTE: this notifications fires _before_ applicationDidBecomeActive,

@ -107,7 +107,7 @@ final class SAEScreenLockViewController: ScreenLockViewController {
isShowingAuthUI = true
ScreenLock.shared.tryToUnlockScreenLock(
ScreenLock.tryToUnlockScreenLock(
success: { [weak self] in
Log.assertOnMainThread()
Log.info("unlock screen lock succeeded.")

@ -1,7 +1,6 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import SessionUIKit
open class ScreenLockViewController: UIViewController {
public enum State {
@ -36,7 +35,7 @@ open class ScreenLockViewController: UIViewController {
public lazy var unlockButton: SessionButton = {
let result: SessionButton = SessionButton(style: .bordered, size: .medium)
result.translatesAutoresizingMaskIntoConstraints = false
result.setTitle("lockAppUnlock".localized(), for: .normal)
result.setTitle("lockAppUnlock".localizedSNUIKit(), for: .normal)
result.addTarget(self, action: #selector(showUnlockUI), for: .touchUpInside)
result.isHidden = true

@ -1,10 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import UIKit
import LocalAuthentication
import SessionMessagingKit
import SessionUtilitiesKit
// MARK: - Log.Category
@ -14,7 +11,17 @@ public extension Log.Category {
// MARK: - ScreenLock
public class ScreenLock {
public enum ScreenLock {
public static let screenLockTimeoutDefault = (15 * 60)
public static let screenLockTimeouts = [
1 * 60,
5 * 60,
15 * 60,
30 * 60,
1 * 60 * 60,
0
]
public enum ScreenLockError: Error {
case general(description: String)
}
@ -26,18 +33,6 @@ public class ScreenLock {
case unexpectedFailure(error: String)
}
public let screenLockTimeoutDefault = (15 * 60)
public let screenLockTimeouts = [
1 * 60,
5 * 60,
15 * 60,
30 * 60,
1 * 60 * 60,
0
]
public static let shared: ScreenLock = ScreenLock()
// MARK: - Methods
/// This method should only be called:
@ -48,7 +43,7 @@ public class ScreenLock {
///
/// * Asynchronously.
/// * On the main thread.
public func tryToUnlockScreenLock(
public static func tryToUnlockScreenLock(
success: @escaping (() -> Void),
failure: @escaping ((Error) -> Void),
unexpectedFailure: @escaping ((Error) -> Void),
@ -57,8 +52,7 @@ public class ScreenLock {
Log.assertOnMainThread()
tryToVerifyLocalAuthentication(
// Description of how and why Signal iOS uses Touch ID/Face ID/Phone Passcode to
// unlock 'screen lock'.
// Description of how and the app uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'.
localizedReason: "authenticateToOpen"
.put(key: "app_name", value: Constants.app_name)
.localized()
@ -93,7 +87,7 @@ public class ScreenLock {
///
/// * Asynchronously.
/// * On the main thread.
private func tryToVerifyLocalAuthentication(
private static func tryToVerifyLocalAuthentication(
localizedReason: String,
completion completionParam: @escaping ((Outcome) -> Void)
) {
@ -155,7 +149,7 @@ public class ScreenLock {
// MARK: - Outcome
private func outcomeForLAError(errorParam: Error?, defaultErrorDescription: String) -> Outcome {
private static func outcomeForLAError(errorParam: Error?, defaultErrorDescription: String) -> Outcome {
if let error = errorParam {
guard let laError = error as? LAError else {
return .failure(error: defaultErrorDescription)
@ -222,7 +216,7 @@ public class ScreenLock {
// MARK: - Context
private func screenLockContext() -> LAContext {
private static func screenLockContext() -> LAContext {
let context = LAContext()
// Never recycle biometric auth.
Loading…
Cancel
Save