HealthKitManager.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import Combine
  2. import Foundation
  3. import HealthKit
  4. import LoopKitUI
  5. import Swinject
  6. protocol HealthKitManager: GlucoseSource {
  7. /// Check all needed permissions
  8. /// Return false if one or more permissions are deny or not choosen
  9. var areAllowAllPermissions: Bool { get }
  10. /// Check availability to save data of BG type to Health store
  11. func checkAvailabilitySaveBG() -> Bool
  12. /// Requests user to give permissions on using HealthKit
  13. func requestPermission(completion: ((Bool, Error?) -> Void)?)
  14. /// Save blood glucose to Health store (dublicate of bg will ignore)
  15. func saveIfNeeded(bloodGlucose: [BloodGlucose])
  16. /// Create observer for data passing beetwen Health Store and FreeAPS
  17. func createObserver()
  18. /// Enable background delivering objects from Apple Health to FreeAPS
  19. func enableBackgroundDelivery()
  20. /// Delete glucose with syncID
  21. func deleteGlucise(syncID: String)
  22. }
  23. final class BaseHealthKitManager: HealthKitManager, Injectable {
  24. private enum Config {
  25. // unwraped HKObjects
  26. static var permissions: Set<HKSampleType> { Set([healthBGObject].compactMap { $0 }) }
  27. // link to object in HealthKit
  28. static let healthBGObject = HKObjectType.quantityType(forIdentifier: .bloodGlucose)
  29. // Meta-data key of FreeASPX data in HealthStore
  30. static let freeAPSMetaKey = "fromFreeAPSX"
  31. }
  32. @Injected() private var glucoseStorage: GlucoseStorage!
  33. @Injected() private var healthKitStore: HKHealthStore!
  34. @Injected() private var settingsManager: SettingsManager!
  35. private let processQueue = DispatchQueue(label: "BaseHealthKitManager.processQueue")
  36. private var lifetime = Lifetime()
  37. // BG that will be return Publisher
  38. @SyncAccess @Persisted(key: "BaseHealthKitManager.newGlucose") private var newGlucose: [BloodGlucose] = []
  39. // last anchor for HKAnchoredQuery
  40. private var lastBloodGlucoseQueryAnchor: HKQueryAnchor? {
  41. set {
  42. persistedAnchor = try? NSKeyedArchiver.archivedData(withRootObject: newValue as Any, requiringSecureCoding: false)
  43. }
  44. get {
  45. guard let data = persistedAnchor else { return nil }
  46. return try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? HKQueryAnchor
  47. }
  48. }
  49. @Persisted(key: "HealthKitManagerAnchor") private var persistedAnchor: Data? = nil
  50. var isAvailableOnCurrentDevice: Bool {
  51. HKHealthStore.isHealthDataAvailable()
  52. }
  53. var areAllowAllPermissions: Bool {
  54. Set(Config.permissions.map { healthKitStore.authorizationStatus(for: $0) })
  55. .intersection([.sharingDenied, .notDetermined])
  56. .isEmpty
  57. }
  58. // NSPredicate, which use during load increment BG from Health store
  59. private var loadBGPredicate: NSPredicate {
  60. // loading only daily bg
  61. let predicateByStartDate = HKQuery.predicateForSamples(
  62. withStart: Date().addingTimeInterval(-1.days.timeInterval),
  63. end: nil,
  64. options: .strictStartDate
  65. )
  66. // loading only not FreeAPS bg
  67. // this predicate dont influence on Deleted Objects, only on added
  68. let predicateByMeta = HKQuery.predicateForObjects(
  69. withMetadataKey: Config.freeAPSMetaKey,
  70. operatorType: .notEqualTo,
  71. value: 1
  72. )
  73. return NSCompoundPredicate(andPredicateWithSubpredicates: [predicateByStartDate, predicateByMeta])
  74. }
  75. init(resolver: Resolver) {
  76. injectServices(resolver)
  77. guard isAvailableOnCurrentDevice,
  78. Config.healthBGObject != nil else { return }
  79. createObserver()
  80. enableBackgroundDelivery()
  81. debug(.service, "HealthKitManager did create")
  82. }
  83. func checkAvailabilitySave(objectTypeToHealthStore: HKObjectType) -> Bool {
  84. healthKitStore.authorizationStatus(for: objectTypeToHealthStore) == .sharingAuthorized
  85. }
  86. func checkAvailabilitySaveBG() -> Bool {
  87. Config.healthBGObject.map { checkAvailabilitySave(objectTypeToHealthStore: $0) } ?? false
  88. }
  89. func requestPermission(completion: ((Bool, Error?) -> Void)? = nil) {
  90. guard isAvailableOnCurrentDevice else {
  91. completion?(false, HKError.notAvailableOnCurrentDevice)
  92. return
  93. }
  94. guard Config.permissions.isNotEmpty else {
  95. completion?(false, HKError.dataNotAvailable)
  96. return
  97. }
  98. healthKitStore.requestAuthorization(toShare: Config.permissions, read: Config.permissions) { status, error in
  99. completion?(status, error)
  100. }
  101. }
  102. func saveIfNeeded(bloodGlucose: [BloodGlucose]) {
  103. guard settingsManager.settings.useAppleHealth,
  104. let sampleType = Config.healthBGObject,
  105. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  106. bloodGlucose.isNotEmpty
  107. else { return }
  108. func save(samples: [HKSample]) {
  109. let sampleIDs = samples.compactMap(\.syncIdentifier)
  110. let samplesToSave = bloodGlucose
  111. .filter { !sampleIDs.contains($0.id) }
  112. .map {
  113. HKQuantitySample(
  114. type: sampleType,
  115. quantity: HKQuantity(unit: .milligramsPerDeciliter, doubleValue: Double($0.glucose!)),
  116. start: $0.dateString,
  117. end: $0.dateString,
  118. metadata: [
  119. HKMetadataKeyExternalUUID: $0.id,
  120. HKMetadataKeySyncIdentifier: $0.id,
  121. HKMetadataKeySyncVersion: 1,
  122. Config.freeAPSMetaKey: true
  123. ]
  124. )
  125. }
  126. healthKitStore.save(samplesToSave) { _, _ in }
  127. }
  128. loadSamplesFromHealth(sampleType: sampleType, withIDs: bloodGlucose.map(\.id))
  129. .receive(on: processQueue)
  130. .sink(receiveValue: save)
  131. .store(in: &lifetime)
  132. }
  133. func createObserver() {
  134. guard settingsManager.settings.useAppleHealth else { return }
  135. guard let bgType = Config.healthBGObject else {
  136. warning(.service, "Can not create HealthKit Observer, because unable to get the Blood Glucose type")
  137. return
  138. }
  139. let query = HKObserverQuery(sampleType: bgType, predicate: nil) { [weak self] _, _, observerError in
  140. guard let self = self else { return }
  141. debug(.service, "Execute HelathKit observer query for loading increment samples")
  142. guard observerError == nil else {
  143. warning(.service, "Error during execution of HelathKit Observer's query", error: observerError!)
  144. return
  145. }
  146. if let incrementQuery = self.getBloodGlucoseHKQuery(predicate: self.loadBGPredicate) {
  147. debug(.service, "Create increment query")
  148. self.healthKitStore.execute(incrementQuery)
  149. }
  150. }
  151. healthKitStore.execute(query)
  152. debug(.service, "Create Observer for Blood Glucose")
  153. }
  154. func enableBackgroundDelivery() {
  155. guard settingsManager.settings.useAppleHealth else {
  156. healthKitStore.disableAllBackgroundDelivery { _, _ in }
  157. return }
  158. guard let bgType = Config.healthBGObject else {
  159. warning(
  160. .service,
  161. "Can not create background delivery, because unable to get the Blood Glucose type"
  162. )
  163. return
  164. }
  165. healthKitStore.enableBackgroundDelivery(for: bgType, frequency: .immediate) { status, error in
  166. guard error == nil else {
  167. warning(.service, "Can not enable background delivery", error: error)
  168. return
  169. }
  170. debug(.service, "Background delivery status is \(status)")
  171. }
  172. }
  173. /// Try to load samples from Health store with id and do some work
  174. private func loadSamplesFromHealth(
  175. sampleType: HKQuantityType,
  176. withIDs ids: [String]
  177. ) -> Future<[HKSample], Never> {
  178. Future { promise in
  179. let predicate = HKQuery.predicateForObjects(
  180. withMetadataKey: HKMetadataKeySyncIdentifier,
  181. allowedValues: ids
  182. )
  183. let query = HKSampleQuery(
  184. sampleType: sampleType,
  185. predicate: predicate,
  186. limit: 1000,
  187. sortDescriptors: nil
  188. ) { _, results, _ in
  189. promise(.success((results as? [HKQuantitySample]) ?? []))
  190. }
  191. self.healthKitStore.execute(query)
  192. }
  193. }
  194. private func getBloodGlucoseHKQuery(predicate: NSPredicate) -> HKQuery? {
  195. guard let sampleType = Config.healthBGObject else { return nil }
  196. let query = HKAnchoredObjectQuery(
  197. type: sampleType,
  198. predicate: predicate,
  199. anchor: lastBloodGlucoseQueryAnchor,
  200. limit: HKObjectQueryNoLimit
  201. ) { [weak self] _, addedObjects, _, anchor, _ in
  202. guard let self = self else { return }
  203. self.processQueue.async {
  204. debug(.service, "AnchoredQuery did execute")
  205. self.lastBloodGlucoseQueryAnchor = anchor
  206. // Added objects
  207. if let bgSamples = addedObjects as? [HKQuantitySample],
  208. bgSamples.isNotEmpty
  209. {
  210. self.prepareSamplesToPublisherFetch(bgSamples)
  211. }
  212. }
  213. }
  214. return query
  215. }
  216. private func prepareSamplesToPublisherFetch(_ samples: [HKQuantitySample]) {
  217. dispatchPrecondition(condition: .onQueue(processQueue))
  218. debug(.service, "Start preparing samples: \(String(describing: samples))")
  219. newGlucose += samples
  220. .compactMap { sample -> HealthKitSample? in
  221. let fromFAX = sample.metadata?[Config.freeAPSMetaKey] as? Bool ?? false
  222. guard !fromFAX else { return nil }
  223. return HealthKitSample(
  224. healthKitId: sample.uuid.uuidString,
  225. date: sample.startDate,
  226. glucose: Int(round(sample.quantity.doubleValue(for: .milligramsPerDeciliter)))
  227. )
  228. }
  229. .map { sample in
  230. BloodGlucose(
  231. _id: sample.healthKitId,
  232. sgv: sample.glucose,
  233. direction: nil,
  234. date: Decimal(Int(sample.date.timeIntervalSince1970) * 1000),
  235. dateString: sample.date,
  236. unfiltered: nil,
  237. filtered: nil,
  238. noise: nil,
  239. glucose: sample.glucose,
  240. type: "sgv"
  241. )
  242. }
  243. .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
  244. newGlucose = newGlucose.removeDublicates()
  245. debug(
  246. .service,
  247. "Current BloodGlucose.Type objects will be send from Publisher during fetch: \(String(describing: newGlucose))"
  248. )
  249. }
  250. // MARK: - GlucoseSource
  251. var glucoseManager: FetchGlucoseManager?
  252. var cgmManager: CGMManagerUI?
  253. var cgmType: CGMType = .nightscout
  254. func fetch(_: DispatchTimer?) -> AnyPublisher<[BloodGlucose], Never> {
  255. Future { [weak self] promise in
  256. guard let self = self else {
  257. promise(.success([]))
  258. return
  259. }
  260. self.processQueue.async {
  261. // debug(.service, "Start fetching HealthKitManager")
  262. guard self.settingsManager.settings.useAppleHealth else {
  263. debug(.service, "HealthKitManager cant return any data, because useAppleHealth option is disable")
  264. promise(.success([]))
  265. return
  266. }
  267. // Remove old BGs
  268. self.newGlucose = self.newGlucose
  269. .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
  270. // Get actual BGs (beetwen Date() - 1 day and Date())
  271. let actualGlucose = self.newGlucose
  272. .filter { $0.dateString <= Date() }
  273. // Update newGlucose
  274. self.newGlucose = self.newGlucose
  275. .filter { !actualGlucose.contains($0) }
  276. // debug(.service, "Actual glucose is \(actualGlucose)")
  277. // debug(.service, "Current state of newGlucose is \(self.newGlucose)")
  278. promise(.success(actualGlucose))
  279. }
  280. }
  281. .eraseToAnyPublisher()
  282. }
  283. func fetchIfNeeded() -> AnyPublisher<[BloodGlucose], Never> {
  284. fetch(nil)
  285. }
  286. func deleteGlucise(syncID: String) {
  287. guard settingsManager.settings.useAppleHealth,
  288. let sampleType = Config.healthBGObject,
  289. checkAvailabilitySave(objectTypeToHealthStore: sampleType)
  290. else { return }
  291. processQueue.async {
  292. let predicate = HKQuery.predicateForObjects(
  293. withMetadataKey: HKMetadataKeySyncIdentifier,
  294. operatorType: .equalTo,
  295. value: syncID
  296. )
  297. self.healthKitStore.deleteObjects(of: sampleType, predicate: predicate) { _, _, error in
  298. guard let error = error else { return }
  299. warning(.service, "Cannot delete sample with syncID: \(syncID)", error: error)
  300. }
  301. }
  302. }
  303. }
  304. enum HealthKitPermissionRequestStatus {
  305. case needRequest
  306. case didRequest
  307. }
  308. enum HKError: Error {
  309. // HealthKit work only iPhone (not on iPad)
  310. case notAvailableOnCurrentDevice
  311. // Some data can be not available on current iOS-device
  312. case dataNotAvailable
  313. }