iOS/Sources/Shared/Settings/SettingsStore.swift

433 lines
14 KiB
Swift

import CoreLocation
import CoreMotion
import Foundation
import KeychainAccess
import UIKit
import Version
public class SettingsStore {
let keychain = AppConstants.Keychain
let prefs = UserDefaults(suiteName: AppConstants.AppGroupID)!
/// These will only be posted on the main thread
public static let webViewRelatedSettingDidChange: Notification.Name = .init("webViewRelatedSettingDidChange")
public static let menuRelatedSettingDidChange: Notification.Name = .init("menuRelatedSettingDidChange")
public static let locationRelatedSettingDidChange: Notification.Name = .init("locationRelatedSettingDidChange")
public var pushID: String? {
get {
prefs.string(forKey: "pushID")
}
set {
prefs.setValue(newValue, forKeyPath: "pushID")
}
}
public var integrationDeviceID: String {
let baseString = Current.device.identifierForVendor() ?? deviceID
switch Current.appConfiguration {
case .beta:
return "beta_" + baseString
case .debug:
return "debug_" + baseString
case .fastlaneSnapshot, .release:
return baseString
}
}
public var deviceID: String {
get {
keychain["deviceID"] ?? defaultDeviceID
}
set {
keychain["deviceID"] = newValue
}
}
#if os(iOS)
public var matterLastPreferredNetWorkMacExtendedAddress: String? {
get {
keychain["matterLastPreferredNetWorkMacExtendedAddress"]
}
set {
keychain["matterLastPreferredNetWorkMacExtendedAddress"] = newValue
}
}
public var matterLastPreferredNetWorkActiveOperationalDataset: String? {
get {
keychain["matterLastPreferredNetWorkActiveOperationalDataset"]
}
set {
keychain["matterLastPreferredNetWorkActiveOperationalDataset"] = newValue
}
}
public var matterLastPreferredNetWorkExtendedPANID: String? {
get {
keychain["matterLastPreferredNetWorkExtendedPANID"]
}
set {
keychain["matterLastPreferredNetWorkExtendedPANID"] = newValue
}
}
public func isLocationEnabled(for state: UIApplication.State) -> Bool {
let authorizationStatus: CLAuthorizationStatus
let locationManager = CLLocationManager()
authorizationStatus = locationManager.authorizationStatus
switch authorizationStatus {
case .authorizedAlways:
return true
case .authorizedWhenInUse:
switch state {
case .active, .inactive:
return true
case .background:
return false
@unknown default:
return false
}
case .denied, .notDetermined, .restricted:
return false
@unknown default:
return false
}
}
#endif
public struct PageZoom: CaseIterable, Equatable, CustomStringConvertible {
public let zoom: Int
init?(preference: Int) {
guard Self.allCases.contains(where: { $0.zoom == preference }) else {
// in case one of the options causes problems, removing it from allCases will kill it
Current.Log.info("disregarding zoom preference for \(preference)")
return nil
}
self.zoom = preference
}
init(_ zoom: IntegerLiteralType) {
self.zoom = zoom
}
public var description: String {
let zoomString = String(format: "%d%%", zoom)
if zoom == 100 {
return L10n.SettingsDetails.General.PageZoom.default(zoomString)
} else {
return zoomString
}
}
public var viewScaleValue: String {
String(format: "%.02f", CGFloat(zoom) / 100.0)
}
public static let `default`: PageZoom = .init(100)
public static let allCases: [PageZoom] = [
// similar zooms to Safari, but with nothing above 200%
.init(50), .init(75), .init(85),
.init(100), .init(115), .init(125), .init(150), .init(175),
.init(200),
]
}
public var pageZoom: PageZoom {
get {
if let pageZoom = PageZoom(preference: prefs.integer(forKey: "page_zoom")) {
return pageZoom
} else {
return .default
}
}
set {
precondition(Thread.isMainThread)
prefs.set(newValue.zoom, forKey: "page_zoom")
NotificationCenter.default.post(name: Self.webViewRelatedSettingDidChange, object: nil)
}
}
public var pinchToZoom: Bool {
get {
prefs.bool(forKey: "pinchToZoom")
}
set {
prefs.set(newValue, forKey: "pinchToZoom")
NotificationCenter.default.post(name: Self.webViewRelatedSettingDidChange, object: nil)
}
}
public var restoreLastURL: Bool {
get {
if let value = prefs.object(forKey: "restoreLastURL") as? NSNumber {
return value.boolValue
} else {
return true
}
}
set {
prefs.set(newValue, forKey: "restoreLastURL")
}
}
public var fullScreen: Bool {
get {
prefs.bool(forKey: "fullScreen")
}
set {
prefs.set(newValue, forKey: "fullScreen")
NotificationCenter.default.post(name: Self.webViewRelatedSettingDidChange, object: nil)
}
}
public var periodicUpdateInterval: TimeInterval? {
get {
if prefs.object(forKey: "periodicUpdateInterval") == nil {
return 300.0
} else {
let doubleValue = prefs.double(forKey: "periodicUpdateInterval")
return doubleValue > 0 ? doubleValue : nil
}
}
set {
if let newValue {
precondition(newValue > 0)
prefs.set(newValue, forKey: "periodicUpdateInterval")
} else {
prefs.set(-1, forKey: "periodicUpdateInterval")
}
}
}
public struct Privacy {
public var messaging: Bool
public var crashes: Bool
public var analytics: Bool
public var alerts: Bool
public var updates: Bool
public var updatesIncludeBetas: Bool
static func key(for keyPath: KeyPath<Privacy, Bool>) -> String {
switch keyPath {
case \.messaging: return "messagingEnabled"
case \.crashes: return "crashesEnabled"
case \.analytics: return "analyticsEnabled"
case \.alerts: return "alertsEnabled"
case \.updates: return "updateCheckingEnabled"
case \.updatesIncludeBetas: return "updatesIncludeBetas"
default: return ""
}
}
static func `default`(for keyPath: KeyPath<Privacy, Bool>) -> Bool {
switch keyPath {
case \.messaging: return true
case \.crashes: return false
case \.analytics: return false
case \.alerts: return true
case \.updates: return true
case \.updatesIncludeBetas: return true
default: return false
}
}
}
public var privacy: Privacy {
get {
func boolValue(for keyPath: KeyPath<Privacy, Bool>) -> Bool {
let key = Privacy.key(for: keyPath)
if prefs.object(forKey: key) == nil {
// value never set, use the default for this one
return Privacy.default(for: keyPath)
}
return prefs.bool(forKey: key)
}
return .init(
messaging: boolValue(for: \.messaging),
crashes: boolValue(for: \.crashes),
analytics: boolValue(for: \.analytics),
alerts: boolValue(for: \.alerts),
updates: boolValue(for: \.updates),
updatesIncludeBetas: boolValue(for: \.updatesIncludeBetas)
)
}
set {
prefs.set(newValue.messaging, forKey: Privacy.key(for: \.messaging))
prefs.set(newValue.crashes, forKey: Privacy.key(for: \.crashes))
prefs.set(newValue.analytics, forKey: Privacy.key(for: \.analytics))
prefs.set(newValue.alerts, forKey: Privacy.key(for: \.alerts))
prefs.set(newValue.updates, forKey: Privacy.key(for: \.updates))
prefs.set(newValue.updatesIncludeBetas, forKey: Privacy.key(for: \.updatesIncludeBetas))
Current.Log.info("privacy updated to \(newValue)")
}
}
public enum LocationVisibility: String, CaseIterable {
case dock
case dockAndMenuBar
case menuBar
public var isStatusItemVisible: Bool {
switch self {
case .dockAndMenuBar, .menuBar: return true
case .dock: return false
}
}
public var isDockVisible: Bool {
switch self {
case .dockAndMenuBar, .dock: return true
case .menuBar: return false
}
}
}
public var locationVisibility: LocationVisibility {
get {
prefs.string(forKey: "locationVisibility").flatMap(LocationVisibility.init(rawValue:)) ?? .dock
}
set {
prefs.set(newValue.rawValue, forKey: "locationVisibility")
NotificationCenter.default.post(
name: Self.menuRelatedSettingDidChange,
object: nil,
userInfo: nil
)
}
}
public var menuItemTemplate: (server: Server, template: String)? {
get {
let server: Server?
if let serverIdentifier = prefs.string(forKey: "menuItemTemplate-server"),
let configured = Current.servers.server(forServerIdentifier: serverIdentifier) {
server = configured
} else {
// backwards compatibility to before servers, or if the server was deleted
server = Current.servers.all.first
}
if let server {
return (server, prefs.string(forKey: "menuItemTemplate") ?? "")
} else {
return nil
}
}
set {
prefs.setValue(newValue?.0.identifier.rawValue, forKey: "menuItemTemplate-server")
prefs.setValue(newValue?.1, forKey: "menuItemTemplate")
NotificationCenter.default.post(
name: Self.menuRelatedSettingDidChange,
object: nil,
userInfo: nil
)
}
}
public struct LocationSource {
public var zone: Bool
public var backgroundFetch: Bool
public var significantLocationChange: Bool
public var pushNotifications: Bool
static func key(for keyPath: KeyPath<LocationSource, Bool>) -> String {
switch keyPath {
case \.zone: return "locationUpdateOnZone"
case \.backgroundFetch: return "locationUpdateOnBackgroundFetch"
case \.significantLocationChange: return "locationUpdateOnSignificant"
case \.pushNotifications: return "locationUpdateOnNotification"
default: return ""
}
}
}
public var locationSources: LocationSource {
get {
func boolValue(for keyPath: KeyPath<LocationSource, Bool>) -> Bool {
let key = LocationSource.key(for: keyPath)
if prefs.object(forKey: key) == nil {
// default to enabled for location source settings
return true
}
return prefs.bool(forKey: key)
}
return .init(
zone: boolValue(for: \.zone),
backgroundFetch: boolValue(for: \.backgroundFetch),
significantLocationChange: boolValue(for: \.significantLocationChange),
pushNotifications: boolValue(for: \.pushNotifications)
)
}
set {
prefs.set(newValue.zone, forKey: LocationSource.key(for: \.zone))
prefs.set(newValue.backgroundFetch, forKey: LocationSource.key(for: \.backgroundFetch))
prefs.set(newValue.significantLocationChange, forKey: LocationSource.key(for: \.significantLocationChange))
prefs.set(newValue.pushNotifications, forKey: LocationSource.key(for: \.pushNotifications))
Current.Log.info("location sources updated to \(newValue)")
NotificationCenter.default.post(name: Self.locationRelatedSettingDidChange, object: nil)
}
}
public var clearBadgeAutomatically: Bool {
get {
if let value = prefs.object(forKey: "clearBadgeAutomatically") as? NSNumber {
return value.boolValue
} else {
return true
}
}
set {
prefs.set(newValue, forKey: "clearBadgeAutomatically")
}
}
public var widgetAuthenticityToken: String {
let key = "widgetAuthenticityToken"
if let existing = prefs.string(forKey: key) {
return existing
} else {
let string = UUID().uuidString
prefs.set(string, forKey: key)
return string
}
}
// MARK: - Private helpers
private var defaultDeviceID: String {
let baseID = removeSpecialCharsFromString(text: Current.device.deviceName())
.replacingOccurrences(of: " ", with: "_")
.lowercased()
if Current.appConfiguration != .release {
return "\(baseID)_\(Current.appConfiguration.description.lowercased())"
}
return baseID
}
private func removeSpecialCharsFromString(text: String) -> String {
let okayChars: Set<Character> =
Set("abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLKMNOPQRSTUVWXYZ1234567890")
return String(text.filter { okayChars.contains($0) })
}
}
public class BluetoothPermissionScreenDisplayedCount: UserDefaultsValueSync<Int> {
public init() {
super.init(settingsKey: "bluetoothPermissionScreenPresentedCount")
}
}