Sfoglia il codice sorgente

HealthKitManager refactoring

Ivan Valkou 4 anni fa
parent
commit
aa7255c8d0

+ 1 - 1
FreeAPS/Sources/APS/FetchGlucoseManager.swift

@@ -86,7 +86,7 @@ final class BaseFetchGlucoseManager: FetchGlucoseManager, Injectable {
                 let glucoseForHealth = filteredByDate.filter { !glucoseFromHealth.contains($0) }
 
                 guard glucoseForHealth.isNotEmpty else { return }
-                self.healthKitManager.saveIfNeeded(bloodGlucoses: glucoseForHealth)
+                self.healthKitManager.saveIfNeeded(bloodGlucose: glucoseForHealth)
             }
             .store(in: &lifetime)
         timer.fire()

+ 0 - 4
FreeAPS/Sources/APS/OpenAPS/Constants.swift

@@ -86,8 +86,4 @@ extension OpenAPS {
         static let tempTargetsPresets = "freeaps/temptargets_presets.json"
         static let calibrations = "freeaps/calibrations.json"
     }
-
-    enum HealthKit {
-        static let downloadedGlucose = "healthkit/downloaded-glucose.json"
-    }
 }

+ 8 - 6
FreeAPS/Sources/Modules/HealthKit/HealthKitStateModel.swift

@@ -23,18 +23,20 @@ extension AppleHealthKit {
                     return
                 }
 
-                self.healthKitManager.requestPermission { _, error in
-                    guard error == nil else {
+                self.healthKitManager.requestPermission { ok, error in
+                    DispatchQueue.main.async {
+                        self.needShowInformationTextForSetPermissions = !self.healthKitManager.checkAvailabilitySaveBG()
+                    }
+
+                    guard ok, error == nil else {
+                        warning(.service, "Permission not granted for HealthKitManager", error: error)
                         return
                     }
 
-                    debug(.service, "User set permission for HealthKitManager")
+                    debug(.service, "Permission  granted HealthKitManager")
 
                     self.healthKitManager.createObserver()
                     self.healthKitManager.enableBackgroundDelivery()
-                    DispatchQueue.main.async {
-                        self.needShowInformationTextForSetPermissions = !self.healthKitManager.checkAvailabilitySaveBG()
-                    }
                 }
             }
         }

+ 1 - 1
FreeAPS/Sources/Modules/NightscoutConfig/NightscoutConfigStateModel.swift

@@ -63,7 +63,7 @@ extension NightscoutConfig {
 
                     guard glucose.isNotEmpty else { return }
                     self.glucoseStorage.storeGlucose(glucose)
-                    self.healthKitManager.saveIfNeeded(bloodGlucoses: glucose)
+                    self.healthKitManager.saveIfNeeded(bloodGlucose: glucose)
                 }
                 .store(in: &lifetime)
         }

+ 4 - 3
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -1,3 +1,4 @@
+import HealthKit
 import SwiftUI
 import Swinject
 
