HealthKitManager.swift 13 KB

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