|
|
@@ -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()
|
|
|
}
|
|
|
}
|
|
|
|