@@ -19,29 +19,22 @@ import OSLog
1919 /// This does mean users who have very little buffer, and are not regularly unlocking their phone, may erroneously derail. There is nothing we
2020 /// can do about this.
2121 static let daysToUpdateOnChangeNotification = 7
22-
2322 private let logger = Logger ( subsystem: " com.beeminder.beeminder " , category: " HealthStoreManager " )
24-
2523 private let goalManager : GoalManager
26-
2724 // TODO: Public for now to use from config
2825 public let healthStore = HKHealthStore ( )
29-
3026 /// The Connection objects responsible for updating goals based on their healthkit metrics
3127 /// Dictionary key is the goal id, as this is stable across goal renames
3228 private nonisolated ( unsafe) var monitors: [ String : HealthKitMetricMonitor ] = [ : ]
33-
3429 /// Protect concurrent modifications to the connections dictionary
3530 private nonisolated let monitorsSemaphore = DispatchSemaphore ( value: 1 )
36-
3731 init ( goalManager: GoalManager , container: NSPersistentContainer ) {
3832 self . goalManager = goalManager
3933 self . modelContainer = container
4034 let context = container. newBackgroundContext ( )
4135 context. name = " HealthStoreManager "
4236 self . modelExecutor = . init( context: context)
4337 }
44-
4538 /// Request acess to HealthKit data for the supplied metric
4639 ///
4740 /// This function will throw an exception on a major failure. However, it will return silently if the user chooses
@@ -51,15 +44,13 @@ import OSLog
5144 logger. notice ( " requestAuthorization for \( metric. databaseString, privacy: . public) " )
5245 try await self . healthStore. requestAuthorization ( toShare: Set ( ) , read: [ metric. sampleType ( ) ] )
5346 }
54-
5547 /// Start listening for background updates to the supplied goal if we are not already doing so
5648 public func ensureUpdatesRegularly( goalID: NSManagedObjectID ) async throws {
5749 let goal = try modelContext. existingObject ( with: goalID) as! Goal
5850 modelContext. refresh ( goal, mergeChanges: false )
5951 guard let metricName = goal. healthKitMetric else { return }
6052 try await self . ensureUpdatesRegularly ( metricNames: [ metricName] , removeMissing: false )
6153 }
62-
6354 /// Ensure we have background update listeners for all known goals such that they
6455 /// will be updated any time the health data changes.
6556 public func ensureGoalsUpdateRegularly( ) async throws {
@@ -68,21 +59,17 @@ import OSLog
6859 let metrics = goals. compactMap { $0. healthKitMetric } . filter { $0 != " " }
6960 return try await ensureUpdatesRegularly ( metricNames: metrics, removeMissing: true )
7061 }
71-
7262 /// Install observers for any goals we currently have permission to read
7363 ///
7464 /// This function will never show a permissions dialog - instead it will not update for
7565 /// metrics where we do not have permission.
7666 public nonisolated func silentlyInstallObservers( context: NSManagedObjectContext ) {
7767 logger. notice ( " Silently installing observer queries " )
78-
7968 guard let goals = goalManager. staleGoals ( context: context) else { return }
8069 let metrics = goals. compactMap { $0. healthKitMetric } . filter { $0 != " " }
8170 let monitors = updateKnownMonitors ( metricNames: metrics, removeMissing: true )
82-
8371 for monitor in monitors { monitor. registerObserverQuery ( ) }
8472 }
85-
8673 /// Immediately update the supplied goal based on HealthKit's data record
8774 ///
8875 /// Any existing beeminder records for the date range provided will be updated or deleted.
@@ -95,16 +82,13 @@ import OSLog
9582 try await updateWithRecentData ( goal: goal, days: days)
9683 try await goalManager. refreshGoal ( goalID)
9784 }
98-
9985 /// Immediately update all known goals based on HealthKit's data record
10086 public func updateAllGoalsWithRecentData( days: Int ) async throws {
10187 logger. notice ( " Updating all goals with recent day for last \( days, privacy: . public) days " )
102-
10388 // We must create this context in a backgrounfd thread as it will be used in background threads
10489 modelContext. refreshAllObjects ( )
10590 guard let goals = goalManager. staleGoals ( context: modelContext) else { return }
10691 let goalsWithHealthData = goals. filter { $0. healthKitMetric != nil && $0. healthKitMetric != " " }
107-
10892 try await withThrowingTaskGroup ( of: Void . self) { group in
10993 for goal in goalsWithHealthData {
11094 let goalID = goal. objectID
@@ -118,28 +102,20 @@ import OSLog
118102 }
119103 try await goalManager. refreshGoals ( )
120104 }
121-
122105 private func ensureUpdatesRegularly( metricNames: any Sequence < String > , removeMissing: Bool ) async throws {
123106 let monitors = updateKnownMonitors ( metricNames: metricNames, removeMissing: removeMissing)
124-
125107 var permissions = Set < HKObjectType > ( )
126108 for monitor in monitors { permissions. insert ( monitor. metric. permissionType ( ) ) }
127- if permissions. count > 0 {
128- try await self . healthStore. requestAuthorization ( toShare: Set ( ) , read: permissions)
129-
130- }
131-
109+ if permissions. count > 0 { try await self . healthStore. requestAuthorization ( toShare: Set ( ) , read: permissions) }
132110 try await withThrowingTaskGroup ( of: Void . self) { group in
133111 for monitor in monitors { group. addTask { try await monitor. setupHealthKit ( ) } }
134112 try await group. waitForAll ( )
135113 }
136114 }
137-
138115 private nonisolated func updateKnownMonitors( metricNames: any Sequence < String > , removeMissing: Bool )
139116 -> [ HealthKitMetricMonitor ]
140117 {
141118 monitorsSemaphore. wait ( )
142-
143119 for metricName in metricNames {
144120 if monitors [ metricName] == nil {
145121 guard let metric = HealthKitConfig . metrics. first ( where: { $0. databaseString == metricName } ) else {
@@ -155,7 +131,6 @@ import OSLog
155131 )
156132 }
157133 }
158-
159134 if removeMissing {
160135 for (metricName, monitor) in monitors {
161136 if !metricNames. contains ( metricName) {
@@ -164,13 +139,10 @@ import OSLog
164139 }
165140 }
166141 }
167-
168142 let requestedMonitors = metricNames. compactMap { monitors [ $0] }
169-
170143 monitorsSemaphore. signal ( )
171144 return requestedMonitors
172145 }
173-
174146 private func updateGoalsForMetricChange( metricName: String , metric: HealthKitMetric ) async {
175147 do {
176148 modelContext. refreshAllObjects ( )
@@ -180,13 +152,11 @@ import OSLog
180152 logger. notice ( " Received an update for metric \( metricName, privacy: . public) but no goals using it " )
181153 return
182154 }
183-
184155 for goal in goalsForMetric {
185156 try await self . updateWithRecentData ( goal: goal, days: HealthStoreManager . daysToUpdateOnChangeNotification)
186157 }
187158 } catch { logger. error ( " Error updating goals for metric change: \( error, privacy: . public) " ) }
188159 }
189-
190160 private func updateWithRecentData( goal: Goal , days: Int ) async throws {
191161 guard let metric = HealthKitConfig . metrics. first ( where: { $0. databaseString == goal. healthKitMetric } ) else {
192162 throw HealthKitError ( " No metric found for goal \( goal. slug) with metric \( goal. healthKitMetric ?? " nil " ) " )
@@ -206,5 +176,9 @@ import OSLog
206176 goalID: goal. objectID,
207177 healthKitDataPoints: nonZeroDataPoints
208178 )
179+
180+ let goal = try self . modelContext. existingObject ( with: goal. objectID) as! Goal
181+ goal. lastSyncedWithHealthKitLocal = Date ( )
182+ try self . modelContext. save ( )
209183 }
210184}
0 commit comments