@@ -20,7 +21,9 @@ extension Settings {
                 Section(header: Text("Services")) {
                     Text("Nightscout").navigationLink(to: .nighscoutConfig, from: self)
                     Text("CGM").navigationLink(to: .cgm, from: self)
-                    Text("Apple Health").navigationLink(to: .healthkit, from: self)
+                    if HKHealthStore.isHealthDataAvailable() {
+                        Text("Apple Health").navigationLink(to: .healthkit, from: self)
+                    }
                     Text("Notifications").navigationLink(to: .notificationsConfig, from: self)
                 }
 
@@ -84,8 +87,6 @@ extension Settings {
                         }
 
                         Group {
-                            Text("HealthKit")
-                                .navigationLink(to: .configEditor(file: OpenAPS.HealthKit.downloadedGlucose), from: self)
                             Text("Target presets")
                                 .navigationLink(to: .configEditor(file: OpenAPS.FreeAPS.tempTargetsPresets), from: self)
                             Text("Calibrations")

+ 114 - 150
FreeAPS/Sources/Services/HealthKit/HealthKitManager.swift

@@ -15,7 +15,7 @@ protocol HealthKitManager: GlucoseSource {
     /// Requests user to give permissions on using HealthKit
     func requestPermission(completion: ((Bool, Error?) -> Void)?)
     /// Save blood glucose to Health store (dublicate of bg will ignore)
-    func saveIfNeeded(bloodGlucoses: [BloodGlucose])
+    func saveIfNeeded(bloodGlucose: [BloodGlucose])
     /// Create observer for data passing beetwen Health Store and FreeAPS
     func createObserver()
     /// Enable background delivering objects from Apple Health to FreeAPS
@@ -23,74 +23,53 @@ protocol HealthKitManager: GlucoseSource {
 }
 
 final class BaseHealthKitManager: HealthKitManager, Injectable {
-    @Injected() private var fileStorage: FileStorage!
     @Injected() private var glucoseStorage: GlucoseStorage!
     @Injected() private var healthKitStore: HKHealthStore!
     @Injected() private var settingsManager: SettingsManager!
 
-    private let queue = DispatchQueue(label: "debugInfoQueue")
-    private var lock = NSLock(label: "helathKitExecureQueryLock")
+    private let processQueue = DispatchQueue(label: "BaseHealthKitManager.processQueue")
 
     private enum Config {
         // unwraped HKObjects
-        static var permissions: Set<HKSampleType> {
-            var result: Set<HKSampleType> = []
-            for permission in optionalPermissions {
-                result.insert(permission!)
-            }
-            return result
-        }
+        static var permissions: Set<HKSampleType> { Set([healthBGObject].compactMap { $0 }) }
 
-        static let optionalPermissions = Set([Config.healthBGObject])
         // link to object in HealthKit
         static let healthBGObject = HKObjectType.quantityType(forIdentifier: .bloodGlucose)
 
-        static let frequencyBackgroundDeliveryBloodGlucoseFromHealth = HKUpdateFrequency(rawValue: 1)!
         // Meta-data key of FreeASPX data in HealthStore
         static let freeAPSMetaKey = "fromFreeAPSX"
     }
 
     // BG that will be return Publisher
-    @Persisted(key: "HealthKitManagerNewGlucose") private var newGlucose: [BloodGlucose] = []
+    @SyncAccess @Persisted(key: "HealthKitManagerNewGlucose") private var newGlucose: [BloodGlucose] = []
 
     // last anchor for HKAnchoredQuery
     private var lastBloodGlucoseQueryAnchor: HKQueryAnchor! {
         set {
-            guard let data = try? NSKeyedArchiver.archivedData(withRootObject: newValue as Any, requiringSecureCoding: false)
-            else {
-                persistedAnchor = Data()
-                return
-            }
-            persistedAnchor = data
+            persistedAnchor = (
+                try? NSKeyedArchiver.archivedData(withRootObject: newValue as Any, requiringSecureCoding: false)
+            ) ?? Data()
         }
         get {
-            guard let result = try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(persistedAnchor) as? HKQueryAnchor else {
-                return HKQueryAnchor(fromValue: 0)
-            }
-            return result
+            (try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(persistedAnchor) as? HKQueryAnchor) ??
+                HKQueryAnchor(fromValue: 0)
         }
     }
 
-    @Persisted(key: "HealthKitManagerAnchor") private var persistedAnchor = Data()
+    @SyncAccess @Persisted(key: "HealthKitManagerAnchor") private var persistedAnchor = Data()
 
     var isAvailableOnCurrentDevice: Bool {
         HKHealthStore.isHealthDataAvailable()
     }
 
     var areAllowAllPermissions: Bool {
-        var result = true
-        Config.permissions.forEach { permission in
-            if [HKAuthorizationStatus.sharingDenied, HKAuthorizationStatus.notDetermined]
-                .contains(healthKitStore.authorizationStatus(for: permission))
-            {
-                result = false
-            }
-        }
-        return result
+        Set(Config.permissions.map { healthKitStore.authorizationStatus(for: $0) })
+            .intersection([.sharingDenied, .notDetermined])
+            .isEmpty
     }
 
     // NSPredicate, which use during load increment BG from Health store
-    private lazy var loadBGPredicate: NSPredicate = {
+    private var loadBGPredicate: NSPredicate {
         // loading only daily bg
         let predicateByStartDate = HKQuery.predicateForSamples(
             withStart: Date().addingTimeInterval(-1.days.timeInterval),
@@ -107,7 +86,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         )
 
         return NSCompoundPredicate(andPredicateWithSubpredicates: [predicateByStartDate, predicateByMeta])
-    }()
+    }
 
     init(resolver: Resolver) {
         injectServices(resolver)
@@ -119,20 +98,11 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
     }
 
     func checkAvailabilitySave(objectTypeToHealthStore: HKObjectType) -> Bool {
-        let status = healthKitStore.authorizationStatus(for: objectTypeToHealthStore)
-        switch status {
-        case .sharingAuthorized:
-            return true
-        default:
-            return false
-        }
+        healthKitStore.authorizationStatus(for: objectTypeToHealthStore) == .sharingAuthorized
     }
 
     func checkAvailabilitySaveBG() -> Bool {
-        guard let sampleType = Config.healthBGObject else {
-            return false
-        }
-        return checkAvailabilitySave(objectTypeToHealthStore: sampleType)
+        Config.healthBGObject.map { checkAvailabilitySave(objectTypeToHealthStore: $0) } ?? false
     }
 
     func requestPermission(completion: ((Bool, Error?) -> Void)? = nil) {
@@ -140,11 +110,9 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
             completion?(false, HKError.notAvailableOnCurrentDevice)
             return
         }
-        for permission in Config.optionalPermissions {
-            guard permission != nil else {
-                completion?(false, HKError.dataNotAvailable)
-                return
-            }
+        guard Config.permissions.isNotEmpty else {
+            completion?(false, HKError.dataNotAvailable)
+            return
         }
 
         healthKitStore.requestAuthorization(toShare: Config.permissions, read: Config.permissions) { status, error in
@@ -152,34 +120,36 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         }
     }
 
-    func saveIfNeeded(bloodGlucoses: [BloodGlucose]) {
+    func saveIfNeeded(bloodGlucose: [BloodGlucose]) {
         guard settingsManager.settings.useAppleHealth,
               let sampleType = Config.healthBGObject,
               checkAvailabilitySave(objectTypeToHealthStore: sampleType),
-              bloodGlucoses.isNotEmpty
+              bloodGlucose.isNotEmpty
         else { return }
 
-        for bgItem in bloodGlucoses {
-            let bgQuantity = HKQuantity(
-                unit: .milligramsPerDeciliter,
-                doubleValue: Double(bgItem.glucose!)
-            )
+        processQueue.async {
+            for bgItem in bloodGlucose {
+                let bgQuantity = HKQuantity(
+                    unit: .milligramsPerDeciliter,
+                    doubleValue: Double(bgItem.glucose!)
+                )
 
-            let bgObjectSample = HKQuantitySample(
-                type: sampleType,
-                quantity: bgQuantity,
-                start: bgItem.dateString,
-                end: bgItem.dateString,
-                metadata: [
-                    HKMetadataKeyExternalUUID: bgItem.id,
-                    HKMetadataKeySyncIdentifier: bgItem.id,
-                    HKMetadataKeySyncVersion: 1,
-                    Config.freeAPSMetaKey: true
-                ]
-            )
-            load(sampleFromHealth: sampleType, withID: bgItem.id) { [weak self] samples in
-                if samples.isEmpty {
-                    self?.healthKitStore.save(bgObjectSample) { _, _ in }
+                let bgObjectSample = HKQuantitySample(
+                    type: sampleType,
+                    quantity: bgQuantity,
+                    start: bgItem.dateString,
+                    end: bgItem.dateString,
+                    metadata: [
+                        HKMetadataKeyExternalUUID: bgItem.id,
+                        HKMetadataKeySyncIdentifier: bgItem.id,
+                        HKMetadataKeySyncVersion: 1,
+                        Config.freeAPSMetaKey: true
+                    ]
+                )
+                self.load(sampleFromHealth: sampleType, withID: bgItem.id) { [weak self] samples in
+                    if samples.isEmpty {
+                        self?.healthKitStore.save(bgObjectSample) { _, _ in }
+                    }
                 }
             }
         }
@@ -189,25 +159,21 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         guard settingsManager.settings.useAppleHealth else { return }
 
         guard let bgType = Config.healthBGObject else {
-            warning(
-                .service,
-                "Can not create HealthKit Observer, because unable to get the Blood Glucose type",
-                description: nil,
-                error: nil
-            )
+            warning(.service, "Can not create HealthKit Observer, because unable to get the Blood Glucose type")
             return
         }
 
-        let query = HKObserverQuery(sampleType: bgType, predicate: nil) { [unowned self] _, _, observerError in
+        let query = HKObserverQuery(sampleType: bgType, predicate: nil) { [weak self] _, _, observerError in
+            guard let self = self else { return }
             debug(.service, "Execute HelathKit observer query for loading increment samples")
             guard observerError == nil else {
                 warning(.service, "Error during execution of HelathKit Observer's query", error: observerError!)
                 return
             }
 
-            if let incrementQuery = getBloodGlucoseHKQuery(predicate: loadBGPredicate) {
+            if let incrementQuery = self.getBloodGlucoseHKQuery(predicate: self.loadBGPredicate) {
                 debug(.service, "Create increment query")
-                healthKitStore.execute(incrementQuery)
+                self.healthKitStore.execute(incrementQuery)
             }
         }
         healthKitStore.execute(query)
@@ -222,19 +188,14 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         guard let bgType = Config.healthBGObject else {
             warning(
                 .service,
-                "Can not create background delivery, because unable to get the Blood Glucose type",
-                description: nil,
-                error: nil
+                "Can not create background delivery, because unable to get the Blood Glucose type"
             )
             return
         }
 
-        healthKitStore.enableBackgroundDelivery(
-            for: bgType,
-            frequency: Config.frequencyBackgroundDeliveryBloodGlucoseFromHealth
-        ) { status, e in
-            guard e == nil else {
-                warning(.service, "Can not enable background delivery", description: nil, error: e)
+        healthKitStore.enableBackgroundDelivery(for: bgType, frequency: .immediate) { status, error in
+            guard error == nil else {
+                warning(.service, "Can not enable background delivery", error: error)
                 return
             }
             debug(.service, "Background delivery status is \(status)")
@@ -247,6 +208,7 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
         withID id: String,
         andDo completion: (([HKSample]) -> Void)?
     ) {
+        dispatchPrecondition(condition: .onQueue(processQueue))
         let predicate = HKQuery.predicateForObjects(
             withMetadataKey: HKMetadataKeySyncIdentifier,
             operatorType: .equalTo,
@@ -278,34 +240,34 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
             predicate: predicate,
             anchor: lastBloodGlucoseQueryAnchor,
             limit: HKObjectQueryNoLimit
-        ) { [unowned self] _, addedObjects, deletedObjects, anchor, _ in
-            queue.sync {
+        ) { [weak self] _, addedObjects, deletedObjects, anchor, _ in
+            guard let self = self else { return }
+            self.processQueue.async {
                 debug(.service, "AnchoredQuery did execute")
-            }
 
-            lastBloodGlucoseQueryAnchor = anchor
+                self.lastBloodGlucoseQueryAnchor = anchor
 
-            // Added objects
-            if let bgSamples = addedObjects as? [HKQuantitySample],
-               bgSamples.isNotEmpty
-            {
-                prepare(bloodGlucoseSamplesToPublisherFetch: bgSamples)
-            }
+                // Added objects
+                if let bgSamples = addedObjects as? [HKQuantitySample],
+                   bgSamples.isNotEmpty
+                {
+                    self.prepareSamplesToPublisherFetch(bgSamples)
+                }
 
-            // Deleted objects
-            if let deletedSamples = deletedObjects,
-               deletedSamples.isNotEmpty
-            {
-                delete(samplesFromLocalStorage: deletedSamples)
+                // Deleted objects
+                if let deletedSamples = deletedObjects,
+                   deletedSamples.isNotEmpty
+                {
+                    self.deleteSamplesFromLocalStorage(deletedSamples)
+                }
             }
         }
         return query
     }
 
-    private func prepare(bloodGlucoseSamplesToPublisherFetch samples: [HKQuantitySample]) {
-        queue.sync {
-            debug(.service, "Start preparing samples: \(String(describing: samples))")
-        }
+    private func prepareSamplesToPublisherFetch(_ samples: [HKQuantitySample]) {
+        dispatchPrecondition(condition: .onQueue(processQueue))
+        debug(.service, "Start preparing samples: \(String(describing: samples))")
 
         newGlucose += samples
             .compactMap { sample -> HealthKitSample? in
@@ -335,54 +297,56 @@ final class BaseHealthKitManager: HealthKitManager, Injectable {
 
         newGlucose = newGlucose.removeDublicates()
 
-        queue.sync {
-            debug(
-                .service,
-                "Current BloodGlucose.Type objects will be send from Publisher during fetch: \(String(describing: newGlucose))"
-            )
-        }
+        debug(
+            .service,
+            "Current BloodGlucose.Type objects will be send from Publisher during fetch: \(String(describing: newGlucose))"
+        )
     }
 
-    private func delete(samplesFromLocalStorage deletedSamples: [HKDeletedObject]) {
-        queue.sync {
-            debug(.service, "Delete HealthKit objects: \(String(describing: deletedSamples))")
-        }
-        DispatchQueue.global(qos: .utility).async {
-            let removingBGID = deletedSamples.map {
-                $0.metadata?[HKMetadataKeySyncIdentifier] as? String ?? $0.uuid.uuidString
-            }
-            self.glucoseStorage.removeGlucose(ids: removingBGID)
-            self.newGlucose = self.newGlucose.filter { !removingBGID.contains($0.id) }
+    private func deleteSamplesFromLocalStorage(_ deletedSamples: [HKDeletedObject]) {
+        dispatchPrecondition(condition: .onQueue(processQueue))
+        debug(.service, "Delete HealthKit objects: \(String(describing: deletedSamples))")
+
+        let removingBGID = deletedSamples.map {
+            $0.metadata?[HKMetadataKeySyncIdentifier] as? String ?? $0.uuid.uuidString
         }
+        glucoseStorage.removeGlucose(ids: removingBGID)
+        newGlucose = newGlucose.filter { !removingBGID.contains($0.id) }
     }
 
     func fetch() -> AnyPublisher<[BloodGlucose], Never> {
-        queue.sync {
-            debug(.service, "Start fetching HealthKitManager")
-        }
-        guard settingsManager.settings.useAppleHealth else {
-            queue.sync {
-                debug(.service, "HealthKitManager cant return any data, because useAppleHealth option is disable")
+        Future { [weak self] promise in
+            guard let self = self else {
+                promise(.success([]))
+                return
             }
-            return Just([]).eraseToAnyPublisher()
-        }
 
-        // Remove old BGs
-        newGlucose = newGlucose
-            .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
-        // Get actual BGs (beetwen Date() - 1 day and Date())
-        let actualGlucose = newGlucose
-            .filter { $0.dateString <= Date() }
-        // Update newGlucose
-        newGlucose = newGlucose
-            .filter { !actualGlucose.contains($0) }
-        queue.sync {
-            debug(.service, "Actual glucose is \(actualGlucose)")
-        }
-        queue.sync {
-            debug(.service, "Current state of newGlucose is \(newGlucose)")
+            self.processQueue.async {
+                debug(.service, "Start fetching HealthKitManager")
+                guard self.settingsManager.settings.useAppleHealth else {
+                    debug(.service, "HealthKitManager cant return any data, because useAppleHealth option is disable")
+                    promise(.success([]))
+                    return
+                }
+
+                // Remove old BGs
+                self.newGlucose = self.newGlucose
+                    .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
+                // Get actual BGs (beetwen Date() - 1 day and Date())
+                let actualGlucose = self.newGlucose
+                    .filter { $0.dateString <= Date() }
+                // Update newGlucose
+                self.newGlucose = self.newGlucose
+                    .filter { !actualGlucose.contains($0) }
+
+                debug(.service, "Actual glucose is \(actualGlucose)")
+
+                debug(.service, "Current state of newGlucose is \(self.newGlucose)")
+
+                promise(.success(actualGlucose))
+            }
         }
-        return Just(actualGlucose).eraseToAnyPublisher()
+        .eraseToAnyPublisher()
     }
 }