HealthKitSampleStore.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. //
  2. // HealthKitSampleStore.swift
  3. // Naterade
  4. //
  5. // Created by Nathan Racklyeft on 1/24/16.
  6. // Copyright © 2016 Nathan Racklyeft. All rights reserved.
  7. //
  8. import Foundation
  9. import HealthKit
  10. import os.log
  11. extension Notification.Name {
  12. public static let StoreAuthorizationStatusDidChange = Notification.Name(rawValue: "com.loudnate.LoopKit.AuthorizationStatusDidChangeNotification")
  13. }
  14. public enum HealthKitSampleStoreResult<T> {
  15. case success(T)
  16. case failure(HealthKitSampleStore.StoreError)
  17. }
  18. public class HealthKitSampleStore {
  19. private let observerQueryUpdateHandlerQueue: DispatchQueue
  20. public enum StoreError: Error {
  21. case authorizationDenied
  22. case healthKitError(HKError)
  23. }
  24. /// The sample type managed by this store
  25. public let sampleType: HKSampleType
  26. /// The health store used for underlying queries
  27. public let healthStore: HKHealthStore
  28. /// Whether the store should fetch data that was written to HealthKit from current app
  29. private let observeHealthKitSamplesFromCurrentApp: Bool
  30. /// Whether the store should fetch data that was written to HealthKit from other apps
  31. private let observeHealthKitSamplesFromOtherApps: Bool
  32. /// Whether the store is observing changes to types
  33. public let observationEnabled: Bool
  34. /// For unit testing only.
  35. internal var testQueryStore: HKSampleQueryTestable?
  36. /// Allows for controlling uses of the system date in unit testing
  37. internal var test_currentDate: Date?
  38. internal func currentDate(timeIntervalSinceNow: TimeInterval = 0) -> Date {
  39. let date = test_currentDate ?? Date()
  40. return date.addingTimeInterval(timeIntervalSinceNow)
  41. }
  42. /// Allows unit test to inject a mock for HKObserverQuery
  43. internal var createObserverQuery: (HKSampleType, NSPredicate?, @escaping (HKObserverQuery, @escaping HKObserverQueryCompletionHandler, Error?) -> Void) -> HKObserverQuery = { (sampleType, predicate, updateHandler) in
  44. return HKObserverQuery(sampleType: sampleType, predicate: predicate, updateHandler: updateHandler)
  45. }
  46. /// Allows unit test to inject a mock for HKAnchoredObjectQuery
  47. internal var createAnchoredObjectQuery: (HKSampleType, NSPredicate?, HKQueryAnchor?, Int, @escaping (HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, Error?) -> Void) -> HKAnchoredObjectQuery = { (sampleType, predicate, anchor, limit, resultsHandler) in
  48. return HKAnchoredObjectQuery(type: sampleType, predicate: predicate, anchor: anchor, limit: limit, resultsHandler: resultsHandler)
  49. }
  50. private let log: OSLog
  51. public init(
  52. healthStore: HKHealthStore,
  53. observeHealthKitSamplesFromCurrentApp: Bool = true,
  54. observeHealthKitSamplesFromOtherApps: Bool = true,
  55. type: HKSampleType,
  56. observationStart: Date,
  57. observationEnabled: Bool,
  58. test_currentDate: Date? = nil
  59. ) {
  60. self.healthStore = healthStore
  61. self.observeHealthKitSamplesFromCurrentApp = observeHealthKitSamplesFromCurrentApp
  62. self.observeHealthKitSamplesFromOtherApps = observeHealthKitSamplesFromOtherApps
  63. self.sampleType = type
  64. self.observationStart = observationStart
  65. self.observationEnabled = observationEnabled
  66. self.test_currentDate = test_currentDate
  67. self.lockedQueryAnchor = Locked<HKQueryAnchor?>(nil)
  68. self.log = OSLog(category: String(describing: Swift.type(of: self)))
  69. self.observerQueryUpdateHandlerQueue = DispatchQueue(label: "com.loopkit.HealthKitSampleStore.observerQueryUpdateHandlerQueue.\(Swift.type(of: self))", qos: .utility)
  70. }
  71. deinit {
  72. if let query = observerQuery {
  73. healthStore.stop(query)
  74. }
  75. observerQuery = nil
  76. }
  77. // MARK: - Authorization
  78. /// Requests authorization from HealthKit to share and read the sample type.
  79. ///
  80. /// This operation is performed asynchronously and the completion will be executed on an arbitrary background queue.
  81. ///
  82. /// - Parameters:
  83. /// - toShare: Whether to request write authorization. Defaults to true.
  84. /// - read: Whether to request read authorization. Defaults to true.
  85. /// - completion: A closure called after the authorization is completed
  86. /// - result: The authorization result
  87. public func authorize(toShare: Bool = true, read: Bool = true, _ completion: @escaping (_ result: HealthKitSampleStoreResult<Bool>) -> Void) {
  88. healthStore.requestAuthorization(toShare: toShare ? [sampleType] : [], read: read ? [sampleType] : []) { (completed, error) -> Void in
  89. if completed && !self.sharingDenied {
  90. self.log.default("Authorize completed: creating HK query")
  91. self.createQuery()
  92. completion(.success(true))
  93. } else {
  94. let authError: StoreError
  95. if let error = error {
  96. authError = .healthKitError(HKError(_nsError: error as NSError))
  97. } else {
  98. authError = .authorizationDenied
  99. }
  100. completion(.failure(authError))
  101. }
  102. NotificationCenter.default.post(name: .StoreAuthorizationStatusDidChange, object: self)
  103. // Do not remove this log: it actually triggers a query by calling preferredUnit, and can update the cache
  104. // And trigger a unit change notification after authorization happens.
  105. self.log.default("Checking units after authorization: %{public}@", String(describing: self.preferredUnit))
  106. }
  107. }
  108. // MARK: - Query support
  109. /// The active observer query
  110. internal var observerQuery: HKObserverQuery? {
  111. didSet {
  112. if let query = oldValue {
  113. healthStore.stop(query)
  114. }
  115. if let query = observerQuery {
  116. log.debug("Executing observerQuery %@", query)
  117. healthStore.execute(query)
  118. }
  119. }
  120. }
  121. /// The earliest sample date for which additions and deletions are observed
  122. public internal(set) var observationStart: Date {
  123. didSet {
  124. // If we are now looking farther back, then reset the query
  125. if oldValue > observationStart {
  126. log.default("observationStart changed: creating HK query")
  127. createQuery()
  128. }
  129. }
  130. }
  131. /// The last-retreived anchor from an anchored object query
  132. internal var queryAnchor: HKQueryAnchor? {
  133. get {
  134. return lockedQueryAnchor.value
  135. }
  136. set {
  137. var changed: Bool = false
  138. lockedQueryAnchor.mutate { (anchor) in
  139. if anchor != newValue {
  140. anchor = newValue
  141. changed = true
  142. }
  143. }
  144. if changed {
  145. queryAnchorDidChange()
  146. }
  147. }
  148. }
  149. private let lockedQueryAnchor: Locked<HKQueryAnchor?>
  150. func queryAnchorDidChange() {
  151. // Subclasses can override
  152. }
  153. /// Called in response to an update by the observer query
  154. ///
  155. /// - Parameters:
  156. /// - query: The query which triggered the update
  157. /// - error: An error during the update, if one occurred
  158. internal final func observeUpdates(to query: HKObserverQuery, completionHandler: @escaping HKObserverQueryCompletionHandler, error: Error?) {
  159. if let error = error {
  160. self.log.error("Observer query %@ notified of error: %{public}@", query, String(describing: error))
  161. completionHandler()
  162. return
  163. }
  164. self.log.default("observeUpdates invoked - queueing handling")
  165. observerQueryUpdateHandlerQueue.async {
  166. let queryAnchor = self.queryAnchor
  167. self.log.default("%@ notified with changes. Fetching from: %{public}@", query, queryAnchor.map(String.init(describing:)) ?? "0")
  168. let semaphore = DispatchSemaphore(value: 0)
  169. let anchoredObjectQuery = self.createAnchoredObjectQuery(self.sampleType, query.predicate, queryAnchor, HKObjectQueryNoLimit) { [weak self] (query, newSamples, deletedSamples, anchor, error) in
  170. if let self = self {
  171. self.anchoredObjectQueryResultsHandler(query: query, newSamples: newSamples, deletedSamples: deletedSamples, anchor: anchor, error: error) {
  172. completionHandler()
  173. semaphore.signal()
  174. }
  175. } else {
  176. completionHandler()
  177. semaphore.signal()
  178. }
  179. }
  180. self.healthStore.execute(anchoredObjectQuery)
  181. semaphore.wait()
  182. }
  183. }
  184. private func anchoredObjectQueryResultsHandler(query: HKAnchoredObjectQuery, newSamples: [HKSample]?, deletedSamples: [HKDeletedObject]?, anchor: HKQueryAnchor?, error: Error?, completion: @escaping () -> Void) {
  185. if let error = error {
  186. self.log.error("Error from anchoredObjectQuery: anchor: %{public}@ error: %{public}@", String(describing: anchor), String(describing: error))
  187. completion()
  188. return
  189. }
  190. self.log.default("anchoredObjectQuery.resultsHandler: new: %{public}d deleted: %{public}d anchor: %{public}@", newSamples?.count ?? 0, deletedSamples?.count ?? 0, String(describing: anchor))
  191. guard let anchor = anchor else {
  192. self.log.error("anchoredObjectQueryResultsHandler called with no anchor")
  193. completion()
  194. return
  195. }
  196. self.processResults(from: query, added: newSamples ?? [], deleted: deletedSamples ?? [], anchor: anchor) { (success) in
  197. if success {
  198. // Do not advance anchor if we failed to update local cache
  199. self.queryAnchor = anchor
  200. }
  201. completion()
  202. }
  203. }
  204. /// Called in response to new results from an anchored object query
  205. ///
  206. /// - Parameters:
  207. /// - query: The executed query
  208. /// - added: An array of samples added
  209. /// - deleted: An array of samples deleted
  210. /// - error: An error from the query, if one occurred
  211. internal func processResults(from query: HKAnchoredObjectQuery, added: [HKSample], deleted: [HKDeletedObject], anchor: HKQueryAnchor, completion: @escaping (Bool) -> Void) {
  212. // To be overridden
  213. completion(true)
  214. }
  215. /// The preferred unit for the sample type
  216. ///
  217. /// The unit may be nil if the health store times out while fetching or the sample type is unsupported
  218. public var preferredUnit: HKUnit? {
  219. let identifier = HKQuantityTypeIdentifier(rawValue: sampleType.identifier)
  220. return HealthStoreUnitCache.unitCache(for: healthStore).preferredUnit(for: identifier)
  221. }
  222. }
  223. // MARK: - Unit Test Support
  224. extension HealthKitSampleStore: HKSampleQueryTestable {
  225. func executeSampleQuery(
  226. for type: HKSampleType,
  227. matching predicate: NSPredicate,
  228. limit: Int = HKObjectQueryNoLimit,
  229. sortDescriptors: [NSSortDescriptor]? = nil,
  230. resultsHandler: @escaping (HKSampleQuery, [HKSample]?, Error?) -> Void
  231. ) {
  232. if let tester = testQueryStore {
  233. tester.executeSampleQuery(for: type, matching: predicate, limit: limit, sortDescriptors: sortDescriptors, resultsHandler: resultsHandler)
  234. } else {
  235. let query = HKSampleQuery(sampleType: type, predicate: predicate, limit: limit, sortDescriptors: sortDescriptors, resultsHandler: resultsHandler)
  236. healthStore.execute(query)
  237. }
  238. }
  239. }
  240. // MARK: - Query
  241. extension HealthKitSampleStore {
  242. internal func predicateForSamples(withStart startDate: Date?, end endDate: Date?, options: HKQueryOptions = []) -> NSPredicate? {
  243. guard observeHealthKitSamplesFromCurrentApp || observeHealthKitSamplesFromOtherApps else {
  244. return nil
  245. }
  246. // Initial predicate is just the date range
  247. var predicate = HKQuery.predicateForSamples(withStart: startDate, end: endDate, options: options)
  248. // If we don't want samples from the current app, then only query samples NOT from the default HKSource
  249. if !observeHealthKitSamplesFromCurrentApp {
  250. predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, NSCompoundPredicate(notPredicateWithSubpredicate: HKQuery.predicateForObjects(from: HKSource.default()))])
  251. // Othewrise, if we don't want samples from other apps, then only query samples from the default HKSource
  252. } else if !observeHealthKitSamplesFromOtherApps {
  253. predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, HKQuery.predicateForObjects(from: HKSource.default())])
  254. }
  255. return predicate
  256. }
  257. }
  258. // MARK: - Observation
  259. extension HealthKitSampleStore {
  260. internal func createQuery() {
  261. log.debug("%@ [sampleType: %@]", #function, sampleType)
  262. log.debug("%@ [observationEnabled: %d]", #function, observationEnabled)
  263. log.debug("%@ [observeHealthKitSamplesFromCurrentApp: %d]", #function, observeHealthKitSamplesFromCurrentApp)
  264. log.debug("%@ [observeHealthKitSamplesFromOtherApps: %d]", #function, observeHealthKitSamplesFromOtherApps)
  265. log.debug("%@ [observationStart: %@]", #function, String(describing: observationStart))
  266. guard observationEnabled else {
  267. return
  268. }
  269. guard let predicate = predicateForSamples(withStart: observationStart, end: nil) else {
  270. return
  271. }
  272. observerQuery = createObserverQuery(sampleType, predicate) { [weak self] (query, completionHandler, error) in
  273. self?.observeUpdates(to: query, completionHandler: completionHandler, error: error)
  274. }
  275. enableBackgroundDelivery { (result) in
  276. switch result {
  277. case .failure(let error):
  278. self.log.error("Error enabling background delivery: %@", error.localizedDescription)
  279. case .success:
  280. self.log.default("Enabled background delivery for %{public}@", self.sampleType)
  281. }
  282. }
  283. }
  284. /// Enables the immediate background delivery of updates to samples from HealthKit.
  285. ///
  286. /// This operation is performed asynchronously and the completion will be executed on an arbitrary background queue.
  287. ///
  288. /// - Parameter completion: A closure called after the request is completed
  289. /// - Parameter result: A boolean indicating the new background delivery state
  290. private func enableBackgroundDelivery(_ completion: @escaping (_ result: HealthKitSampleStoreResult<Bool>) -> Void) {
  291. #if os(iOS)
  292. healthStore.enableBackgroundDelivery(for: sampleType, frequency: .immediate) { (enabled, error) in
  293. if let error = error {
  294. completion(.failure(.healthKitError(HKError(_nsError: error as NSError))))
  295. } else if enabled {
  296. completion(.success(true))
  297. } else {
  298. assertionFailure()
  299. }
  300. }
  301. #endif
  302. }
  303. /// Disables the immediate background delivery of updates to samples from HealthKit.
  304. ///
  305. /// This operation is performed asynchronously and the completion will be executed on an arbitrary background queue.
  306. ///
  307. /// - Parameter completion: A closure called after the request is completed
  308. /// - Parameter result: A boolean indicating the new background delivery state
  309. private func disableBackgroundDelivery(_ completion: @escaping (_ result: HealthKitSampleStoreResult<Bool>) -> Void) {
  310. #if os(iOS)
  311. healthStore.disableBackgroundDelivery(for: sampleType) { (disabled, error) in
  312. if let error = error {
  313. completion(.failure(.healthKitError(HKError(_nsError: error as NSError))))
  314. } else if disabled {
  315. completion(.success(false))
  316. } else {
  317. assertionFailure()
  318. }
  319. }
  320. #endif
  321. }
  322. }
  323. // MARK: - HKHealthStore helpers
  324. extension HealthKitSampleStore {
  325. /// True if the user has explicitly authorized access to any required share types
  326. public var sharingAuthorized: Bool {
  327. return healthStore.authorizationStatus(for: sampleType) == .sharingAuthorized
  328. }
  329. /// True if the user has explicitly denied access to any required share types
  330. public var sharingDenied: Bool {
  331. return healthStore.authorizationStatus(for: sampleType) == .sharingDenied
  332. }
  333. /// True if the store requires authorization
  334. public var authorizationRequired: Bool {
  335. return healthStore.authorizationStatus(for: sampleType) == .notDetermined
  336. }
  337. /**
  338. Queries the preferred unit for the authorized share types. If more than one unit is retrieved,
  339. then the completion contains just one of them.
  340. - parameter completion: A closure called after the query is completed. This closure takes two arguments:
  341. - unit: The retrieved unit
  342. - error: An error object explaining why the retrieval was unsuccessful
  343. */
  344. @available(*, deprecated, message: "Use HealthKitSampleStore.getter:preferredUnit instead")
  345. public func preferredUnit(_ completion: @escaping (_ unit: HKUnit?, _ error: Error?) -> Void) {
  346. preferredUnit { result in
  347. switch result {
  348. case .success(let unit):
  349. completion(unit, nil)
  350. case .failure(let error):
  351. completion(nil, error)
  352. }
  353. }
  354. }
  355. /// Queries the preferred unit for the sample type.
  356. ///
  357. /// This operation is performed asynchronously and the completion will be executed on an arbitrary background queue.
  358. ///
  359. /// - Parameter completion: A closure called after the query is completed
  360. /// - Parameter result: The query result
  361. @available(*, deprecated, message: "Use HealthKitSampleStore.getter:preferredUnit instead")
  362. private func preferredUnit(_ completion: @escaping (_ result: HealthKitSampleStoreResult<HKUnit>) -> Void) {
  363. let quantityTypes = [self.sampleType].compactMap { (sampleType) -> HKQuantityType? in
  364. return sampleType as? HKQuantityType
  365. }
  366. self.healthStore.preferredUnits(for: Set(quantityTypes)) { (quantityToUnit, error) -> Void in
  367. if let error = error {
  368. completion(.failure(.healthKitError(HKError(_nsError: error as NSError))))
  369. } else if let unit = quantityToUnit.values.first {
  370. completion(.success(unit))
  371. } else {
  372. assertionFailure()
  373. }
  374. }
  375. }
  376. }
  377. extension HealthKitSampleStore: CustomDebugStringConvertible {
  378. public var debugDescription: String {
  379. return """
  380. * observerQuery: \(String(describing: observerQuery))
  381. * observationStart: \(observationStart)
  382. * observationEnabled: \(observationEnabled)
  383. * authorizationRequired: \(authorizationRequired)
  384. """
  385. }
  386. }
  387. extension HealthKitSampleStore.StoreError: LocalizedError {
  388. public var errorDescription: String? {
  389. switch self {
  390. case .authorizationDenied:
  391. return LocalizedString("Authorization Denied", comment: "The error description describing when Health sharing was denied")
  392. case .healthKitError(let error):
  393. return error.localizedDescription
  394. }
  395. }
  396. public var recoverySuggestion: String? {
  397. switch self {
  398. case .authorizationDenied:
  399. return LocalizedString("Please re-enable sharing in Health", comment: "The error recovery suggestion when Health sharing was denied")
  400. case .healthKitError(let error):
  401. return error.errorUserInfo[NSLocalizedRecoverySuggestionErrorKey] as? String
  402. }
  403. }
  404. }