HealthKitSampleStore.swift 19 KB

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