HealthKitManager.swift 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779
  1. import Combine
  2. import CoreData
  3. import Foundation
  4. import HealthKit
  5. import LoopKit
  6. import LoopKitUI
  7. import Swinject
  8. protocol HealthKitManager: GlucoseSource {
  9. /// Check all needed permissions
  10. /// Return false if one or more permissions are deny or not choosen
  11. var areAllowAllPermissions: Bool { get }
  12. /// Check availability to save data of BG type to Health store
  13. func checkAvailabilitySaveBG() -> Bool
  14. /// Requests user to give permissions on using HealthKit
  15. func requestPermission() async throws -> Bool
  16. /// Save blood glucose to Health store
  17. func uploadGlucose() async
  18. /// Save carbs to Health store
  19. func uploadCarbs() async
  20. /// Save Insulin to Health store
  21. func saveIfNeeded(pumpEvents events: [PumpHistoryEvent])
  22. /// Create observer for data passing beetwen Health Store and Trio
  23. func createBGObserver()
  24. /// Enable background delivering objects from Apple Health to Trio
  25. func enableBackgroundDelivery()
  26. /// Delete glucose with syncID
  27. func deleteGlucose(syncID: String)
  28. /// delete carbs with syncID
  29. func deleteCarbs(syncID: String, fpuID: String)
  30. /// delete insulin with syncID
  31. func deleteInsulin(syncID: String)
  32. }
  33. final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver, PumpHistoryObserver, CarbsStoredDelegate {
  34. private enum Config {
  35. // unwraped HKObjects
  36. static var readPermissions: Set<HKSampleType> {
  37. Set([healthBGObject].compactMap { $0 }) }
  38. static var writePermissions: Set<HKSampleType> {
  39. Set([healthBGObject, healthCarbObject, healthInsulinObject].compactMap { $0 }) }
  40. // link to object in HealthKit
  41. static let healthBGObject = HKObjectType.quantityType(forIdentifier: .bloodGlucose)
  42. static let healthCarbObject = HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates)
  43. static let healthInsulinObject = HKObjectType.quantityType(forIdentifier: .insulinDelivery)
  44. // Meta-data key of FreeASPX data in HealthStore
  45. static let freeAPSMetaKey = "From Trio"
  46. }
  47. @Injected() private var glucoseStorage: GlucoseStorage!
  48. @Injected() private var healthKitStore: HKHealthStore!
  49. @Injected() private var settingsManager: SettingsManager!
  50. @Injected() private var broadcaster: Broadcaster!
  51. @Injected() var carbsStorage: CarbsStorage!
  52. private var backgroundContext = CoreDataStack.shared.newTaskContext()
  53. func carbsStorageHasUpdatedCarbs(_: BaseCarbsStorage) {
  54. Task.detached { [weak self] in
  55. guard let self = self else { return }
  56. await self.uploadCarbs()
  57. }
  58. }
  59. private let processQueue = DispatchQueue(label: "BaseHealthKitManager.processQueue")
  60. private var lifetime = Lifetime()
  61. // BG that will be return Publisher
  62. @SyncAccess @Persisted(key: "BaseHealthKitManager.newGlucose") private var newGlucose: [BloodGlucose] = []
  63. // last anchor for HKAnchoredQuery
  64. private var lastBloodGlucoseQueryAnchor: HKQueryAnchor? {
  65. set {
  66. persistedBGAnchor = try? NSKeyedArchiver.archivedData(withRootObject: newValue as Any, requiringSecureCoding: false)
  67. }
  68. get {
  69. guard let data = persistedBGAnchor else { return nil }
  70. return try? NSKeyedUnarchiver.unarchivedObject(ofClass: HKQueryAnchor.self, from: data)
  71. }
  72. }
  73. @Persisted(key: "HealthKitManagerAnchor") private var persistedBGAnchor: Data? = nil
  74. var isAvailableOnCurrentDevice: Bool {
  75. HKHealthStore.isHealthDataAvailable()
  76. }
  77. var areAllowAllPermissions: Bool {
  78. Set(Config.readPermissions.map { healthKitStore.authorizationStatus(for: $0) })
  79. .intersection([.notDetermined])
  80. .isEmpty &&
  81. Set(Config.writePermissions.map { healthKitStore.authorizationStatus(for: $0) })
  82. .intersection([.sharingDenied, .notDetermined])
  83. .isEmpty
  84. }
  85. // NSPredicate, which use during load increment BG from Health store
  86. private var loadBGPredicate: NSPredicate {
  87. // loading only daily bg
  88. let predicateByStartDate = HKQuery.predicateForSamples(
  89. withStart: Date().addingTimeInterval(-1.days.timeInterval),
  90. end: nil,
  91. options: .strictStartDate
  92. )
  93. // loading only not FreeAPS bg
  94. // this predicate dont influence on Deleted Objects, only on added
  95. let predicateByMeta = HKQuery.predicateForObjects(
  96. withMetadataKey: Config.freeAPSMetaKey,
  97. operatorType: .notEqualTo,
  98. value: 1
  99. )
  100. return NSCompoundPredicate(andPredicateWithSubpredicates: [predicateByStartDate, predicateByMeta])
  101. }
  102. init(resolver: Resolver) {
  103. injectServices(resolver)
  104. guard isAvailableOnCurrentDevice,
  105. Config.healthBGObject != nil else { return }
  106. broadcaster.register(CarbsObserver.self, observer: self)
  107. broadcaster.register(PumpHistoryObserver.self, observer: self)
  108. carbsStorage.delegate = self
  109. debug(.service, "HealthKitManager did create")
  110. }
  111. func checkAvailabilitySave(objectTypeToHealthStore: HKObjectType) -> Bool {
  112. healthKitStore.authorizationStatus(for: objectTypeToHealthStore) == .sharingAuthorized
  113. }
  114. func checkAvailabilitySaveBG() -> Bool {
  115. Config.healthBGObject.map { checkAvailabilitySave(objectTypeToHealthStore: $0) } ?? false
  116. }
  117. func requestPermission() async throws -> Bool {
  118. guard isAvailableOnCurrentDevice else {
  119. throw HKError.notAvailableOnCurrentDevice
  120. }
  121. guard Config.readPermissions.isNotEmpty, Config.writePermissions.isNotEmpty else {
  122. throw HKError.dataNotAvailable
  123. }
  124. return try await withCheckedThrowingContinuation { continuation in
  125. healthKitStore.requestAuthorization(toShare: Config.writePermissions, read: Config.readPermissions) { status, error in
  126. if let error = error {
  127. continuation.resume(throwing: error)
  128. } else {
  129. continuation.resume(returning: status)
  130. }
  131. }
  132. }
  133. }
  134. // Glucose Upload
  135. func uploadGlucose() async {
  136. await uploadGlucose(glucoseStorage.getGlucoseNotYetUploadedToHealth())
  137. await uploadGlucose(glucoseStorage.getManualGlucoseNotYetUploadedToHealth())
  138. }
  139. func uploadGlucose(_ glucose: [BloodGlucose]) async {
  140. guard settingsManager.settings.useAppleHealth,
  141. let sampleType = Config.healthBGObject,
  142. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  143. glucose.isNotEmpty
  144. else { return }
  145. do {
  146. // Create HealthKit samples from all the passed glucose values
  147. let glucoseSamples = glucose.compactMap { glucoseSample -> HKQuantitySample? in
  148. guard let glucoseValue = glucoseSample.glucose else { return nil }
  149. return HKQuantitySample(
  150. type: sampleType,
  151. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double(glucoseValue)),
  152. start: glucoseSample.dateString,
  153. end: glucoseSample.dateString,
  154. metadata: [
  155. HKMetadataKeyExternalUUID: glucoseSample.id,
  156. HKMetadataKeySyncIdentifier: glucoseSample.id,
  157. HKMetadataKeySyncVersion: 1,
  158. Config.freeAPSMetaKey: true
  159. ]
  160. )
  161. }
  162. guard glucoseSamples.isNotEmpty else {
  163. debug(.service, "No glucose samples available for upload.")
  164. return
  165. }
  166. // Attempt to save the blood glucose samples to Apple Health
  167. try await healthKitStore.save(glucoseSamples)
  168. debug(.service, "Successfully stored \(glucoseSamples.count) blood glucose samples in HealthKit.")
  169. // After successful upload, update the isUploadedToHealth flag in Core Data
  170. await updateGlucoseAsUploaded(glucose)
  171. } catch {
  172. debug(.service, "Failed to upload glucose samples to HealthKit: \(error.localizedDescription)")
  173. }
  174. }
  175. private func updateGlucoseAsUploaded(_ glucose: [BloodGlucose]) async {
  176. await backgroundContext.perform {
  177. let ids = glucose.map(\.id) as NSArray
  178. let fetchRequest: NSFetchRequest<GlucoseStored> = GlucoseStored.fetchRequest()
  179. fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
  180. do {
  181. let results = try self.backgroundContext.fetch(fetchRequest)
  182. for result in results {
  183. result.isUploadedToHealth = true
  184. }
  185. guard self.backgroundContext.hasChanges else { return }
  186. try self.backgroundContext.save()
  187. } catch let error as NSError {
  188. debugPrint(
  189. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"
  190. )
  191. }
  192. }
  193. }
  194. // Carbs Upload
  195. func uploadCarbs() async {
  196. await uploadCarbs(carbsStorage.getCarbsNotYetUploadedToHealth())
  197. }
  198. func uploadCarbs(_ carbs: [CarbsEntry]) async {
  199. guard settingsManager.settings.useAppleHealth,
  200. let sampleType = Config.healthCarbObject,
  201. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  202. carbs.isNotEmpty
  203. else { return }
  204. do {
  205. // Create HealthKit samples from all the passed carb values
  206. let carbSamples = carbs.compactMap { carbSample -> HKQuantitySample? in
  207. guard let id = carbSample.id else { return nil }
  208. let carbValue = carbSample.carbs
  209. return HKQuantitySample(
  210. type: sampleType,
  211. quantity: HKQuantity(unit: .gram(), doubleValue: Double(carbValue)),
  212. start: carbSample.actualDate ?? Date(),
  213. end: carbSample.actualDate ?? Date(),
  214. metadata: [
  215. HKMetadataKeyExternalUUID: id,
  216. HKMetadataKeySyncIdentifier: id,
  217. HKMetadataKeySyncVersion: 1,
  218. Config.freeAPSMetaKey: true
  219. ]
  220. )
  221. }
  222. guard carbSamples.isNotEmpty else {
  223. debug(.service, "No glucose samples available for upload.")
  224. return
  225. }
  226. // Attempt to save the blood glucose samples to Apple Health
  227. try await healthKitStore.save(carbSamples)
  228. debug(.service, "Successfully stored \(carbSamples.count) carb samples in HealthKit.")
  229. // After successful upload, update the isUploadedToHealth flag in Core Data
  230. await updateCarbsAsUploaded(carbs)
  231. } catch {
  232. debug(.service, "Failed to upload carb samples to HealthKit: \(error.localizedDescription)")
  233. }
  234. }
  235. private func updateCarbsAsUploaded(_ carbs: [CarbsEntry]) async {
  236. await backgroundContext.perform {
  237. let ids = carbs.map(\.id) as NSArray
  238. let fetchRequest: NSFetchRequest<CarbEntryStored> = CarbEntryStored.fetchRequest()
  239. fetchRequest.predicate = NSPredicate(format: "id IN %@", ids)
  240. do {
  241. let results = try self.backgroundContext.fetch(fetchRequest)
  242. for result in results {
  243. result.isUploadedToHealth = true
  244. }
  245. guard self.backgroundContext.hasChanges else { return }
  246. try self.backgroundContext.save()
  247. } catch let error as NSError {
  248. debugPrint(
  249. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update isUploadedToHealth: \(error.userInfo)"
  250. )
  251. }
  252. }
  253. }
  254. func saveIfNeeded(carbs: [CarbsEntry]) {
  255. guard settingsManager.settings.useAppleHealth,
  256. let sampleType = Config.healthCarbObject,
  257. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  258. carbs.isNotEmpty
  259. else { return }
  260. let carbsWithId = carbs.filter { c in
  261. guard c.id != nil else { return false }
  262. return true
  263. }
  264. func save(samples: [HKSample]) {
  265. let sampleIDs = samples.compactMap(\.syncIdentifier)
  266. let sampleDates = samples.map(\.startDate)
  267. let samplesToSave = carbsWithId
  268. .filter { !sampleIDs.contains($0.id ?? "") } // id existing in AH
  269. // .filter { !sampleDates.contains($0.actualDate ?? $0.createdAt) } // not id but exactly the same datetime
  270. .filter { !sampleDates.contains($0.createdAt) } // not id but exactly the same datetime
  271. .map {
  272. HKQuantitySample(
  273. type: sampleType,
  274. quantity: HKQuantity(unit: .gram(), doubleValue: Double($0.carbs)),
  275. start: $0.actualDate ?? $0.createdAt,
  276. end: $0.actualDate ?? $0.createdAt,
  277. metadata: [
  278. HKMetadataKeySyncIdentifier: $0.id ?? "_id",
  279. HKMetadataKeySyncVersion: 1,
  280. Config.freeAPSMetaKey: true
  281. ]
  282. )
  283. }
  284. healthKitStore.save(samplesToSave) { (success: Bool, error: Error?) -> Void in
  285. if !success {
  286. debug(.service, "Failed to store carb entry in HealthKit Store!")
  287. debug(.service, error?.localizedDescription ?? "Unknown error")
  288. }
  289. }
  290. }
  291. loadSamplesFromHealth(sampleType: sampleType, completion: { samples in
  292. save(samples: samples)
  293. })
  294. }
  295. func saveIfNeeded(pumpEvents events: [PumpHistoryEvent]) {
  296. guard settingsManager.settings.useAppleHealth,
  297. let sampleType = Config.healthInsulinObject,
  298. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  299. events.isNotEmpty
  300. else { return }
  301. func save(bolusToModify: [InsulinBolus], bolus: [InsulinBolus], basal: [InsulinBasal]) {
  302. // first step : delete the HK value
  303. // second step : recreate with the new value !
  304. bolusToModify.forEach { syncID in
  305. let predicate = HKQuery.predicateForObjects(
  306. withMetadataKey: HKMetadataKeySyncIdentifier,
  307. operatorType: .equalTo,
  308. value: syncID.id
  309. )
  310. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  311. guard let error = error else { return }
  312. warning(.service, "Cannot delete sample with syncID: \(syncID.id)", error: error)
  313. }
  314. }
  315. let bolusTotal = bolus + bolusToModify
  316. let bolusSamples = bolusTotal
  317. .map {
  318. HKQuantitySample(
  319. type: sampleType,
  320. quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double($0.amount)),
  321. start: $0.date,
  322. end: $0.date,
  323. metadata: [
  324. HKMetadataKeyInsulinDeliveryReason: NSNumber(2),
  325. HKMetadataKeyExternalUUID: $0.id,
  326. HKMetadataKeySyncIdentifier: $0.id,
  327. HKMetadataKeySyncVersion: 1,
  328. Config.freeAPSMetaKey: true
  329. ]
  330. )
  331. }
  332. let basalSamples = basal
  333. .map {
  334. HKQuantitySample(
  335. type: sampleType,
  336. quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double($0.amount)),
  337. start: $0.startDelivery,
  338. end: $0.endDelivery,
  339. metadata: [
  340. HKMetadataKeyInsulinDeliveryReason: NSNumber(1),
  341. HKMetadataKeyExternalUUID: $0.id,
  342. HKMetadataKeySyncIdentifier: $0.id,
  343. HKMetadataKeySyncVersion: 1,
  344. Config.freeAPSMetaKey: true
  345. ]
  346. )
  347. }
  348. healthKitStore.save(bolusSamples + basalSamples) { (success: Bool, error: Error?) -> Void in
  349. if !success {
  350. debug(.service, "Failed to store insulin entry in HealthKit Store!")
  351. debug(.service, error?.localizedDescription ?? "Unknown error")
  352. }
  353. }
  354. }
  355. loadSamplesFromHealth(sampleType: sampleType, withIDs: events.map(\.id), completion: { samples in
  356. let sampleIDs = samples.compactMap(\.syncIdentifier)
  357. let bolusToModify = events
  358. .filter { $0.type == .bolus && sampleIDs.contains($0.id) }
  359. .compactMap { event -> InsulinBolus? in
  360. guard let amount = event.amount else { return nil }
  361. guard let sampleAmount = samples.first(where: { $0.syncIdentifier == event.id }) as? HKQuantitySample
  362. else { return nil }
  363. if Double(amount) != sampleAmount.quantity.doubleValue(for: .internationalUnit()) {
  364. return InsulinBolus(id: sampleAmount.syncIdentifier!, amount: amount, date: event.timestamp)
  365. } else { return nil }
  366. }
  367. let bolus = events
  368. .filter { $0.type == .bolus && !sampleIDs.contains($0.id) }
  369. .compactMap { event -> InsulinBolus? in
  370. guard let amount = event.amount else { return nil }
  371. return InsulinBolus(id: event.id, amount: amount, date: event.timestamp)
  372. }
  373. let basalEvents = events
  374. .filter { $0.type == .tempBasal && !sampleIDs.contains($0.id) }
  375. .sorted(by: { $0.timestamp < $1.timestamp })
  376. let basal = basalEvents.enumerated()
  377. .compactMap { item -> InsulinBasal? in
  378. let nextElementEventIndex = item.offset + 1
  379. guard basalEvents.count > nextElementEventIndex else { return nil }
  380. var minimalDose = self.settingsManager.preferences.bolusIncrement
  381. if (minimalDose != 0.05) || (minimalDose != 0.025) {
  382. minimalDose = Decimal(0.05)
  383. }
  384. let nextBasalEvent = basalEvents[nextElementEventIndex]
  385. let secondsOfCurrentBasal = nextBasalEvent.timestamp.timeIntervalSince(item.element.timestamp)
  386. let amount = Decimal(secondsOfCurrentBasal / 3600) * (item.element.rate ?? 0)
  387. let incrementsRaw = amount / minimalDose
  388. var amountRounded: Decimal
  389. if incrementsRaw >= 1 {
  390. let incrementsRounded = floor(Double(incrementsRaw))
  391. amountRounded = Decimal(round(incrementsRounded * Double(minimalDose) * 100_000.0) / 100_000.0)
  392. } else {
  393. amountRounded = 0
  394. }
  395. let id = String(item.element.id.dropFirst())
  396. guard amountRounded > 0,
  397. id != ""
  398. else { return nil }
  399. return InsulinBasal(
  400. id: id,
  401. amount: amountRounded,
  402. startDelivery: item.element.timestamp,
  403. endDelivery: nextBasalEvent.timestamp
  404. )
  405. }
  406. save(bolusToModify: bolusToModify, bolus: bolus, basal: basal)
  407. })
  408. }
  409. func pumpHistoryDidUpdate(_ events: [PumpHistoryEvent]) {
  410. saveIfNeeded(pumpEvents: events)
  411. }
  412. func createBGObserver() {
  413. guard settingsManager.settings.useAppleHealth else { return }
  414. guard let bgType = Config.healthBGObject else {
  415. warning(.service, "Can not create HealthKit Observer, because unable to get the Blood Glucose type")
  416. return
  417. }
  418. let query = HKObserverQuery(sampleType: bgType, predicate: nil) { [weak self] _, _, observerError in
  419. guard let self = self else { return }
  420. debug(.service, "Execute HealthKit observer query for loading increment samples")
  421. guard observerError == nil else {
  422. warning(.service, "Error during execution of HealthKit Observer's query", error: observerError!)
  423. return
  424. }
  425. if let incrementQuery = self.getBloodGlucoseHKQuery(predicate: self.loadBGPredicate) {
  426. debug(.service, "Create increment query")
  427. self.healthKitStore.execute(incrementQuery)
  428. }
  429. }
  430. healthKitStore.execute(query)
  431. debug(.service, "Create Observer for Blood Glucose")
  432. }
  433. func enableBackgroundDelivery() {
  434. guard settingsManager.settings.useAppleHealth else {
  435. healthKitStore.disableAllBackgroundDelivery { _, _ in }
  436. return }
  437. guard let bgType = Config.healthBGObject else {
  438. warning(
  439. .service,
  440. "Can not create background delivery, because unable to get the Blood Glucose type"
  441. )
  442. return
  443. }
  444. healthKitStore.enableBackgroundDelivery(for: bgType, frequency: .immediate) { status, error in
  445. guard error == nil else {
  446. warning(.service, "Can not enable background delivery", error: error)
  447. return
  448. }
  449. debug(.service, "Background delivery status is \(status)")
  450. }
  451. }
  452. /// Try to load samples from Health store
  453. private func loadSamplesFromHealth(
  454. sampleType: HKQuantityType,
  455. limit: Int = 100,
  456. completion: @escaping (_ samples: [HKSample]) -> Void
  457. ) {
  458. let query = HKSampleQuery(
  459. sampleType: sampleType,
  460. predicate: nil,
  461. limit: limit,
  462. sortDescriptors: nil
  463. ) { _, results, _ in
  464. completion(results as? [HKQuantitySample] ?? [])
  465. }
  466. healthKitStore.execute(query)
  467. }
  468. /// Try to load samples from Health store with id and do some work
  469. private func loadSamplesFromHealth(
  470. sampleType: HKQuantityType,
  471. withIDs ids: [String],
  472. limit: Int = 100,
  473. completion: @escaping (_ samples: [HKSample]) -> Void
  474. ) {
  475. let predicate = HKQuery.predicateForObjects(
  476. withMetadataKey: HKMetadataKeySyncIdentifier,
  477. allowedValues: ids
  478. )
  479. let query = HKSampleQuery(
  480. sampleType: sampleType,
  481. predicate: predicate,
  482. limit: limit,
  483. sortDescriptors: nil
  484. ) { _, results, _ in
  485. completion(results as? [HKQuantitySample] ?? [])
  486. }
  487. healthKitStore.execute(query)
  488. }
  489. private func getBloodGlucoseHKQuery(predicate: NSPredicate) -> HKQuery? {
  490. guard let sampleType = Config.healthBGObject else { return nil }
  491. let query = HKAnchoredObjectQuery(
  492. type: sampleType,
  493. predicate: predicate,
  494. anchor: lastBloodGlucoseQueryAnchor,
  495. limit: HKObjectQueryNoLimit
  496. ) { [weak self] _, addedObjects, _, anchor, _ in
  497. guard let self = self else { return }
  498. self.processQueue.async {
  499. debug(.service, "AnchoredQuery did execute")
  500. self.lastBloodGlucoseQueryAnchor = anchor
  501. // Added objects
  502. if let bgSamples = addedObjects as? [HKQuantitySample],
  503. bgSamples.isNotEmpty
  504. {
  505. self.prepareBGSamplesToPublisherFetch(bgSamples)
  506. }
  507. }
  508. }
  509. return query
  510. }
  511. private func prepareBGSamplesToPublisherFetch(_ samples: [HKQuantitySample]) {
  512. dispatchPrecondition(condition: .onQueue(processQueue))
  513. newGlucose += samples
  514. .compactMap { sample -> HealthKitSample? in
  515. let fromTrio = sample.metadata?[Config.freeAPSMetaKey] as? Bool ?? false
  516. guard !fromTrio else { return nil }
  517. return HealthKitSample(
  518. healthKitId: sample.uuid.uuidString,
  519. date: sample.startDate,
  520. glucose: Int(round(sample.quantity.doubleValue(for: .milligramsPerDeciliter)))
  521. )
  522. }
  523. .map { sample in
  524. BloodGlucose(
  525. _id: sample.healthKitId,
  526. sgv: sample.glucose,
  527. direction: nil,
  528. date: Decimal(Int(sample.date.timeIntervalSince1970) * 1000),
  529. dateString: sample.date,
  530. unfiltered: Decimal(sample.glucose),
  531. filtered: nil,
  532. noise: nil,
  533. glucose: sample.glucose,
  534. type: "sgv"
  535. )
  536. }
  537. .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
  538. newGlucose = newGlucose.removeDublicates()
  539. }
  540. // MARK: - GlucoseSource
  541. var glucoseManager: FetchGlucoseManager?
  542. var cgmManager: CGMManagerUI?
  543. func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
  544. Future { [weak self] promise in
  545. guard let self = self else {
  546. promise(.success([]))
  547. return
  548. }
  549. self.processQueue.async {
  550. guard self.settingsManager.settings.useAppleHealth else {
  551. promise(.success([]))
  552. return
  553. }
  554. // Remove old BGs
  555. self.newGlucose = self.newGlucose
  556. .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
  557. // Get actual BGs (beetwen Date() - 1 day and Date())
  558. let actualGlucose = self.newGlucose
  559. .filter { $0.dateString <= Date() }
  560. // Update newGlucose
  561. self.newGlucose = self.newGlucose
  562. .filter { !actualGlucose.contains($0) }
  563. // debug(.service, "Actual glucose is \(actualGlucose)")
  564. // debug(.service, "Current state of newGlucose is \(self.newGlucose)")
  565. promise(.success(actualGlucose))
  566. }
  567. }
  568. .eraseToAnyPublisher()
  569. }
  570. func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
  571. fetch(nil)
  572. }
  573. func deleteGlucose(syncID: String) {
  574. guard settingsManager.settings.useAppleHealth,
  575. let sampleType = Config.healthBGObject,
  576. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  577. else { return }
  578. processQueue.async {
  579. let predicate = HKQuery.predicateForObjects(
  580. withMetadataKey: HKMetadataKeySyncIdentifier,
  581. operatorType: .equalTo,
  582. value: syncID
  583. )
  584. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  585. guard let error = error else { return }
  586. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  587. }
  588. }
  589. }
  590. // - MARK Carbs function
  591. func deleteCarbs(syncID: String, fpuID: String) {
  592. guard settingsManager.settings.useAppleHealth,
  593. let sampleType = Config.healthCarbObject,
  594. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  595. else { return }
  596. print("meals 4: ID: " + syncID + " FPU ID: " + fpuID)
  597. if syncID != "" {
  598. let predicate = HKQuery.predicateForObjects(
  599. withMetadataKey: HKMetadataKeySyncIdentifier,
  600. operatorType: .equalTo,
  601. value: syncID
  602. )
  603. healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  604. guard let error = error else { return }
  605. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  606. }
  607. }
  608. if fpuID != "" {
  609. // processQueue.async {
  610. let recentCarbs: [CarbsEntry] = carbsStorage.recent()
  611. let ids = recentCarbs.filter { $0.fpuID == fpuID }.compactMap(\.id)
  612. let predicate = HKQuery.predicateForObjects(
  613. withMetadataKey: HKMetadataKeySyncIdentifier,
  614. allowedValues: ids
  615. )
  616. print("found IDs: " + ids.description)
  617. healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  618. guard let error = error else { return }
  619. warning(.service, "Cannot delete sample with fpuID: \(fpuID)", error: error)
  620. }
  621. // }
  622. }
  623. }
  624. func carbsDidUpdate(_ carbs: [CarbsEntry]) {
  625. saveIfNeeded(carbs: carbs)
  626. }
  627. // - MARK Insulin function
  628. func deleteInsulin(syncID: String) {
  629. guard settingsManager.settings.useAppleHealth,
  630. let sampleType = Config.healthInsulinObject,
  631. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  632. else { return }
  633. processQueue.async {
  634. let predicate = HKQuery.predicateForObjects(
  635. withMetadataKey: HKMetadataKeySyncIdentifier,
  636. operatorType: .equalTo,
  637. value: syncID
  638. )
  639. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  640. guard let error = error else { return }
  641. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  642. }
  643. }
  644. }
  645. }
  646. enum HealthKitPermissionRequestStatus {
  647. case needRequest
  648. case didRequest
  649. }
  650. enum HKError: Error {
  651. // HealthKit work only iPhone (not on iPad)
  652. case notAvailableOnCurrentDevice
  653. // Some data can be not available on current iOS-device
  654. case dataNotAvailable
  655. }
  656. private struct InsulinBolus {
  657. var id: String
  658. var amount: Decimal
  659. var date: Date
  660. }
  661. private struct InsulinBasal {
  662. var id: String
  663. var amount: Decimal
  664. var startDelivery: Date
  665. var endDelivery: Date
  666. }