HealthKitManager.swift 12 KB

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