HealthKitManager.swift 14 KB

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