diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 5dacdb83c..f5f2846bc 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -311,7 +311,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 */; }; FD66CB2A2BF3449B00268FAB /* SessionSnodeKit.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */; }; FD6A38E92C2A630E00762359 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38E82C2A630E00762359 /* CocoaLumberjackSwift */; }; FD6A38EC2C2A63B500762359 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38EB2C2A63B500762359 /* KeychainSwift */; }; @@ -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 = ""; }; FD52090428B4915F006098F6 /* PrivacySettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivacySettingsViewModel.swift; sourceTree = ""; }; FD52090628B49738006098F6 /* ConfirmationModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationModal.swift; sourceTree = ""; }; - FD52090828B59411006098F6 /* ScreenLockUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockUI.swift; sourceTree = ""; }; - FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = ""; }; + FD52090828B59411006098F6 /* ScreenLockWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockWindow.swift; sourceTree = ""; }; FD559DF42A7368CB00C7C62A /* DispatchQueue+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DispatchQueue+Utilities.swift"; sourceTree = ""; }; FD5931A62A8DA5DA0040147D /* SQLInterpolation+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SQLInterpolation+Utilities.swift"; sourceTree = ""; }; FD5931AA2A8DCB0A0040147D /* ScopeAdapter+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ScopeAdapter+Utilities.swift"; sourceTree = ""; }; @@ -1918,6 +1916,8 @@ FD61FCF82D308CC5005752DE /* GroupMemberSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberSpec.swift; sourceTree = ""; }; FD61FCFA2D34A5DE005752DE /* _007_SplitSnodeReceivedMessageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _007_SplitSnodeReceivedMessageInfo.swift; sourceTree = ""; }; FD6531892AA025C500DFEEAA /* TestDependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDependencies.swift; sourceTree = ""; }; + FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLockViewController.swift; sourceTree = ""; }; + FD6673FE2D77F9BE00041530 /* ScreenLock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenLock.swift; sourceTree = ""; }; FD66CB272BF3449B00268FAB /* SessionSnodeKit.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SessionSnodeKit.xctestplan; sourceTree = ""; }; FD66CB2B2BF344C600268FAB /* SessionMessageKit.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = SessionMessageKit.xctestplan; sourceTree = ""; }; FD6A38F02C2A66B100762359 /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = ""; }; @@ -2826,6 +2826,7 @@ C35D0DB425AE5F1200B6BF49 /* UIEdgeInsets.swift */, FD848B9228420164000E298B /* UnicodeScalar+Utilities.swift */, C38EF2EF255B6DBB007E1867 /* Weak.swift */, + FD6673FE2D77F9BE00041530 /* ScreenLock.swift */, FD5D201D27B0D87C00FEA984 /* SessionId.swift */, 7B7CB191271508AD0079FF93 /* CallRingTonePlayer.swift */, 7B0EFDED274F598600FFAAE7 /* TimestampUtils.swift */, @@ -2864,7 +2865,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 */, @@ -3122,6 +3123,7 @@ FD52090628B49738006098F6 /* ConfirmationModal.swift */, C38EF2A3255B6D93007E1867 /* PlaceholderIcon.swift */, C38EF2A4255B6D93007E1867 /* ProfilePictureView.swift */, + FD6673FC2D77F54400041530 /* ScreenLockViewController.swift */, FD0150572CA27DEE005B08A1 /* ScrollableLabel.swift */, FD71165A28E6DDBC00B47552 /* StyledNavigationController.swift */, FD0B77AF29B69A65009169BA /* TopBannerController.swift */, @@ -3136,7 +3138,6 @@ children = ( C33FD9B7255A54A300E217F9 /* Meta */, C36096ED25AD20FD008B62B2 /* Media Viewing & Editing */, - C36096EE25AD21BC008B62B2 /* Screen Lock */, C3851CD225624B060061EEB0 /* Shared Views */, C360970125AD22D3008B62B2 /* Shared View Controllers */, C3CA3B11255CF17200F4C6D4 /* Utilities */, @@ -3320,15 +3321,6 @@ path = "Media Viewing & Editing"; sourceTree = ""; }; - C36096EE25AD21BC008B62B2 /* Screen Lock */ = { - isa = PBXGroup; - children = ( - C38EF2E2255B6DB9007E1867 /* ScreenLock.swift */, - FD52090A28B59BB4006098F6 /* ScreenLockViewController.swift */, - ); - path = "Screen Lock"; - sourceTree = ""; - }; C360970125AD22D3008B62B2 /* Shared View Controllers */ = { isa = PBXGroup; children = ( @@ -5791,6 +5783,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 */, @@ -5826,7 +5819,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 */, @@ -5851,7 +5843,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 */, @@ -6007,6 +5998,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 */, @@ -6319,7 +6311,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 */, @@ -6430,6 +6421,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 */, diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index e39048605..eb5605ec0 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -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 diff --git a/Session/Shared/ScreenLockUI.swift b/Session/Shared/ScreenLockWindow.swift similarity index 50% rename from Session/Shared/ScreenLockUI.swift rename to Session/Shared/ScreenLockWindow.swift index 6d8ca7ed5..0d3979903 100644 --- a/Session/Shared/ScreenLockUI.swift +++ b/Session/Shared/ScreenLockWindow.swift @@ -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 = 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, diff --git a/SessionShareExtension/SAEScreenLockViewController.swift b/SessionShareExtension/SAEScreenLockViewController.swift index 171c05dba..81d970a69 100644 --- a/SessionShareExtension/SAEScreenLockViewController.swift +++ b/SessionShareExtension/SAEScreenLockViewController.swift @@ -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.") diff --git a/SignalUtilitiesKit/Screen Lock/ScreenLockViewController.swift b/SessionUIKit/Components/ScreenLockViewController.swift similarity index 96% rename from SignalUtilitiesKit/Screen Lock/ScreenLockViewController.swift rename to SessionUIKit/Components/ScreenLockViewController.swift index 26f215092..39b6ba851 100644 --- a/SignalUtilitiesKit/Screen Lock/ScreenLockViewController.swift +++ b/SessionUIKit/Components/ScreenLockViewController.swift @@ -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 diff --git a/SignalUtilitiesKit/Screen Lock/ScreenLock.swift b/SessionUtilitiesKit/General/ScreenLock.swift similarity index 91% rename from SignalUtilitiesKit/Screen Lock/ScreenLock.swift rename to SessionUtilitiesKit/General/ScreenLock.swift index 230b37d28..edc9b930c 100644 --- a/SignalUtilitiesKit/Screen Lock/ScreenLock.swift +++ b/SessionUtilitiesKit/General/ScreenLock.swift @@ -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.