HealthKitManager.swift 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import Foundation
  2. import HealthKit
  3. import Swinject
  4. protocol HealthKitManager {
  5. /// Check availability HealthKit on current device and user's permissions
  6. var isAvailableOnCurrentDevice: Bool { get }
  7. /// Check all needed permissions
  8. /// Return false if one or more permissions are deny or not choosen
  9. var areAllowAllPermissions: Bool { get }
  10. /// Check availability HealthKit on current device and user's permission of object
  11. func isAvailableFor(object: HKObjectType) -> Bool
  12. /// Requests user to give permissions on using HealthKit
  13. func requestPermission(completion: ((Bool, Error?) -> Void)?)
  14. /// Save blood glucose data to HealthKit store
  15. func save(bloodGlucoses: [BloodGlucose], completion: ((Result<Bool, Error>) -> Void)?)
  16. /// Create observer for data passing beetwen Health Store and FreeAPS
  17. func createObserver()
  18. /// Enable background delivering objects from Apple Health to FreeAPS
  19. func enableBackgroundDelivery()
  20. }
  21. final class BaseHealthKitManager: HealthKitManager, Injectable {
  22. @Injected() private var fileStorage: FileStorage!
  23. @Injected() private var glucoseStorage: GlucoseStorage!
  24. @Injected() private var healthKitStore: HKHealthStore!
  25. private enum Config {
  26. // unwraped HKObjects
  27. static var permissions: Set<HKSampleType> {
  28. var result: Set<HKSampleType> = []
  29. for permission in optionalPermissions {
  30. result.insert(permission!)
  31. }
  32. return result
  33. }
  34. static let optionalPermissions = Set([Config.healthBGObject])
  35. // link to object in HealthKit
  36. static let healthBGObject = HKObjectType.quantityType(forIdentifier: .bloodGlucose)
  37. static let frequencyBackgroundDeliveryBloodGlucoseFromHealth = HKUpdateFrequency(rawValue: 10)!
  38. }
  39. var isAvailableOnCurrentDevice: Bool {
  40. HKHealthStore.isHealthDataAvailable()
  41. }
  42. var areAllowAllPermissions: Bool {
  43. var result = true
  44. Config.permissions.forEach { permission in
  45. if [HKAuthorizationStatus.sharingDenied, HKAuthorizationStatus.notDetermined]
  46. .contains(healthKitStore.authorizationStatus(for: permission))
  47. {
  48. result = false
  49. }
  50. }
  51. return result
  52. }
  53. init(resolver: Resolver) {
  54. injectServices(resolver)
  55. guard isAvailableOnCurrentDevice, let bjObject = Config.healthBGObject else {
  56. return
  57. }
  58. if isAvailableFor(object: bjObject) {
  59. debug(.service, "Create HealthKit Observer for Blood Glucose")
  60. createObserver()
  61. }
  62. enableBackgroundDelivery()
  63. }
  64. func isAvailableFor(object: HKObjectType) -> Bool {
  65. let status = healthKitStore.authorizationStatus(for: object)
  66. switch status {
  67. case .sharingAuthorized:
  68. return true
  69. default:
  70. return false
  71. }
  72. }
  73. func requestPermission(completion: ((Bool, Error?) -> Void)? = nil) {
  74. guard isAvailableOnCurrentDevice else {
  75. completion?(false, HKError.notAvailableOnCurrentDevice)
  76. return
  77. }
  78. for permission in Config.optionalPermissions {
  79. guard permission != nil else {
  80. completion?(false, HKError.dataNotAvailable)
  81. return
  82. }
  83. }
  84. healthKitStore.requestAuthorization(toShare: Config.permissions, read: Config.permissions) { status, error in
  85. completion?(status, error)
  86. }
  87. }
  88. func save(bloodGlucoses: [BloodGlucose], completion: ((Result<Bool, Error>) -> Void)? = nil) {
  89. for bgItem in bloodGlucoses {
  90. let bgQuantity = HKQuantity(
  91. unit: .milligramsPerDeciliter,
  92. doubleValue: Double(bgItem.glucose!)
  93. )
  94. let bgObjectSample = HKQuantitySample(
  95. type: Config.healthBGObject!,
  96. quantity: bgQuantity,
  97. start: bgItem.dateString,
  98. end: bgItem.dateString,
  99. metadata: [
  100. "HKMetadataKeyExternalUUID": bgItem.id,
  101. "HKMetadataKeySyncIdentifier": bgItem.id,
  102. "HKMetadataKeySyncVersion": 1,
  103. "fromFreeAPSX": true
  104. ]
  105. )
  106. healthKitStore.save(bgObjectSample) { status, error in
  107. guard error == nil else {
  108. completion?(Result.failure(error!))
  109. return
  110. }
  111. completion?(Result.success(status))
  112. }
  113. }
  114. }
  115. func createObserver() {
  116. guard let bgType = Config.healthBGObject else {
  117. warning(
  118. .service,
  119. "Can not create HealthKit Observer, because unable to get the Blood Glucose type",
  120. description: nil,
  121. error: nil
  122. )
  123. return
  124. }
  125. let query = HKObserverQuery(sampleType: bgType, predicate: nil) { [unowned self] _, _, observerError in
  126. if let _ = observerError {
  127. return
  128. }
  129. // loading only daily bg
  130. let predicate = HKQuery.predicateForSamples(
  131. withStart: Date().addingTimeInterval(-1.days.timeInterval),
  132. end: nil,
  133. options: .strictStartDate
  134. )
  135. healthKitStore.execute(getQueryForDeletedBloodGlucose(sampleType: bgType, predicate: predicate))
  136. healthKitStore.execute(getQueryForAddedBloodGlucose(sampleType: bgType, predicate: predicate))
  137. }
  138. healthKitStore.execute(query)
  139. }
  140. func enableBackgroundDelivery() {
  141. guard let bgType = Config.healthBGObject else {
  142. warning(
  143. .service,
  144. "Can not create HealthKit Background Delivery, because unable to get the Blood Glucose type",
  145. description: nil,
  146. error: nil
  147. )
  148. return
  149. }
  150. healthKitStore.enableBackgroundDelivery(
  151. for: bgType,
  152. frequency: Config.frequencyBackgroundDeliveryBloodGlucoseFromHealth
  153. ) { status, e in
  154. guard e == nil else {
  155. warning(.service, "Can not enable background delivery for Apple Health", description: nil, error: e)
  156. return
  157. }
  158. debug(.service, "HealthKit background delivery status is \(status)")
  159. }
  160. }
  161. private func getQueryForDeletedBloodGlucose(sampleType: HKQuantityType, predicate: NSPredicate) -> HKQuery {
  162. let query = HKAnchoredObjectQuery(
  163. type: sampleType,
  164. predicate: predicate,
  165. anchor: nil,
  166. limit: 1000
  167. ) { [unowned self] _, _, deletedObjects, _, _ in
  168. guard let samples = deletedObjects else {
  169. return
  170. }
  171. DispatchQueue.global(qos: .utility).async {
  172. var removingBGID = [String]()
  173. samples.forEach {
  174. if let idString = $0.metadata?["HKMetadataKeySyncIdentifier"] as? String {
  175. removingBGID.append(idString)
  176. } else {
  177. removingBGID.append($0.uuid.uuidString)
  178. }
  179. }
  180. glucoseStorage.removeGlucose(byIDCollection: removingBGID)
  181. }
  182. }
  183. return query
  184. }
  185. private func getQueryForAddedBloodGlucose(sampleType: HKQuantityType, predicate: NSPredicate) -> HKQuery {
  186. let query = HKSampleQuery(
  187. sampleType: sampleType,
  188. predicate: predicate,
  189. limit: Int(HKObjectQueryNoLimit),
  190. sortDescriptors: nil
  191. ) { [unowned self] _, results, _ in
  192. guard let samples = results as? [HKQuantitySample] else {
  193. return
  194. }
  195. let oldSamples: [HealthKitSample] = fileStorage
  196. .retrieve(OpenAPS.HealthKit.downloadedGlucose, as: [HealthKitSample].self) ?? []
  197. var newSamples = [HealthKitSample]()
  198. for sample in samples {
  199. if sample.wasUserEntered {
  200. newSamples.append(HealthKitSample(
  201. healthKitId: sample.uuid.uuidString,
  202. date: sample.startDate,
  203. glucose: Int(round(sample.quantity.doubleValue(for: .milligramsPerDeciliter)))
  204. ))
  205. }
  206. }
  207. newSamples = newSamples
  208. .filter { !oldSamples.contains($0) }
  209. newSamples.forEach({ sample in
  210. let glucose = BloodGlucose(
  211. _id: sample.healthKitId,
  212. sgv: sample.glucose,
  213. direction: nil,
  214. date: Decimal(Int(sample.date.timeIntervalSince1970) * 1000),
  215. dateString: sample.date,
  216. unfiltered: nil,
  217. filtered: nil,
  218. noise: nil,
  219. glucose: sample.glucose,
  220. type: "sgv"
  221. )
  222. glucoseStorage.storeGlucose([glucose])
  223. })
  224. let savingSamples = (newSamples + oldSamples)
  225. .removeDublicates()
  226. .filter { $0.date >= Date().addingTimeInterval(-1.days.timeInterval) }
  227. self.fileStorage.save(savingSamples, as: OpenAPS.HealthKit.downloadedGlucose)
  228. }
  229. return query
  230. }
  231. }
  232. enum HealthKitPermissionRequestStatus {
  233. case needRequest
  234. case didRequest
  235. }
  236. enum HKError: Error {
  237. // HealthKit work only iPhone (not on iPad)
  238. case notAvailableOnCurrentDevice
  239. // Some data can be not available on current iOS-device
  240. case dataNotAvailable
  241. }