HealthKitManager.swift 14 KB

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