import Foundation import UIKit import Postbox import Display import SwiftSignalKit import MonotonicTime import AccountContext import TelegramPresentationData import PasscodeUI import TelegramUIPreferences import ImageBlur import FastBlur import AppLockState private func isLocked(passcodeSettings: PresentationPasscodeSettings, state: LockState, isApplicationActive: Bool) -> Bool { if state.isManuallyLocked { return true } else if let autolockTimeout = passcodeSettings.autolockTimeout { var bootTimestamp: Int32 = 0 let uptime = getDeviceUptimeSeconds(&bootTimestamp) let timestamp = MonotonicTimestamp(bootTimestamp: bootTimestamp, uptime: uptime) let applicationActivityTimestamp = state.applicationActivityTimestamp if let applicationActivityTimestamp = applicationActivityTimestamp { if timestamp.bootTimestamp != applicationActivityTimestamp.bootTimestamp { return true } if timestamp.uptime >= applicationActivityTimestamp.uptime + autolockTimeout { return true } } else { return true } } return false } private func getCoveringViewSnaphot(window: Window1) -> UIImage? { let scale: CGFloat = 0.5 let unscaledSize = window.hostView.containerView.frame.size return generateImage(CGSize(width: floor(unscaledSize.width * scale), height: floor(unscaledSize.height * scale)), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.scaleBy(x: scale, y: scale) UIGraphicsPushContext(context) window.forEachViewController({ controller in if let controller = controller as? PasscodeEntryController { controller.displayNode.alpha = 0.0 } return true }) window.hostView.containerView.drawHierarchy(in: CGRect(origin: CGPoint(), size: unscaledSize), afterScreenUpdates: false) window.forEachViewController({ controller in if let controller = controller as? PasscodeEntryController { controller.displayNode.alpha = 1.0 } return true }) UIGraphicsPopContext() }).flatMap(applyScreenshotEffectToImage) } public final class AppLockContextImpl: AppLockContext { private let rootPath: String private let syncQueue = Queue() private let applicationBindings: TelegramApplicationBindings private let accountManager: AccountManager private let presentationDataSignal: Signal private let window: Window1? private let rootController: UIViewController? private var coveringView: LockedWindowCoveringView? private var passcodeController: PasscodeEntryController? private var timestampRenewTimer: SwiftSignalKit.Timer? private var currentStateValue: LockState private let currentState = Promise() private let autolockTimeout = ValuePromise(nil, ignoreRepeated: true) private let autolockReportTimeout = ValuePromise(nil, ignoreRepeated: true) private let isCurrentlyLockedPromise = Promise() public var isCurrentlyLocked: Signal { return self.isCurrentlyLockedPromise.get() |> distinctUntilChanged } private var lastActiveTimestamp: Double? private var lastActiveValue: Bool = false public init(rootPath: String, window: Window1?, rootController: UIViewController?, applicationBindings: TelegramApplicationBindings, accountManager: AccountManager, presentationDataSignal: Signal, lockIconInitialFrame: @escaping () -> CGRect?) { assert(Queue.mainQueue().isCurrent()) self.applicationBindings = applicationBindings self.accountManager = accountManager self.presentationDataSignal = presentationDataSignal self.rootPath = rootPath self.window = window self.rootController = rootController if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: self.rootPath))), let current = try? JSONDecoder().decode(LockState.self, from: data) { self.currentStateValue = current } else { self.currentStateValue = LockState() } self.autolockTimeout.set(self.currentStateValue.autolockTimeout) let _ = (combineLatest(queue: .mainQueue(), accountManager.accessChallengeData(), accountManager.sharedData(keys: Set([ApplicationSpecificSharedDataKeys.presentationPasscodeSettings])), presentationDataSignal, applicationBindings.applicationIsActive, self.currentState.get() ) |> deliverOnMainQueue).start(next: { [weak self] accessChallengeData, sharedData, presentationData, appInForeground, state in guard let strongSelf = self else { return } let passcodeSettings: PresentationPasscodeSettings = sharedData.entries[ApplicationSpecificSharedDataKeys.presentationPasscodeSettings] as? PresentationPasscodeSettings ?? .defaultSettings let timestamp = CFAbsoluteTimeGetCurrent() var becameActiveRecently = false if appInForeground { if !strongSelf.lastActiveValue { strongSelf.lastActiveValue = true strongSelf.lastActiveTimestamp = timestamp } if let lastActiveTimestamp = strongSelf.lastActiveTimestamp { if lastActiveTimestamp + 0.5 > timestamp { becameActiveRecently = true } } } else { strongSelf.lastActiveValue = false } var shouldDisplayCoveringView = false var isCurrentlyLocked = false if !accessChallengeData.data.isLockable { if let passcodeController = strongSelf.passcodeController { strongSelf.passcodeController = nil passcodeController.dismiss() } strongSelf.autolockTimeout.set(nil) strongSelf.autolockReportTimeout.set(nil) } else { if let autolockTimeout = passcodeSettings.autolockTimeout, !appInForeground { shouldDisplayCoveringView = true } if !appInForeground { if let autolockTimeout = passcodeSettings.autolockTimeout { strongSelf.autolockReportTimeout.set(autolockTimeout) } else if state.isManuallyLocked { strongSelf.autolockReportTimeout.set(1) } else { strongSelf.autolockReportTimeout.set(nil) } } else { strongSelf.autolockReportTimeout.set(nil) } strongSelf.autolockTimeout.set(passcodeSettings.autolockTimeout) if isLocked(passcodeSettings: passcodeSettings, state: state, isApplicationActive: appInForeground) { isCurrentlyLocked = true let biometrics: PasscodeEntryControllerBiometricsMode if passcodeSettings.enableBiometrics { biometrics = .enabled(passcodeSettings.biometricsDomainState) } else { biometrics = .none } if let passcodeController = strongSelf.passcodeController { if becameActiveRecently, case .enabled = biometrics, appInForeground { passcodeController.requestBiometrics() } passcodeController.ensureInputFocused() } else { let passcodeController = PasscodeEntryController(applicationBindings: strongSelf.applicationBindings, accountManager: strongSelf.accountManager, appLockContext: strongSelf, presentationData: presentationData, presentationDataSignal: strongSelf.presentationDataSignal, challengeData: accessChallengeData.data, biometrics: biometrics, arguments: PasscodeEntryControllerPresentationArguments(animated: !becameActiveRecently, lockIconInitialFrame: { [weak self] in if let lockViewFrame = lockIconInitialFrame() { return lockViewFrame } else { return CGRect() } })) if becameActiveRecently, appInForeground { passcodeController.presentationCompleted = { [weak passcodeController] in if case .enabled = biometrics { passcodeController?.requestBiometrics() } passcodeController?.ensureInputFocused() } } passcodeController.presentedOverCoveringView = true passcodeController.isOpaqueWhenInOverlay = true strongSelf.passcodeController = passcodeController if let rootViewController = strongSelf.rootController { if let presentedViewController = rootViewController.presentedViewController as? UIActivityViewController { } else { rootViewController.dismiss(animated: false, completion: nil) } } strongSelf.window?.present(passcodeController, on: .passcode) } } else if let passcodeController = strongSelf.passcodeController { strongSelf.passcodeController = nil passcodeController.dismiss() } } strongSelf.updateTimestampRenewTimer(shouldRun: appInForeground && !isCurrentlyLocked) strongSelf.isCurrentlyLockedPromise.set(.single(!appInForeground || isCurrentlyLocked)) if shouldDisplayCoveringView { if strongSelf.coveringView == nil, let window = strongSelf.window { let coveringView = LockedWindowCoveringView(theme: presentationData.theme) coveringView.updateSnapshot(getCoveringViewSnaphot(window: window)) strongSelf.coveringView = coveringView window.coveringView = coveringView if let rootViewController = strongSelf.rootController { if let presentedViewController = rootViewController.presentedViewController as? UIActivityViewController { } else { rootViewController.dismiss(animated: false, completion: nil) } } } } else { if let coveringView = strongSelf.coveringView { strongSelf.coveringView = nil strongSelf.window?.coveringView = nil } } }) self.currentState.set(.single(self.currentStateValue)) let _ = (self.autolockTimeout.get() |> deliverOnMainQueue).start(next: { [weak self] autolockTimeout in self?.updateLockState { state in var state = state state.autolockTimeout = autolockTimeout return state } }) } private func updateTimestampRenewTimer(shouldRun: Bool) { if shouldRun { if self.timestampRenewTimer == nil { let timestampRenewTimer = SwiftSignalKit.Timer(timeout: 5.0, repeat: true, completion: { [weak self] in guard let strongSelf = self else { return } strongSelf.updateApplicationActivityTimestamp() }, queue: .mainQueue()) self.timestampRenewTimer = timestampRenewTimer timestampRenewTimer.start() } } else { if let timestampRenewTimer = self.timestampRenewTimer { self.timestampRenewTimer = nil timestampRenewTimer.invalidate() } } } private func updateApplicationActivityTimestamp() { self.updateLockState { state in var bootTimestamp: Int32 = 0 let uptime = getDeviceUptimeSeconds(&bootTimestamp) var state = state state.applicationActivityTimestamp = MonotonicTimestamp(bootTimestamp: bootTimestamp, uptime: uptime) return state } } private func updateLockState(_ f: @escaping (LockState) -> LockState) { Queue.mainQueue().async { let updatedState = f(self.currentStateValue) if updatedState != self.currentStateValue { self.currentStateValue = updatedState self.currentState.set(.single(updatedState)) let path = appLockStatePath(rootPath: self.rootPath) self.syncQueue.async { if let data = try? JSONEncoder().encode(updatedState) { let _ = try? data.write(to: URL(fileURLWithPath: path), options: .atomic) } } } } } public var invalidAttempts: Signal { return self.currentState.get() |> map { state in return state.unlockAttemts.flatMap { unlockAttemts in return AccessChallengeAttempts(count: unlockAttemts.count, bootTimestamp: unlockAttemts.timestamp.bootTimestamp, uptime: unlockAttemts.timestamp.uptime) } } } public var autolockDeadline: Signal { return self.autolockReportTimeout.get() |> distinctUntilChanged |> map { value -> Int32? in if let value = value { return Int32(Date().timeIntervalSince1970) + value } else { return nil } } } public func lock() { self.updateLockState { state in var state = state state.isManuallyLocked = true return state } } public func unlock() { self.updateLockState { state in var state = state state.unlockAttemts = nil state.isManuallyLocked = false var bootTimestamp: Int32 = 0 let uptime = getDeviceUptimeSeconds(&bootTimestamp) let timestamp = MonotonicTimestamp(bootTimestamp: bootTimestamp, uptime: uptime) state.applicationActivityTimestamp = timestamp return state } } public func failedUnlockAttempt() { self.updateLockState { state in var state = state var unlockAttemts = state.unlockAttemts ?? UnlockAttempts(count: 0, timestamp: MonotonicTimestamp(bootTimestamp: 0, uptime: 0)) unlockAttemts.count += 1 var bootTimestamp: Int32 = 0 let uptime = getDeviceUptimeSeconds(&bootTimestamp) let timestamp = MonotonicTimestamp(bootTimestamp: bootTimestamp, uptime: uptime) unlockAttemts.timestamp = timestamp state.unlockAttemts = unlockAttemts return state } } }