// // Copyright 2023 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only // private extension UserDefaults { static var app: UserDefaults { CurrentAppContext().appUserDefaults() } static func removeAll() { UserDefaults.standard.removeAll() app.removeAll() } private func removeAll() { owsAssertDebug(CurrentAppContext().isMainApp) for (key, _) in self.dictionaryRepresentation() { self.removeObject(forKey: key) } self.synchronize() } } public enum NotificationType: UInt { case noNameNoPreview = 0 case nameNoPreview = 1 case namePreview = 2 public var displayName: String { switch self { case .namePreview: return OWSLocalizedString("NOTIFICATIONS_SENDER_AND_MESSAGE", comment: "") case .nameNoPreview: return OWSLocalizedString("NOTIFICATIONS_SENDER_ONLY", comment: "") case .noNameNoPreview: return OWSLocalizedString("NOTIFICATIONS_NONE", comment: "") } } } @objc(OWSPreferences) public class Preferences: NSObject { private enum Key: String { case screenSecurity = "Screen Security Key" case notificationPreviewType = "Notification Preview Type Key" case playSoundInForeground = "NotificationSoundInForeground" case lastRecordedPushToken = "LastRecordedPushToken" case lastRecordedVoipToken = "LastRecordedVoipToken" case callsHideIPAddress = "CallsHideIPAddress" case hasDeclinedNoContactsView = "hasDeclinedNoContactsView" case hasGeneratedThumbnails = "OWSPreferencesKeyHasGeneratedThumbnails" case shouldShowUnidentifiedDeliveryIndicators = "OWSPreferencesKeyShouldShowUnidentifiedDeliveryIndicators" case shouldNotifyOfNewAccountKey = "OWSPreferencesKeyShouldNotifyOfNewAccountKey" case iOSUpgradeNagDate = "iOSUpgradeNagDate" case systemCallLogEnabled = "OWSPreferencesKeySystemCallLogEnabled" case wasViewOnceTooltipShown = "OWSPreferencesKeyWasViewOnceTooltipShown" case wasDeleteForEveryoneConfirmationShown = "OWSPreferencesKeyWasDeleteForEveryoneConfirmationShown" case wasBlurTooltipShown = "OWSPreferencesKeyWasBlurTooltipShown" case wasGroupCallTooltipShown = "OWSPreferencesKeyWasGroupCallTooltipShown" case wasGroupCallTooltipShownCount = "OWSPreferencesKeyWasGroupCallTooltipShownCount" // Obsolete // case callKitEnabled = "CallKitEnabled" // case callKitPrivacyEnabled = "CallKitPrivacyEnabled" } private enum UserDefaultsKeys { static let deviceScale = "OWSPreferencesKeyDeviceScale" static let isAudibleErrorLoggingEnabled = "IsAudibleErrorLoggingEnabled" static let enableDebugLog = "Debugging Log Enabled Key" } private static let preferencesCollection = "SignalPreferences" private let keyValueStore = SDSKeyValueStore(collection: Preferences.preferencesCollection) public override init() { super.init() if CurrentAppContext().hasUI { CurrentAppContext().appUserDefaults().set(UIScreen.main.scale, forKey: UserDefaultsKeys.deviceScale) } SwiftSingletons.register(self) } // MARK: Helpers public func removeAllValues() { UserDefaults.removeAll() // We don't need to clear our key-value store; database // storage is cleared otherwise. } private func hasValue(forKey key: Key) -> Bool { let result = databaseStorage.read { transaction in return keyValueStore.hasValue(forKey: key.rawValue, transaction: transaction) } return result } private func removeValue(forKey key: Key) { databaseStorage.write { transaction in keyValueStore.removeValue(forKey: key.rawValue, transaction: transaction) } } private func bool(forKey key: Key, defaultValue: Bool) -> Bool { let result = databaseStorage.read { transaction in keyValueStore.getBool(key.rawValue, defaultValue: defaultValue, transaction: transaction) } return result } private func setBool(_ value: Bool, forKey key: Key) { databaseStorage.write { transaction in setBool(value, forKey: key, tx: transaction) } } private func setBool(_ value: Bool, forKey key: Key, tx: SDSAnyWriteTransaction) { keyValueStore.setBool(value, key: key.rawValue, transaction: tx) } private func uint(forKey key: Key, defaultValue: UInt) -> UInt { let result = databaseStorage.read { transaction in keyValueStore.getUInt(key.rawValue, defaultValue: defaultValue, transaction: transaction) } return result } private func setUInt(_ value: UInt, forKey key: Key) { databaseStorage.write { transaction in keyValueStore.setUInt(value, key: key.rawValue, transaction: transaction) } } private func date(forKey key: Key) -> Date? { let date = databaseStorage.read { transaction in keyValueStore.getDate(key.rawValue, transaction: transaction) } return date } private func setDate(_ value: Date, forKey key: Key) { databaseStorage.write { transaction in keyValueStore.setDate(value, key: key.rawValue, transaction: transaction) } } private func string(forKey key: Key) -> String? { return databaseStorage.read { tx in getString(for: key, tx: tx) } } private func getString(for key: Key, tx: SDSAnyReadTransaction) -> String? { return keyValueStore.getString(key.rawValue, transaction: tx) } private func setString(_ value: String?, forKey key: Key) { databaseStorage.write { tx in setString(value, for: key, tx: tx) } } private func setString(_ value: String?, for key: Key, tx: SDSAnyWriteTransaction) { keyValueStore.setString(value, key: key.rawValue, transaction: tx) } // MARK: Logging public static var isLoggingEnabled: Bool { // See: setIsLoggingEnabled. if let preference = UserDefaults.app.object(forKey: UserDefaultsKeys.enableDebugLog) as? Bool { return preference } return true } public static func setIsLoggingEnabled(_ value: Bool) { owsAssertDebug(CurrentAppContext().isMainApp) // Logging preferences are stored in UserDefaults instead of the database, so that we can (optionally) start // logging before the database is initialized. This is important because sometimes there are problems *with* the // database initialization, and without logging it would be hard to track down. UserDefaults.app.set(value, forKey: UserDefaultsKeys.enableDebugLog) } @objc public static var isAudibleErrorLoggingEnabled: Bool { UserDefaults.app.bool(forKey: UserDefaultsKeys.isAudibleErrorLoggingEnabled) && FeatureFlags.choochoo } public static func setIsAudibleErrorLoggingEnabled(_ value: Bool) { UserDefaults.app.set(value, forKey: UserDefaultsKeys.isAudibleErrorLoggingEnabled) } // MARK: Specific Preferences public var isScreenSecurityEnabled: Bool { bool(forKey: .screenSecurity, defaultValue: false) } public func setIsScreenSecurityEnabled(_ value: Bool) { setBool(value, forKey: .screenSecurity) } public var hasDeclinedNoContactsView: Bool { bool(forKey: .hasDeclinedNoContactsView, defaultValue: false) } public func setHasDeclinedNoContactsView(_ value: Bool) { setBool(value, forKey: .hasDeclinedNoContactsView) } public var hasGeneratedThumbnails: Bool { bool(forKey: .hasGeneratedThumbnails, defaultValue: false) } public func setHasGeneratedThumbnails(_ value: Bool) { setBool(value, forKey: .hasGeneratedThumbnails) } public var iOSUpgradeNagDate: Date? { date(forKey: .iOSUpgradeNagDate) } public func setIOSUpgradeNagDate(_ value: Date) { setDate(value, forKey: .iOSUpgradeNagDate) } @objc public var shouldShowUnidentifiedDeliveryIndicators: Bool { bool(forKey: .shouldShowUnidentifiedDeliveryIndicators, defaultValue: false) } public func shouldShowUnidentifiedDeliveryIndicators(transaction: SDSAnyReadTransaction) -> Bool { keyValueStore.getBool( Key.shouldShowUnidentifiedDeliveryIndicators.rawValue, defaultValue: false, transaction: transaction ) } public func setShouldShowUnidentifiedDeliveryIndicatorsAndSendSyncMessage(_ value: Bool) { setBool(value, forKey: .shouldShowUnidentifiedDeliveryIndicators) SSKEnvironment.shared.syncManager.sendConfigurationSyncMessage() SSKEnvironment.shared.storageServiceManagerRef.recordPendingLocalAccountUpdates() } @objc public func setShouldShowUnidentifiedDeliveryIndicators(_ value: Bool, transaction: SDSAnyWriteTransaction) { keyValueStore.setBool(value, key: Key.shouldShowUnidentifiedDeliveryIndicators.rawValue, transaction: transaction) } public func shouldNotifyOfNewAccounts(transaction: SDSAnyReadTransaction) -> Bool { keyValueStore.getBool(Key.shouldNotifyOfNewAccountKey.rawValue, defaultValue: false, transaction: transaction) } public func setShouldNotifyOfNewAccounts(_ value: Bool, transaction: SDSAnyWriteTransaction) { keyValueStore.setBool(value, key: Key.shouldNotifyOfNewAccountKey.rawValue, transaction: transaction) } public var cachedDeviceScale: CGFloat { guard !CurrentAppContext().hasUI else { return UIScreen.main.scale } guard let cachedValue = CurrentAppContext().appUserDefaults().object(forKey: UserDefaultsKeys.deviceScale) as? CGFloat else { return UIScreen.main.scale } return cachedValue } // MARK: Calls public func isSystemCallLogEnabled(tx: SDSAnyReadTransaction) -> Bool { return keyValueStore.getBool(Key.systemCallLogEnabled.rawValue, defaultValue: true, transaction: tx) } public func setIsSystemCallLogEnabled(_ value: Bool) { setBool(value, forKey: .systemCallLogEnabled) } // Allow callers to connect directly, when desirable, vs. enforcing TURN only proxy connectivity public var doCallsHideIPAddress: Bool { bool(forKey: .callsHideIPAddress, defaultValue: false) } public func setDoCallsHideIPAddress(_ value: Bool) { setBool(value, forKey: .callsHideIPAddress) } // MARK: UI Tooltips public var wasViewOnceTooltipShown: Bool { bool(forKey: .wasViewOnceTooltipShown, defaultValue: false) } public func setWasViewOnceTooltipShown() { setBool(true, forKey: .wasViewOnceTooltipShown) } public func wasGroupCallTooltipShown(withTransaction transaction: SDSAnyReadTransaction) -> Bool { keyValueStore.getBool(Key.wasGroupCallTooltipShown.rawValue, defaultValue: false, transaction: transaction) } public func incrementGroupCallTooltipShownCount() { let currentCount = uint(forKey: .wasGroupCallTooltipShownCount, defaultValue: 0) let incrementedCount = currentCount + 1 // If we have shown the tooltip more than 3 times, don't show it again. if incrementedCount > 3 { databaseStorage.write(block: setWasGroupCallTooltipShown(tx:)) } else { setUInt(incrementedCount, forKey: .wasGroupCallTooltipShownCount) } } public func setWasGroupCallTooltipShown(tx: SDSAnyWriteTransaction) { setBool(true, forKey: .wasGroupCallTooltipShown, tx: tx) } public var wasBlurTooltipShown: Bool { bool(forKey: .wasBlurTooltipShown, defaultValue: false) } public func setWasBlurTooltipShown( ) { setBool(true, forKey: .wasBlurTooltipShown) } public var wasDeleteForEveryoneConfirmationShown: Bool { bool(forKey: .wasDeleteForEveryoneConfirmationShown, defaultValue: false) } public func setWasDeleteForEveryoneConfirmationShown() { setBool(true, forKey: .wasDeleteForEveryoneConfirmationShown) } // MARK: Notification Preferences public var soundInForeground: Bool { bool(forKey: .playSoundInForeground, defaultValue: true) } public func setSoundInForeground(_ value: Bool) { setBool(value, forKey: .playSoundInForeground) } public func notificationPreviewType(tx: SDSAnyReadTransaction) -> NotificationType { let rawValue = keyValueStore.getUInt( Key.notificationPreviewType.rawValue, transaction: tx ) return rawValue.flatMap(NotificationType.init(rawValue:)) ?? .namePreview } public func setNotificationPreviewType(_ value: NotificationType) { setUInt(value.rawValue, forKey: .notificationPreviewType) } // MARK: Push Tokens public var pushToken: String? { string(forKey: .lastRecordedPushToken) } public func getPushToken(tx: SDSAnyReadTransaction) -> String? { return getString(for: .lastRecordedPushToken, tx: tx) } public func setPushToken(_ value: String, tx: SDSAnyWriteTransaction) { setString(value, for: .lastRecordedPushToken, tx: tx) } public var voipToken: String? { string(forKey: .lastRecordedVoipToken) } public func getVoipToken(tx: SDSAnyReadTransaction) -> String? { return getString(for: .lastRecordedVoipToken, tx: tx) } public func setVoipToken(_ value: String?, tx: SDSAnyWriteTransaction) { setString(value, for: .lastRecordedVoipToken, tx: tx) } public func unsetRecordedAPNSTokens() { Logger.warn("Forgetting recorded APNS tokens") removeValue(forKey: .lastRecordedPushToken) removeValue(forKey: .lastRecordedVoipToken) } }