HealthKitManager.swift 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. import Combine
  2. import Foundation
  3. import HealthKit
  4. import LoopKit
  5. import LoopKitUI
  6. import Swinject
  7. protocol HealthKitManager: GlucoseSource {
  8. /// Check all needed permissions
  9. /// Return false if one or more permissions are deny or not choosen
  10. var areAllowAllPermissions: Bool { get }
  11. /// Check availability to save data of BG type to Health store
  12. func checkAvailabilitySaveBG() -> Bool
  13. /// Requests user to give permissions on using HealthKit
  14. func requestPermission(completion: ((Bool, Error?) -> Void)?)
  15. /// Save blood glucose to Health store (dublicate of bg will ignore)
  16. func saveIfNeeded(bloodGlucose: [BloodGlucose])
  17. /// Save carbs to Health store (dublicate of bg will ignore)
  18. func saveIfNeeded(carbs: [CarbsEntry])
  19. /// Save Insulin to Health store
  20. func saveIfNeeded(pumpEvents events: [PumpHistoryEvent])
  21. /// Create observer for data passing beetwen Health Store and FreeAPS
  22. func createBGObserver()
  23. /// Enable background delivering objects from Apple Health to FreeAPS
  24. func enableBackgroundDelivery()
  25. /// Delete glucose with syncID
  26. func deleteGlucose(syncID: String)
  27. /// delete carbs with syncID
  28. func deleteCarbs(syncID: String)
  29. /// delete insulin with syncID
  30. func deleteInsulin(syncID: String)
  31. }
  32. final class BaseHealthKitManager: HealthKitManager, Injectable, CarbsObserver {
  33. private enum Config {
  34. // unwraped HKObjects
  35. static var readPermissions: Set<HKSampleType> {
  36. Set([healthBGObject].compactMap { $0 }) }
  37. static var writePermissions: Set<HKSampleType> {
  38. Set([healthBGObject, healthCarbObject, healthInsulinObject].compactMap { $0 }) }
  39. // link to object in HealthKit
  40. static let healthBGObject = HKObjectType.quantityType(forIdentifier: .bloodGlucose)
  41. static let healthCarbObject = HKObjectType.quantityType(forIdentifier: .dietaryCarbohydrates)
  42. static let healthInsulinObject = HKObjectType.quantityType(forIdentifier: .insulinDelivery)
  43. // Meta-data key of FreeASPX data in HealthStore
  44. static let freeAPSMetaKey = "fromFreeAPSX"
  45. }
  46. @Injected() private var glucoseStorage: GlucoseStorage!
  47. @Injected() private var healthKitStore: HKHealthStore!
  48. @Injected() private var settingsManager: SettingsManager!
  49. @Injected() private var broadcaster: Broadcaster!
  50. private let processQueue = DispatchQueue(label: "BaseHealthKitManager.processQueue")
  51. private var lifetime = Lifetime()
  52. // BG that will be return Publisher
  53. @SyncAccess @Persisted(key: "BaseHealthKitManager.newGlucose") private var newGlucose: [BloodGlucose] = []
  54. // last anchor for HKAnchoredQuery
  55. private var lastBloodGlucoseQueryAnchor: HKQueryAnchor? {
  56. set {
  57. persistedBGAnchor = try? NSKeyedArchiver.archivedData(withRootObject: newValue as Any, requiringSecureCoding: false)
  58. }
  59. get {
  60. guard let data = persistedBGAnchor else { return nil }
  61. return try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? HKQueryAnchor
  62. }
  63. }
  64. @Persisted(key: "HealthKitManagerAnchor") private var persistedBGAnchor: Data? = nil
  65. var isAvailableOnCurrentDevice: Bool {
  66. HKHealthStore.isHealthDataAvailable()
  67. }
  68. var areAllowAllPermissions: Bool {
  69. Set(Config.readPermissions.map { healthKitStore.authorizationStatus(for: $0) })
  70. .intersection([.notDetermined])
  71. .isEmpty &&
  72. Set(Config.writePermissions.map { healthKitStore.authorizationStatus(for: $0) })
  73. .intersection([.sharingDenied, .notDetermined])
  74. .isEmpty
  75. }
  76. // NSPredicate, which use during load increment BG from Health store
  77. private var loadBGPredicate: NSPredicate {
  78. // loading only daily bg
  79. let predicateByStartDate = HKQuery.predicateForSamples(
  80. withStart: Date().addingTimeInterval(-1.days.timeInterval),
  81. end: nil,
  82. options: .strictStartDate
  83. )
  84. // loading only not FreeAPS bg
  85. // this predicate dont influence on Deleted Objects, only on added
  86. let predicateByMeta = HKQuery.predicateForObjects(
  87. withMetadataKey: Config.freeAPSMetaKey,
  88. operatorType: .notEqualTo,
  89. value: 1
  90. )
  91. return NSCompoundPredicate(andPredicateWithSubpredicates: [predicateByStartDate, predicateByMeta])
  92. }
  93. init(resolver: Resolver) {
  94. injectServices(resolver)
  95. guard isAvailableOnCurrentDevice,
  96. Config.healthBGObject != nil else { return }
  97. createBGObserver()
  98. enableBackgroundDelivery()
  99. broadcaster.register(CarbsObserver.self, observer: self)
  100. debug(.service, "HealthKitManager did create")
  101. }
  102. func checkAvailabilitySave(objectTypeToHealthStore: HKObjectType) -> Bool {
  103. healthKitStore.authorizationStatus(for: objectTypeToHealthStore) == .sharingAuthorized
  104. }
  105. func checkAvailabilitySaveBG() -> Bool {
  106. Config.healthBGObject.map { checkAvailabilitySave(objectTypeToHealthStore: $0) } ?? false
  107. }
  108. func requestPermission(completion: ((Bool, Error?) -> Void)? = nil) {
  109. guard isAvailableOnCurrentDevice else {
  110. completion?(false, HKError.notAvailableOnCurrentDevice)
  111. return
  112. }
  113. guard Config.readPermissions.isNotEmpty, Config.writePermissions.isNotEmpty else {
  114. completion?(false, HKError.dataNotAvailable)
  115. return
  116. }
  117. healthKitStore.requestAuthorization(toShare: Config.writePermissions, read: Config.readPermissions) { status, error in
  118. completion?(status, error)
  119. }
  120. }
  121. func saveIfNeeded(bloodGlucose: [BloodGlucose]) {
  122. guard settingsManager.settings.useAppleHealth,
  123. let sampleType = Config.healthBGObject,
  124. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  125. bloodGlucose.isNotEmpty
  126. else { return }
  127. func save(samples: [HKSample]) {
  128. let sampleIDs = samples.compactMap(\.syncIdentifier)
  129. let samplesToSave = bloodGlucose
  130. .filter { !sampleIDs.contains($0.id) }
  131. .map {
  132. HKQuantitySample(
  133. type: sampleType,
  134. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double($0.glucose!)),
  135. start: $0.dateString,
  136. end: $0.dateString,
  137. metadata: [
  138. HKMetadataKeyExternalUUID: $0.id,
  139. HKMetadataKeySyncIdentifier: $0.id,
  140. HKMetadataKeySyncVersion: 1,
  141. Config.freeAPSMetaKey: true
  142. ]
  143. )
  144. }
  145. healthKitStore.save(samplesToSave) { _, _ in }
  146. }
  147. loadSamplesFromHealth(sampleType: sampleType, withIDs: bloodGlucose.map(\.id))
  148. .receive(on: processQueue)
  149. .sink(receiveValue: save)
  150. .store(in: &lifetime)
  151. }
  152. func saveIfNeeded(carbs: [CarbsEntry]) {
  153. guard settingsManager.settings.useAppleHealth,
  154. let sampleType = Config.healthCarbObject,
  155. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  156. carbs.isNotEmpty
  157. else { return }
  158. let carbsWithId = carbs.filter { c in
  159. guard c.id != nil else { return false }
  160. return true
  161. }
  162. func save(samples: [HKSample]) {
  163. let sampleIDs = samples.compactMap(\.syncIdentifier)
  164. let sampleDates = samples.map(\.startDate)
  165. let samplesToSave = carbsWithId
  166. .filter { !sampleIDs.contains($0.id!) } // id existing in AH
  167. .filter { !sampleDates.contains($0.createdAt) } // not id but exaclty the same datetime
  168. .map {
  169. HKQuantitySample(
  170. type: sampleType,
  171. quantity: HKQuantity(unit: .gram(), doubleValue: Double($0.carbs)),
  172. start: $0.createdAt,
  173. end: $0.createdAt,
  174. metadata: [
  175. HKMetadataKeyExternalUUID: $0.id ?? "_id",
  176. HKMetadataKeySyncIdentifier: $0.id ?? "_id",
  177. HKMetadataKeySyncVersion: 1,
  178. Config.freeAPSMetaKey: true
  179. ]
  180. )
  181. }
  182. healthKitStore.save(samplesToSave) { _, _ in }
  183. }
  184. loadSamplesFromHealth(sampleType: sampleType)
  185. .receive(on: processQueue)
  186. .sink(receiveValue: save)
  187. .store(in: &lifetime)
  188. }
  189. func saveIfNeeded(pumpEvents events: [PumpHistoryEvent]) {
  190. guard settingsManager.settings.useAppleHealth,
  191. let sampleType = Config.healthInsulinObject,
  192. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  193. events.isNotEmpty
  194. else { return }
  195. func save(bolus: [InsulinBolus], basal: [InsulinBasal]) {
  196. let bolusSamples = bolus
  197. .map {
  198. HKQuantitySample(
  199. type: sampleType,
  200. quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double($0.amount)),
  201. start: $0.date,
  202. end: $0.date,
  203. metadata: [
  204. HKMetadataKeyInsulinDeliveryReason: NSNumber(2),
  205. HKMetadataKeyExternalUUID: $0.id,
  206. HKMetadataKeySyncIdentifier: $0.id,
  207. HKMetadataKeySyncVersion: 1,
  208. Config.freeAPSMetaKey: true
  209. ]
  210. )
  211. }
  212. let basalSamples = basal
  213. .map {
  214. HKQuantitySample(
  215. type: sampleType,
  216. quantity: HKQuantity(unit: .internationalUnit(), doubleValue: Double($0.amount)),
  217. start: $0.startDelivery,
  218. end: $0.endDelivery,
  219. metadata: [
  220. HKMetadataKeyInsulinDeliveryReason: NSNumber(1),
  221. HKMetadataKeyExternalUUID: $0.id,
  222. HKMetadataKeySyncIdentifier: $0.id,
  223. HKMetadataKeySyncVersion: 1,
  224. Config.freeAPSMetaKey: true
  225. ]
  226. )
  227. }
  228. healthKitStore.save(bolusSamples + basalSamples) { _, _ in }
  229. }
  230. loadSamplesFromHealth(sampleType: sampleType, withIDs: events.map(\.id))
  231. .receive(on: processQueue)
  232. .compactMap { samples -> ([InsulinBolus], [InsulinBasal]) in
  233. let sampleIDs = samples.compactMap(\.syncIdentifier)
  234. let bolus = events
  235. .filter { $0.type == .bolus && !sampleIDs.contains($0.id) }
  236. .compactMap { event -> InsulinBolus? in
  237. guard let amount = event.amount else { return nil }
  238. return InsulinBolus(id: event.id, amount: amount, date: event.timestamp)
  239. }
  240. let basalEvents = events
  241. .filter { $0.type == .tempBasal && !sampleIDs.contains($0.id) }
  242. let basal = basalEvents.enumerated()
  243. .compactMap { item -> InsulinBasal? in
  244. let nextElementEventIndex = item.offset + 1
  245. guard basalEvents.count > nextElementEventIndex else { return nil }
  246. let nextBasalEvent = basalEvents[nextElementEventIndex]
  247. let secondsOfCurrentBasal = nextBasalEvent.timestamp.timeIntervalSince(item.element.timestamp)
  248. let amount = Decimal(secondsOfCurrentBasal / 3600) * (item.element.rate ?? 0)
  249. let id = String(item.element.id.dropFirst())
  250. guard amount > 0,
  251. id != ""
  252. else { return nil }
  253. return InsulinBasal(
  254. id: id,
  255. amount: amount,
  256. startDelivery: item.element.timestamp,
  257. endDelivery: nextBasalEvent.timestamp
  258. )
  259. }
  260. return (bolus, basal)
  261. }
  262. .sink(receiveValue: save)
  263. .store(in: &lifetime)
  264. }
  265. func createBGObserver() {
  266. guard settingsManager.settings.useAppleHealth else { return }
  267. guard let bgType = Config.healthBGObject else {
  268. warning(.service, "Can not create HealthKit Observer, because unable to get the Blood Glucose type")
  269. return
  270. }
  271. let query = HKObserverQuery(sampleType: bgType, predicate: nil) { [weak self] _, _, observerError in
  272. guard let self = self else { return }
  273. debug(.service, "Execute HelathKit observer query for loading increment samples")
  274. guard observerError == nil else {
  275. warning(.service, "Error during execution of HelathKit Observer's query", error: observerError!)
  276. return
  277. }
  278. if let incrementQuery = self.getBloodGlucoseHKQuery(predicate: self.loadBGPredicate) {
  279. debug(.service, "Create increment query")
  280. self.healthKitStore.execute(incrementQuery)
  281. }
  282. }
  283. healthKitStore.execute(query)
  284. debug(.service, "Create Observer for Blood Glucose")
  285. }
  286. func enableBackgroundDelivery() {
  287. guard settingsManager.settings.useAppleHealth else {
  288. healthKitStore.disableAllBackgroundDelivery { _, _ in }
  289. return }
  290. guard let bgType = Config.healthBGObject else {
  291. warning(
  292. .service,
  293. "Can not create background delivery, because unable to get the Blood Glucose type"
  294. )
  295. return
  296. }
  297. healthKitStore.enableBackgroundDelivery(for: bgType, frequency: .immediate) { status, error in
  298. guard error == nil else {
  299. warning(.service, "Can not enable background delivery", error: error)
  300. return
  301. }
  302. debug(.service, "Background delivery status is \(status)")
  303. }
  304. }
  305. /// Try to load samples from Health store
  306. private func loadSamplesFromHealth(
  307. sampleType: HKQuantityType
  308. ) -> Future<[HKSample], Never> {
  309. Future { promise in
  310. let query = HKSampleQuery(
  311. sampleType: sampleType,
  312. predicate: nil,
  313. limit: 1000,
  314. sortDescriptors: nil
  315. ) { _, results, _ in
  316. promise(.success((results as? [HKQuantitySample]) ?? []))
  317. }
  318. self.healthKitStore.execute(query)
  319. }
  320. }
  321. /// Try to load samples from Health store with id and do some work
  322. private func loadSamplesFromHealth(
  323. sampleType: HKQuantityType,
  324. withIDs ids: [String]
  325. ) -> Future<[HKSample], Never> {
  326. Future { promise in
  327. let predicate = HKQuery.predicateForObjects(
  328. withMetadataKey: HKMetadataKeySyncIdentifier,
  329. allowedValues: ids
  330. )
  331. let query = HKSampleQuery(
  332. sampleType: sampleType,
  333. predicate: predicate,
  334. limit: 1000,
  335. sortDescriptors: nil
  336. ) { _, results, _ in
  337. promise(.success((results as? [HKQuantitySample]) ?? []))
  338. }
  339. self.healthKitStore.execute(query)
  340. }
  341. }
  342. private func getBloodGlucoseHKQuery(predicate: NSPredicate) -> HKQuery? {
  343. guard let sampleType = Config.healthBGObject else { return nil }
  344. let query = HKAnchoredObjectQuery(
  345. type: sampleType,
  346. predicate: predicate,
  347. anchor: lastBloodGlucoseQueryAnchor,
  348. limit: HKObjectQueryNoLimit
  349. ) { [weak self] _, addedObjects, _, anchor, _ in
  350. guard let self = self else { return }
  351. self.processQueue.async {
  352. debug(.service, "AnchoredQuery did execute")
  353. self.lastBloodGlucoseQueryAnchor = anchor
  354. // Added objects
  355. if let bgSamples = addedObjects as? [HKQuantitySample],
  356. bgSamples.isNotEmpty
  357. {
  358. self.prepareBGSamplesToPublisherFetch(bgSamples)
  359. }
  360. }
  361. }
  362. return query
  363. }
  364. private func prepareBGSamplesToPublisherFetch(_ samples: [HKQuantitySample]) {
  365. dispatchPrecondition(condition: .onQueue(processQueue))
  366. debug(.service, "Start preparing samples: \(String(describing: samples))")
  367. newGlucose += samples
  368. .compactMap { sample -> HealthKitSample? in
  369. let fromFAX = sample.metadata?[Config.freeAPSMetaKey] as? Bool ?? false
  370. guard !fromFAX else { return nil }
  371. return HealthKitSample(
  372. healthKitId: sample.uuid.uuidString,
  373. date: sample.startDate,
  374. glucose: Int(round(sample.quantity.doubleValue(for: .milligramsPerDeciliter)))
  375. )
  376. }
  377. .map { sample in
  378. BloodGlucose(
  379. _id: sample.healthKitId,
  380. sgv: sample.glucose,
  381. direction: nil,
  382. date: Decimal(Int(sample.date.timeIntervalSince1970) * 1000),
  383. dateString: sample.date,
  384. unfiltered: nil,
  385. filtered: nil,
  386. noise: nil,
  387. glucose: sample.glucose,
  388. type: "sgv"
  389. )
  390. }
  391. .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
  392. newGlucose = newGlucose.removeDublicates()
  393. debug(
  394. .service,
  395. "Current BloodGlucose.Type objects will be send from Publisher during fetch: \(String(describing: newGlucose))"
  396. )
  397. }
  398. // MARK: - GlucoseSource
  399. var glucoseManager: FetchGlucoseManager?
  400. var cgmManager: CGMManagerUI?
  401. var cgmType: CGMType = .nightscout
  402. func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
  403. Future { [weak self] promise in
  404. guard let self = self else {
  405. promise(.success([]))
  406. return
  407. }
  408. self.processQueue.async {
  409. // debug(.service, "Start fetching HealthKitManager")
  410. guard self.settingsManager.settings.useAppleHealth else {
  411. debug(.service, "HealthKitManager cant return any data, because useAppleHealth option is disable")
  412. promise(.success([]))
  413. return
  414. }
  415. // Remove old BGs
  416. self.newGlucose = self.newGlucose
  417. .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
  418. // Get actual BGs (beetwen Date() - 1 day and Date())
  419. let actualGlucose = self.newGlucose
  420. .filter { $0.dateString <= Date() }
  421. // Update newGlucose
  422. self.newGlucose = self.newGlucose
  423. .filter { !actualGlucose.contains($0) }
  424. // debug(.service, "Actual glucose is \(actualGlucose)")
  425. // debug(.service, "Current state of newGlucose is \(self.newGlucose)")
  426. promise(.success(actualGlucose))
  427. }
  428. }
  429. .eraseToAnyPublisher()
  430. }
  431. func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
  432. fetch(nil)
  433. }
  434. func deleteGlucose(syncID: String) {
  435. guard settingsManager.settings.useAppleHealth,
  436. let sampleType = Config.healthBGObject,
  437. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  438. else { return }
  439. processQueue.async {
  440. let predicate = HKQuery.predicateForObjects(
  441. withMetadataKey: HKMetadataKeySyncIdentifier,
  442. operatorType: .equalTo,
  443. value: syncID
  444. )
  445. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  446. guard let error = error else { return }
  447. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  448. }
  449. }
  450. }
  451. // - MARK Carbs function
  452. func deleteCarbs(syncID: String) {
  453. guard settingsManager.settings.useAppleHealth,
  454. let sampleType = Config.healthCarbObject,
  455. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  456. else { return }
  457. processQueue.async {
  458. let predicate = HKQuery.predicateForObjects(
  459. withMetadataKey: HKMetadataKeySyncIdentifier,
  460. operatorType: .equalTo,
  461. value: syncID
  462. )
  463. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  464. guard let error = error else { return }
  465. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  466. }
  467. }
  468. }
  469. func carbsDidUpdate(_ carbs: [CarbsEntry]) {
  470. saveIfNeeded(carbs: carbs)
  471. }
  472. // - MARK Insulin function
  473. func deleteInsulin(syncID: String) {
  474. guard settingsManager.settings.useAppleHealth,
  475. let sampleType = Config.healthInsulinObject,
  476. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  477. else { return }
  478. processQueue.async {
  479. let predicate = HKQuery.predicateForObjects(
  480. withMetadataKey: HKMetadataKeySyncIdentifier,
  481. operatorType: .equalTo,
  482. value: syncID
  483. )
  484. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  485. guard let error = error else { return }
  486. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  487. }
  488. }
  489. }
  490. }
  491. enum HealthKitPermissionRequestStatus {
  492. case needRequest
  493. case didRequest
  494. }
  495. enum HKError: Error {
  496. // HealthKit work only iPhone (not on iPad)
  497. case notAvailableOnCurrentDevice
  498. // Some data can be not available on current iOS-device
  499. case dataNotAvailable
  500. }
  501. private struct InsulinBolus {
  502. var id: String
  503. var amount: Decimal
  504. var date: Date
  505. }
  506. private struct InsulinBasal {
  507. var id: String
  508. var amount: Decimal
  509. var startDelivery: Date
  510. var endDelivery: Date
  511. }