session-ios/SignalUtilitiesKit/Screen Lock/OWSScreenLock.swift
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

252 lines
11 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import LocalAuthentication
import SessionMessagingKit
// FIXME: Refactor this once the 'PrivacySettingsTableViewController' and 'OWSScreenLockUI' have been refactored
@objc public class OWSScreenLock: NSObject {
public enum OWSScreenLockOutcome {
case success
case cancel
case failure(error: String)
case unexpectedFailure(error: String)
}
@objc public let screenLockTimeoutDefault = (15 * kMinuteInterval)
@objc public let screenLockTimeouts = [
1 * kMinuteInterval,
5 * kMinuteInterval,
15 * kMinuteInterval,
30 * kMinuteInterval,
1 * kHourInterval,
0
]
@objc public static let ScreenLockDidChange = Notification.Name("ScreenLockDidChange")
// MARK: - Singleton class
@objc(sharedManager)
public static let shared = OWSScreenLock()
private override init() {
super.init()
SwiftSingletons.register(self)
}
// MARK: - Properties
@objc public func isScreenLockEnabled() -> Bool {
return GRDBStorage.shared[.isScreenLockEnabled]
}
@objc
public func setIsScreenLockEnabled(_ value: Bool) {
GRDBStorage.shared.writeAsync(
updates: { db in db[.isScreenLockEnabled] = value },
completion: { _, _ in
NotificationCenter.default.postNotificationNameAsync(OWSScreenLock.ScreenLockDidChange, object: nil)
}
)
}
@objc public func screenLockTimeout() -> TimeInterval {
return GRDBStorage.shared[.screenLockTimeoutSeconds]
.defaulting(to: screenLockTimeoutDefault)
}
@objc public func setScreenLockTimeout(_ value: TimeInterval) {
GRDBStorage.shared.writeAsync(
updates: { db in db[.screenLockTimeoutSeconds] = value },
completion: { _, _ in
NotificationCenter.default.postNotificationNameAsync(OWSScreenLock.ScreenLockDidChange, object: nil)
}
)
}
// MARK: - Methods
// This method should only be called:
//
// * On the main thread.
//
// Exactly one of these completions will be performed:
//
// * Asynchronously.
// * On the main thread.
@objc public func tryToUnlockScreenLock(success: @escaping (() -> Void),
failure: @escaping ((Error) -> Void),
unexpectedFailure: @escaping ((Error) -> Void),
cancel: @escaping (() -> Void)) {
AssertIsOnMainThread()
tryToVerifyLocalAuthentication(localizedReason: NSLocalizedString("SCREEN_LOCK_REASON_UNLOCK_SCREEN_LOCK",
comment: "Description of how and why Signal iOS uses Touch ID/Face ID/Phone Passcode to unlock 'screen lock'."),
completion: { (outcome: OWSScreenLockOutcome) in
AssertIsOnMainThread()
switch outcome {
case .failure(let error):
Logger.error("local authentication failed with error: \(error)")
failure(self.authenticationError(errorDescription: error))
case .unexpectedFailure(let error):
Logger.error("local authentication failed with unexpected error: \(error)")
unexpectedFailure(self.authenticationError(errorDescription: error))
case .success:
Logger.verbose("local authentication succeeded.")
success()
case .cancel:
Logger.verbose("local authentication cancelled.")
cancel()
}
})
}
// This method should only be called:
//
// * On the main thread.
//
// completionParam will be performed:
//
// * Asynchronously.
// * On the main thread.
private func tryToVerifyLocalAuthentication(localizedReason: String,
completion completionParam: @escaping ((OWSScreenLockOutcome) -> Void)) {
AssertIsOnMainThread()
let defaultErrorDescription = NSLocalizedString("SCREEN_LOCK_ENABLE_UNKNOWN_ERROR",
comment: "Indicates that an unknown error occurred while using Touch ID/Face ID/Phone Passcode.")
// Ensure completion is always called on the main thread.
let completion = { (outcome: OWSScreenLockOutcome) in
DispatchQueue.main.async {
completionParam(outcome)
}
}
let context = screenLockContext()
var authError: NSError?
let canEvaluatePolicy = context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &authError)
if !canEvaluatePolicy || authError != nil {
Logger.error("could not determine if local authentication is supported: \(String(describing: authError))")
let outcome = self.outcomeForLAError(errorParam: authError,
defaultErrorDescription: defaultErrorDescription)
switch outcome {
case .success:
owsFailDebug("local authentication unexpected success")
completion(.failure(error:defaultErrorDescription))
case .cancel, .failure, .unexpectedFailure:
completion(outcome)
}
return
}
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: localizedReason) { success, evaluateError in
if success {
Logger.info("local authentication succeeded.")
completion(.success)
} else {
let outcome = self.outcomeForLAError(errorParam: evaluateError,
defaultErrorDescription: defaultErrorDescription)
switch outcome {
case .success:
owsFailDebug("local authentication unexpected success")
completion(.failure(error:defaultErrorDescription))
case .cancel, .failure, .unexpectedFailure:
completion(outcome)
}
}
}
}
// MARK: - Outcome
private func outcomeForLAError(errorParam: Error?, defaultErrorDescription: String) -> OWSScreenLockOutcome {
if let error = errorParam {
guard let laError = error as? LAError else {
return .failure(error:defaultErrorDescription)
}
if #available(iOS 11.0, *) {
switch laError.code {
case .biometryNotAvailable:
Logger.error("local authentication error: biometryNotAvailable.")
return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_AVAILABLE",
comment: "Indicates that Touch ID/Face ID/Phone Passcode are not available on this device."))
case .biometryNotEnrolled:
Logger.error("local authentication error: biometryNotEnrolled.")
return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_ENROLLED",
comment: "Indicates that Touch ID/Face ID/Phone Passcode is not configured on this device."))
case .biometryLockout:
Logger.error("local authentication error: biometryLockout.")
return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_LOCKOUT",
comment: "Indicates that Touch ID/Face ID/Phone Passcode is 'locked out' on this device due to authentication failures."))
default:
// Fall through to second switch
break
}
}
switch laError.code {
case .authenticationFailed:
Logger.error("local authentication error: authenticationFailed.")
return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_FAILED",
comment: "Indicates that Touch ID/Face ID/Phone Passcode authentication failed."))
case .userCancel, .userFallback, .systemCancel, .appCancel:
Logger.info("local authentication cancelled.")
return .cancel
case .passcodeNotSet:
Logger.error("local authentication error: passcodeNotSet.")
return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_PASSCODE_NOT_SET",
comment: "Indicates that Touch ID/Face ID/Phone Passcode passcode is not set."))
case .touchIDNotAvailable:
Logger.error("local authentication error: touchIDNotAvailable.")
return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_AVAILABLE",
comment: "Indicates that Touch ID/Face ID/Phone Passcode are not available on this device."))
case .touchIDNotEnrolled:
Logger.error("local authentication error: touchIDNotEnrolled.")
return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_NOT_ENROLLED",
comment: "Indicates that Touch ID/Face ID/Phone Passcode is not configured on this device."))
case .touchIDLockout:
Logger.error("local authentication error: touchIDLockout.")
return .failure(error: NSLocalizedString("SCREEN_LOCK_ERROR_LOCAL_AUTHENTICATION_LOCKOUT",
comment: "Indicates that Touch ID/Face ID/Phone Passcode is 'locked out' on this device due to authentication failures."))
case .invalidContext:
owsFailDebug("context not valid.")
return .unexpectedFailure(error:defaultErrorDescription)
case .notInteractive:
owsFailDebug("context not interactive.")
return .unexpectedFailure(error:defaultErrorDescription)
}
}
return .failure(error:defaultErrorDescription)
}
private func authenticationError(errorDescription: String) -> Error {
return OWSErrorWithCodeDescription(.localAuthenticationError,
errorDescription)
}
// MARK: - Context
private func screenLockContext() -> LAContext {
let context = LAContext()
// Never recycle biometric auth.
context.touchIDAuthenticationAllowableReuseDuration = TimeInterval(0)
if #available(iOS 11.0, *) {
assert(!context.interactionNotAllowed)
}
return context
}
}