SettingsStore.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. //
  2. // SettingsStore.swift
  3. // LoopKit
  4. //
  5. // Created by Darin Krauss on 10/14/19.
  6. // Copyright © 2019 LoopKit Authors. All rights reserved.
  7. //
  8. import os.log
  9. import Foundation
  10. import CoreData
  11. import HealthKit
  12. public protocol SettingsStoreDelegate: AnyObject {
  13. /**
  14. Informs the delegate that the settings store has updated settings data.
  15. - Parameter settingsStore: The settings store that has updated settings data.
  16. */
  17. func settingsStoreHasUpdatedSettingsData(_ settingsStore: SettingsStore)
  18. }
  19. public class SettingsStore {
  20. public weak var delegate: SettingsStoreDelegate?
  21. private let store: PersistenceController
  22. private let expireAfter: TimeInterval
  23. private let dataAccessQueue = DispatchQueue(label: "com.loopkit.SettingsStore.dataAccessQueue", qos: .utility)
  24. private let log = OSLog(category: "SettingsStore")
  25. public init(store: PersistenceController, expireAfter: TimeInterval) {
  26. self.store = store
  27. self.expireAfter = expireAfter
  28. }
  29. public func storeSettings(_ settings: StoredSettings, completion: @escaping () -> Void) {
  30. dataAccessQueue.async {
  31. if let data = self.encodeSettings(settings) {
  32. self.store.managedObjectContext.performAndWait {
  33. let object = SettingsObject(context: self.store.managedObjectContext)
  34. object.data = data
  35. object.date = settings.date
  36. self.store.save()
  37. }
  38. }
  39. self.purgeExpiredSettings()
  40. completion()
  41. }
  42. }
  43. public var expireDate: Date {
  44. return Date(timeIntervalSinceNow: -expireAfter)
  45. }
  46. private func purgeExpiredSettings() {
  47. purgeSettingsObjects(before: expireDate)
  48. }
  49. public func purgeSettings(before date: Date, completion: @escaping (Error?) -> Void) {
  50. dataAccessQueue.async {
  51. self.purgeSettingsObjects(before: date, completion: completion)
  52. }
  53. }
  54. private func purgeSettingsObjects(before date: Date, completion: ((Error?) -> Void)? = nil) {
  55. dispatchPrecondition(condition: .onQueue(dataAccessQueue))
  56. var purgeError: Error?
  57. store.managedObjectContext.performAndWait {
  58. do {
  59. let count = try self.store.managedObjectContext.purgeObjects(of: SettingsObject.self, matching: NSPredicate(format: "date < %@", date as NSDate))
  60. self.log.info("Purged %d SettingsObjects", count)
  61. } catch let error {
  62. self.log.error("Unable to purge SettingsObjects: %{public}@", String(describing: error))
  63. purgeError = error
  64. }
  65. }
  66. if let purgeError = purgeError {
  67. completion?(purgeError)
  68. return
  69. }
  70. delegate?.settingsStoreHasUpdatedSettingsData(self)
  71. completion?(nil)
  72. }
  73. private static var encoder: PropertyListEncoder = {
  74. let encoder = PropertyListEncoder()
  75. encoder.outputFormat = .binary
  76. return encoder
  77. }()
  78. private func encodeSettings(_ settings: StoredSettings) -> Data? {
  79. do {
  80. return try SettingsStore.encoder.encode(settings)
  81. } catch let error {
  82. self.log.error("Error encoding StoredSettings: %@", String(describing: error))
  83. return nil
  84. }
  85. }
  86. private static var decoder = PropertyListDecoder()
  87. private func decodeSettings(fromData data: Data) -> StoredSettings? {
  88. do {
  89. return try SettingsStore.decoder.decode(StoredSettings.self, from: data)
  90. } catch let error {
  91. self.log.error("Error decoding StoredSettings: %@", String(describing: error))
  92. return nil
  93. }
  94. }
  95. }
  96. extension SettingsStore {
  97. public struct QueryAnchor: RawRepresentable {
  98. public typealias RawValue = [String: Any]
  99. internal var modificationCounter: Int64
  100. public init() {
  101. self.modificationCounter = 0
  102. }
  103. public init?(rawValue: RawValue) {
  104. guard let modificationCounter = rawValue["modificationCounter"] as? Int64 else {
  105. return nil
  106. }
  107. self.modificationCounter = modificationCounter
  108. }
  109. public var rawValue: RawValue {
  110. var rawValue: RawValue = [:]
  111. rawValue["modificationCounter"] = modificationCounter
  112. return rawValue
  113. }
  114. }
  115. public enum SettingsQueryResult {
  116. case success(QueryAnchor, [StoredSettings])
  117. case failure(Error)
  118. }
  119. public func executeSettingsQuery(fromQueryAnchor queryAnchor: QueryAnchor?, limit: Int, completion: @escaping (SettingsQueryResult) -> Void) {
  120. dataAccessQueue.async {
  121. var queryAnchor = queryAnchor ?? QueryAnchor()
  122. var queryResult = [StoredSettings]()
  123. var queryError: Error?
  124. guard limit > 0 else {
  125. completion(.success(queryAnchor, queryResult))
  126. return
  127. }
  128. self.store.managedObjectContext.performAndWait {
  129. let storedRequest: NSFetchRequest<SettingsObject> = SettingsObject.fetchRequest()
  130. storedRequest.predicate = NSPredicate(format: "modificationCounter > %d", queryAnchor.modificationCounter)
  131. storedRequest.sortDescriptors = [NSSortDescriptor(key: "modificationCounter", ascending: true)]
  132. storedRequest.fetchLimit = limit
  133. do {
  134. let stored = try self.store.managedObjectContext.fetch(storedRequest)
  135. if let modificationCounter = stored.max(by: { $0.modificationCounter < $1.modificationCounter })?.modificationCounter {
  136. queryAnchor.modificationCounter = modificationCounter
  137. }
  138. queryResult.append(contentsOf: stored.compactMap { self.decodeSettings(fromData: $0.data) })
  139. } catch let error {
  140. queryError = error
  141. return
  142. }
  143. }
  144. if let queryError = queryError {
  145. completion(.failure(queryError))
  146. return
  147. }
  148. completion(.success(queryAnchor, queryResult))
  149. }
  150. }
  151. }
  152. public struct StoredSettings {
  153. public let date: Date
  154. public let dosingEnabled: Bool
  155. public let glucoseTargetRangeSchedule: GlucoseRangeSchedule?
  156. public let preMealTargetRange: DoubleRange?
  157. public let workoutTargetRange: DoubleRange?
  158. public let overridePresets: [TemporaryScheduleOverridePreset]?
  159. public let scheduleOverride: TemporaryScheduleOverride?
  160. public let preMealOverride: TemporaryScheduleOverride?
  161. public let maximumBasalRatePerHour: Double?
  162. public let maximumBolus: Double?
  163. public let suspendThreshold: GlucoseThreshold?
  164. public let deviceToken: String?
  165. public let insulinModel: StoredInsulinModel?
  166. public let basalRateSchedule: BasalRateSchedule?
  167. public let insulinSensitivitySchedule: InsulinSensitivitySchedule?
  168. public let carbRatioSchedule: CarbRatioSchedule?
  169. public let bloodGlucoseUnit: HKUnit?
  170. public let syncIdentifier: String
  171. public init(date: Date = Date(),
  172. dosingEnabled: Bool = false,
  173. glucoseTargetRangeSchedule: GlucoseRangeSchedule? = nil,
  174. preMealTargetRange: DoubleRange? = nil,
  175. workoutTargetRange: DoubleRange? = nil,
  176. overridePresets: [TemporaryScheduleOverridePreset]? = nil,
  177. scheduleOverride: TemporaryScheduleOverride? = nil,
  178. preMealOverride: TemporaryScheduleOverride? = nil,
  179. maximumBasalRatePerHour: Double? = nil,
  180. maximumBolus: Double? = nil,
  181. suspendThreshold: GlucoseThreshold? = nil,
  182. deviceToken: String? = nil,
  183. insulinModel: StoredInsulinModel? = nil,
  184. basalRateSchedule: BasalRateSchedule? = nil,
  185. insulinSensitivitySchedule: InsulinSensitivitySchedule? = nil,
  186. carbRatioSchedule: CarbRatioSchedule? = nil,
  187. bloodGlucoseUnit: HKUnit? = nil,
  188. syncIdentifier: String = UUID().uuidString) {
  189. self.date = date
  190. self.dosingEnabled = dosingEnabled
  191. self.glucoseTargetRangeSchedule = glucoseTargetRangeSchedule
  192. self.preMealTargetRange = preMealTargetRange
  193. self.workoutTargetRange = workoutTargetRange
  194. self.overridePresets = overridePresets
  195. self.scheduleOverride = scheduleOverride
  196. self.preMealOverride = preMealOverride
  197. self.maximumBasalRatePerHour = maximumBasalRatePerHour
  198. self.maximumBolus = maximumBolus
  199. self.suspendThreshold = suspendThreshold
  200. self.deviceToken = deviceToken
  201. self.insulinModel = insulinModel
  202. self.basalRateSchedule = basalRateSchedule
  203. self.insulinSensitivitySchedule = insulinSensitivitySchedule
  204. self.carbRatioSchedule = carbRatioSchedule
  205. self.bloodGlucoseUnit = bloodGlucoseUnit
  206. self.syncIdentifier = syncIdentifier
  207. }
  208. }
  209. extension StoredSettings: Codable {
  210. public init(from decoder: Decoder) throws {
  211. let container = try decoder.container(keyedBy: CodingKeys.self)
  212. var bloodGlucoseUnit: HKUnit?
  213. if let bloodGlucoseUnitString = try container.decodeIfPresent(String.self, forKey: .bloodGlucoseUnit) {
  214. bloodGlucoseUnit = HKUnit(from: bloodGlucoseUnitString)
  215. }
  216. self.init(date: try container.decode(Date.self, forKey: .date),
  217. dosingEnabled: try container.decode(Bool.self, forKey: .dosingEnabled),
  218. glucoseTargetRangeSchedule: try container.decodeIfPresent(GlucoseRangeSchedule.self, forKey: .glucoseTargetRangeSchedule),
  219. preMealTargetRange: try container.decodeIfPresent(DoubleRange.self, forKey: .preMealTargetRange),
  220. workoutTargetRange: try container.decodeIfPresent(DoubleRange.self, forKey: .workoutTargetRange),
  221. overridePresets: try container.decodeIfPresent([TemporaryScheduleOverridePreset].self, forKey: .overridePresets),
  222. scheduleOverride: try container.decodeIfPresent(TemporaryScheduleOverride.self, forKey: .scheduleOverride),
  223. preMealOverride: try container.decodeIfPresent(TemporaryScheduleOverride.self, forKey: .preMealOverride),
  224. maximumBasalRatePerHour: try container.decodeIfPresent(Double.self, forKey: .maximumBasalRatePerHour),
  225. maximumBolus: try container.decodeIfPresent(Double.self, forKey: .maximumBolus),
  226. suspendThreshold: try container.decodeIfPresent(GlucoseThreshold.self, forKey: .suspendThreshold),
  227. deviceToken: try container.decodeIfPresent(String.self, forKey: .deviceToken),
  228. insulinModel: try container.decodeIfPresent(StoredInsulinModel.self, forKey: .insulinModel),
  229. basalRateSchedule: try container.decodeIfPresent(BasalRateSchedule.self, forKey: .basalRateSchedule),
  230. insulinSensitivitySchedule: try container.decodeIfPresent(InsulinSensitivitySchedule.self, forKey: .insulinSensitivitySchedule),
  231. carbRatioSchedule: try container.decodeIfPresent(CarbRatioSchedule.self, forKey: .carbRatioSchedule),
  232. bloodGlucoseUnit: bloodGlucoseUnit,
  233. syncIdentifier: try container.decode(String.self, forKey: .syncIdentifier))
  234. }
  235. public func encode(to encoder: Encoder) throws {
  236. var container = encoder.container(keyedBy: CodingKeys.self)
  237. try container.encode(date, forKey: .date)
  238. try container.encode(dosingEnabled, forKey: .dosingEnabled)
  239. try container.encodeIfPresent(glucoseTargetRangeSchedule, forKey: .glucoseTargetRangeSchedule)
  240. try container.encodeIfPresent(preMealTargetRange, forKey: .preMealTargetRange)
  241. try container.encodeIfPresent(workoutTargetRange, forKey: .workoutTargetRange)
  242. try container.encodeIfPresent(overridePresets, forKey: .overridePresets)
  243. try container.encodeIfPresent(scheduleOverride, forKey: .scheduleOverride)
  244. try container.encodeIfPresent(preMealOverride, forKey: .preMealOverride)
  245. try container.encodeIfPresent(maximumBasalRatePerHour, forKey: .maximumBasalRatePerHour)
  246. try container.encodeIfPresent(maximumBolus, forKey: .maximumBolus)
  247. try container.encodeIfPresent(suspendThreshold, forKey: .suspendThreshold)
  248. try container.encodeIfPresent(deviceToken, forKey: .deviceToken)
  249. try container.encodeIfPresent(insulinModel, forKey: .insulinModel)
  250. try container.encodeIfPresent(basalRateSchedule, forKey: .basalRateSchedule)
  251. try container.encodeIfPresent(insulinSensitivitySchedule, forKey: .insulinSensitivitySchedule)
  252. try container.encodeIfPresent(carbRatioSchedule, forKey: .carbRatioSchedule)
  253. try container.encodeIfPresent(bloodGlucoseUnit?.unitString, forKey: .bloodGlucoseUnit)
  254. try container.encode(syncIdentifier, forKey: .syncIdentifier)
  255. }
  256. private enum CodingKeys: String, CodingKey {
  257. case date
  258. case dosingEnabled
  259. case glucoseTargetRangeSchedule
  260. case preMealTargetRange
  261. case workoutTargetRange
  262. case overridePresets
  263. case scheduleOverride
  264. case preMealOverride
  265. case maximumBasalRatePerHour
  266. case maximumBolus
  267. case suspendThreshold
  268. case deviceToken
  269. case insulinModel
  270. case basalRateSchedule
  271. case insulinSensitivitySchedule
  272. case carbRatioSchedule
  273. case bloodGlucoseUnit
  274. case syncIdentifier
  275. }
  276. }
  277. // MARK: - Critical Event Log Export
  278. extension SettingsStore: CriticalEventLog {
  279. private var exportProgressUnitCountPerObject: Int64 { 11 }
  280. private var exportFetchLimit: Int { Int(criticalEventLogExportProgressUnitCountPerFetch / exportProgressUnitCountPerObject) }
  281. public var exportName: String { "Settings.json" }
  282. public func exportProgressTotalUnitCount(startDate: Date, endDate: Date? = nil) -> Result<Int64, Error> {
  283. var result: Result<Int64, Error>?
  284. self.store.managedObjectContext.performAndWait {
  285. do {
  286. let request: NSFetchRequest<SettingsObject> = SettingsObject.fetchRequest()
  287. request.predicate = self.exportDatePredicate(startDate: startDate, endDate: endDate)
  288. let objectCount = try self.store.managedObjectContext.count(for: request)
  289. result = .success(Int64(objectCount) * exportProgressUnitCountPerObject)
  290. } catch let error {
  291. result = .failure(error)
  292. }
  293. }
  294. return result!
  295. }
  296. public func export(startDate: Date, endDate: Date, to stream: OutputStream, progress: Progress) -> Error? {
  297. let encoder = JSONStreamEncoder(stream: stream)
  298. var modificationCounter: Int64 = 0
  299. var fetching = true
  300. var error: Error?
  301. while fetching && error == nil {
  302. self.store.managedObjectContext.performAndWait {
  303. do {
  304. guard !progress.isCancelled else {
  305. throw CriticalEventLogError.cancelled
  306. }
  307. let request: NSFetchRequest<SettingsObject> = SettingsObject.fetchRequest()
  308. request.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [NSPredicate(format: "modificationCounter > %d", modificationCounter),
  309. self.exportDatePredicate(startDate: startDate, endDate: endDate)])
  310. request.sortDescriptors = [NSSortDescriptor(key: "modificationCounter", ascending: true)]
  311. request.fetchLimit = self.exportFetchLimit
  312. let objects = try self.store.managedObjectContext.fetch(request)
  313. if objects.isEmpty {
  314. fetching = false
  315. return
  316. }
  317. try encoder.encode(objects)
  318. modificationCounter = objects.last!.modificationCounter
  319. progress.completedUnitCount += Int64(objects.count) * exportProgressUnitCountPerObject
  320. } catch let fetchError {
  321. error = fetchError
  322. }
  323. }
  324. }
  325. if let closeError = encoder.close(), error == nil {
  326. error = closeError
  327. }
  328. return error
  329. }
  330. private func exportDatePredicate(startDate: Date, endDate: Date? = nil) -> NSPredicate {
  331. var predicate = NSPredicate(format: "date >= %@", startDate as NSDate)
  332. if let endDate = endDate {
  333. predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSPredicate(format: "date < %@", endDate as NSDate)])
  334. }
  335. return predicate
  336. }
  337. }
  338. // MARK: - Core Data (Bulk) - TEST ONLY
  339. extension SettingsStore {
  340. public func addStoredSettings(settings: [StoredSettings], completion: @escaping (Error?) -> Void) {
  341. guard !settings.isEmpty else {
  342. completion(nil)
  343. return
  344. }
  345. dataAccessQueue.async {
  346. var error: Error?
  347. self.store.managedObjectContext.performAndWait {
  348. for setting in settings {
  349. guard let data = self.encodeSettings(setting) else {
  350. continue
  351. }
  352. let object = SettingsObject(context: self.store.managedObjectContext)
  353. object.data = data
  354. object.date = setting.date
  355. }
  356. error = self.store.save()
  357. }
  358. guard error == nil else {
  359. completion(error)
  360. return
  361. }
  362. self.log.info("Added %d SettingsObjects", settings.count)
  363. self.delegate?.settingsStoreHasUpdatedSettingsData(self)
  364. completion(nil)
  365. }
  366. }
  367. }