HealthKitManager.swift 15 KB

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