HealthKitManager.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  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. @Injected() private var glucoseStorage: GlucoseStorage!
  25. @Injected() private var healthKitStore: HKHealthStore!
  26. @Injected() private var settingsManager: SettingsManager!
  27. private let processQueue = DispatchQueue(label: "BaseHealthKitManager.processQueue")
  28. private enum Config {
  29. // unwraped HKObjects
  30. static var permissions: Set<HKSampleType> { Set([healthBGObject].compactMap { $0 }) }
  31. // link to object in HealthKit
  32. static let healthBGObject = HKObjectType.quantityType(forIdentifier: .bloodGlucose)
  33. // Meta-data key of FreeASPX data in HealthStore
  34. static let freeAPSMetaKey = "fromFreeAPSX"
  35. }
  36. // BG that will be return Publisher
  37. @SyncAccess @Persisted(key: "HealthKitManagerNewGlucose") private var newGlucose: [BloodGlucose] = []
  38. // last anchor for HKAnchoredQuery
  39. private var lastBloodGlucoseQueryAnchor: HKQueryAnchor! {
  40. set {
  41. persistedAnchor = (
  42. try? NSKeyedArchiver.archivedData(withRootObject: newValue as Any, requiringSecureCoding: false)
  43. ) ?? Data()
  44. }
  45. get {
  46. (try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(persistedAnchor) as? HKQueryAnchor) ??
  47. HKQueryAnchor(fromValue: 0)
  48. }
  49. }
  50. @SyncAccess @Persisted(key: "HealthKitManagerAnchor") private var persistedAnchor = Data()
  51. var isAvailableOnCurrentDevice: Bool {
  52. HKHealthStore.isHealthDataAvailable()
  53. }
  54. var areAllowAllPermissions: Bool {
  55. Set(Config.permissions.map { healthKitStore.authorizationStatus(for: $0) })
  56. .intersection([.sharingDenied, .notDetermined])
  57. .isEmpty
  58. }
  59. // NSPredicate, which use during load increment BG from Health store
  60. private var loadBGPredicate: NSPredicate {
  61. // loading only daily bg
  62. let predicateByStartDate = HKQuery.predicateForSamples(
  63. withStart: Date().addingTimeInterval(-1.days.timeInterval),
  64. end: nil,
  65. options: .strictStartDate
  66. )
  67. // loading only not FreeAPS bg
  68. // this predicate dont influence on Deleted Objects, only on added
  69. let predicateByMeta = HKQuery.predicateForObjects(
  70. withMetadataKey: Config.freeAPSMetaKey,
  71. operatorType: .notEqualTo,
  72. value: 1
  73. )
  74. return NSCompoundPredicate(andPredicateWithSubpredicates: [predicateByStartDate, predicateByMeta])
  75. }
  76. init(resolver: Resolver) {
  77. injectServices(resolver)
  78. guard isAvailableOnCurrentDevice,
  79. Config.healthBGObject != nil else { return }
  80. createObserver()
  81. enableBackgroundDelivery()
  82. debug(.service, "HealthKitManager did create")
  83. }
  84. func checkAvailabilitySave(objectTypeToHealthStore: HKObjectType) -> Bool {
  85. healthKitStore.authorizationStatus(for: objectTypeToHealthStore) == .sharingAuthorized
  86. }
  87. func checkAvailabilitySaveBG() -> Bool {
  88. Config.healthBGObject.map { checkAvailabilitySave(objectTypeToHealthStore: $0) } ?? false
  89. }
  90. func requestPermission(completion: ((Bool, Error?) -> Void)? = nil) {
  91. guard isAvailableOnCurrentDevice else {
  92. completion?(false, HKError.notAvailableOnCurrentDevice)
  93. return
  94. }
  95. guard Config.permissions.isNotEmpty else {
  96. completion?(false, HKError.dataNotAvailable)
  97. return
  98. }
  99. healthKitStore.requestAuthorization(toShare: Config.permissions, read: Config.permissions) { status, error in
  100. completion?(status, error)
  101. }
  102. }
  103. func saveIfNeeded(bloodGlucose: [BloodGlucose]) {
  104. guard settingsManager.settings.useAppleHealth,
  105. let sampleType = Config.healthBGObject,
  106. checkAvailabilitySave(objectTypeToHealthStore: sampleType),
  107. bloodGlucose.isNotEmpty
  108. else { return }
  109. processQueue.async {
  110. for bgItem in bloodGlucose {
  111. let bgQuantity = HKQuantity(
  112. unit: .milligramsPerDeciliter,
  113. doubleValue: Double(bgItem.glucose!)
  114. )
  115. let bgObjectSample = HKQuantitySample(
  116. type: sampleType,
  117. quantity: bgQuantity,
  118. start: bgItem.dateString,
  119. end: bgItem.dateString,
  120. metadata: [
  121. HKMetadataKeyExternalUUID: bgItem.id,
  122. HKMetadataKeySyncIdentifier: bgItem.id,
  123. HKMetadataKeySyncVersion: 1,
  124. Config.freeAPSMetaKey: true
  125. ]
  126. )
  127. self.load(sampleFromHealth: sampleType, withID: bgItem.id) { [weak self] samples in
  128. if samples.isEmpty {
  129. self?.healthKitStore.save(bgObjectSample) { _, _ in }
  130. }
  131. }
  132. }
  133. }
  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 load(
  177. sampleFromHealth sampleType: HKQuantityType,
  178. withID id: String,
  179. andDo completion: (([HKSample]) -> Void)?
  180. ) {
  181. dispatchPrecondition(condition: .onQueue(processQueue))
  182. let predicate = HKQuery.predicateForObjects(
  183. withMetadataKey: HKMetadataKeySyncIdentifier,
  184. operatorType: .equalTo,
  185. value: id
  186. )
  187. let query = HKSampleQuery(
  188. sampleType: sampleType,
  189. predicate: predicate,
  190. limit: 1,
  191. sortDescriptors: nil
  192. ) { _, results, _ in
  193. guard let samples = results as? [HKQuantitySample] else {
  194. completion?([])
  195. return
  196. }
  197. completion?(samples)
  198. }
  199. healthKitStore.execute(query)
  200. }
  201. private func getBloodGlucoseHKQuery(predicate: NSPredicate) -> HKQuery? {
  202. guard let sampleType = Config.healthBGObject else { return nil }
  203. let query = HKAnchoredObjectQuery(
  204. type: sampleType,
  205. predicate: predicate,
  206. anchor: lastBloodGlucoseQueryAnchor,
  207. limit: HKObjectQueryNoLimit
  208. ) { [weak self] _, addedObjects, deletedObjects, anchor, _ in
  209. guard let self = self else { return }
  210. self.processQueue.async {
  211. debug(.service, "AnchoredQuery did execute")
  212. self.lastBloodGlucoseQueryAnchor = anchor
  213. // Added objects
  214. if let bgSamples = addedObjects as? [HKQuantitySample],
  215. bgSamples.isNotEmpty
  216. {
  217. self.prepareSamplesToPublisherFetch(bgSamples)
  218. }
  219. // Deleted objects
  220. if let deletedSamples = deletedObjects,
  221. deletedSamples.isNotEmpty
  222. {
  223. self.deleteSamplesFromLocalStorage(deletedSamples)
  224. }
  225. }
  226. }
  227. return query
  228. }
  229. private func prepareSamplesToPublisherFetch(_ samples: [HKQuantitySample]) {
  230. dispatchPrecondition(condition: .onQueue(processQueue))
  231. debug(.service, "Start preparing samples: \(String(describing: samples))")
  232. newGlucose += samples
  233. .compactMap { sample -> HealthKitSample? in
  234. let fromFAX = sample.metadata?[Config.freeAPSMetaKey] as? Bool ?? false
  235. guard !fromFAX else { return nil }
  236. return HealthKitSample(
  237. healthKitId: sample.uuid.uuidString,
  238. date: sample.startDate,
  239. glucose: Int(round(sample.quantity.doubleValue(for: .milligramsPerDeciliter)))
  240. )
  241. }
  242. .map { sample in
  243. BloodGlucose(
  244. _id: sample.healthKitId,
  245. sgv: sample.glucose,
  246. direction: nil,
  247. date: Decimal(Int(sample.date.timeIntervalSince1970) * 1000),
  248. dateString: sample.date,
  249. unfiltered: nil,
  250. filtered: nil,
  251. noise: nil,
  252. glucose: sample.glucose,
  253. type: "sgv"
  254. )
  255. }
  256. .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
  257. newGlucose = newGlucose.removeDublicates()
  258. debug(
  259. .service,
  260. "Current BloodGlucose.Type objects will be send from Publisher during fetch: \(String(describing: newGlucose))"
  261. )
  262. }
  263. private func deleteSamplesFromLocalStorage(_ deletedSamples: [HKDeletedObject]) {
  264. dispatchPrecondition(condition: .onQueue(processQueue))
  265. debug(.service, "Delete HealthKit objects: \(String(describing: deletedSamples))")
  266. let removingBGID = deletedSamples.map {
  267. $0.metadata?[HKMetadataKeySyncIdentifier] as? String ?? $0.uuid.uuidString
  268. }
  269. glucoseStorage.removeGlucose(ids: removingBGID)
  270. newGlucose = newGlucose.filter { !removingBGID.contains($0.id) }
  271. }
  272. func fetch() -> AnyPublisher<[BloodGlucose], Never> {
  273. Future { [weak self] promise in
  274. guard let self = self else {
  275. promise(.success([]))
  276. return
  277. }
  278. self.processQueue.async {
  279. debug(.service, "Start fetching HealthKitManager")
  280. guard self.settingsManager.settings.useAppleHealth else {
  281. debug(.service, "HealthKitManager cant return any data, because useAppleHealth option is disable")
  282. promise(.success([]))
  283. return
  284. }
  285. // Remove old BGs
  286. self.newGlucose = self.newGlucose
  287. .filter { $0.dateString >= Date().addingTimeInterval(-1.days.timeInterval) }
  288. // Get actual BGs (beetwen Date() - 1 day and Date())
  289. let actualGlucose = self.newGlucose
  290. .filter { $0.dateString <= Date() }
  291. // Update newGlucose
  292. self.newGlucose = self.newGlucose
  293. .filter { !actualGlucose.contains($0) }
  294. debug(.service, "Actual glucose is \(actualGlucose)")
  295. debug(.service, "Current state of newGlucose is \(self.newGlucose)")
  296. promise(.success(actualGlucose))
  297. }
  298. }
  299. .eraseToAnyPublisher()
  300. }
  301. }
  302. enum HealthKitPermissionRequestStatus {
  303. case needRequest
  304. case didRequest
  305. }
  306. enum HKError: Error {
  307. // HealthKit work only iPhone (not on iPad)
  308. case notAvailableOnCurrentDevice
  309. // Some data can be not available on current iOS-device
  310. case dataNotAvailable
  311. }