AnchoredHealthDataReader.swift
5.74 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import Foundation
import HealthKit
struct AnchoredHealthSleepReadResult {
let data: [NativeSleepInterval]
let anchor: Data
}
/// Reads HealthKit sleep changes with HKQueryAnchor and converts them to the
/// upload payload model used by the sleep uploader.
final class AnchoredHealthDataReader {
private let service: HealthKitService
private let anchorStore: AnchoredHealthUploadAnchorStore
private let userIdProvider: () -> Int?
init(
service: HealthKitService = .shared,
anchorStore: AnchoredHealthUploadAnchorStore = AnchoredHealthUploadAnchorStore(),
userIdProvider: @escaping () -> Int? = { AppShared.shared.userId }
) {
self.service = service
self.anchorStore = anchorStore
self.userIdProvider = userIdProvider
}
func readAllSleep() async throws -> AnchoredHealthSleepReadResult {
guard let userId = userIdProvider(), userId > 0 else {
throw NativeHealthUploadError.missingUserId
}
let sleepAnchorKey = AnchoredHealthUploadAnchorStore.sleepAnchorKey
let storedAnchorData = anchorStore.data(userId: userId, anchorKey: sleepAnchorKey)
Self.log(
"reader.sleep.anchor.before userId=\(userId) anchorKey=\(sleepAnchorKey) \(Self.describeAnchorData(storedAnchorData)) initialStart=\(Self.debugTimestamp(firstUploadStartDate().timeIntervalSince1970))"
)
let changes = try await service.fetchAnchoredChanges(
anchor: anchorStore.anchor(userId: userId, anchorKey: sleepAnchorKey),
initialStartDate: firstUploadStartDate()
)
let archivedAnchor = try SharedHealthAnchoredUploadSupport.archive(changes.newAnchor)
Self.log(
"reader.sleep.anchor.query userId=\(userId) anchorKey=\(sleepAnchorKey) source=\(changes.sourceIdentifier) deleted=\(changes.deletedObjectCount) samples=\(changes.samples.count) sampleRange=\(Self.describeSamples(changes.samples)) newAnchor=\(Self.describeAnchorData(archivedAnchor))"
)
guard !changes.samples.isEmpty else {
Self.log("reader.sleep.all userId=\(userId) count=0 range=empty")
return AnchoredHealthSleepReadResult(data: [], anchor: archivedAnchor)
}
let startDate = queryStartDate(for: changes.samples)
let endDate = Date()
Self.log(
"reader.sleep.fetchRange userId=\(userId) start=\(Self.debugTimestamp(startDate.timeIntervalSince1970)) end=\(Self.debugTimestamp(endDate.timeIntervalSince1970))"
)
let intervals = try await service.fetchSleepData(startDate: startDate, endDate: endDate)
let sortedIntervals = Self.deduplicateSleep(intervals).sorted {
if $0.toTime == $1.toTime {
return $0.fromTime < $1.fromTime
}
return $0.toTime < $1.toTime
}
Self.log(
"reader.sleep.all userId=\(userId) count=\(sortedIntervals.count) range=\(Self.describeSleepRange(sortedIntervals))"
)
Self.logSleepIntervals(sortedIntervals, userId: userId, prefix: "reader.sleep.data.item")
return AnchoredHealthSleepReadResult(data: sortedIntervals, anchor: archivedAnchor)
}
}
private extension AnchoredHealthDataReader {
func firstUploadStartDate() -> Date {
let years = NativeHealthUploadConfiguration.firstUploadLookbackYears
let date = Calendar.current.date(byAdding: .year, value: -years, to: Date())
?? Date(timeIntervalSinceNow: -TimeInterval(years * 365 * 24 * 60 * 60))
return Calendar.current.startOfDay(for: date)
}
func queryStartDate(for samples: [HKSample]) -> Date {
let earliest = samples.map(\.startDate).min() ?? firstUploadStartDate()
return max(Calendar.current.startOfDay(for: earliest), firstUploadStartDate())
}
static func deduplicateSleep(_ data: [NativeSleepInterval]) -> [NativeSleepInterval] {
var seen = Set<String>()
return data.filter { interval in
let key = "\(interval.dataType)-\(interval.fromTime)-\(interval.toTime)"
return seen.insert(key).inserted
}
}
static func log(_ message: String) {
DebugLogger.debugLog("[ArchUploader] \(message)")
}
static func logSleepIntervals(
_ intervals: [NativeSleepInterval],
userId: Int,
prefix: String
) {
intervals.forEach { interval in
log(
"\(prefix) userId=\(userId) dataType=\(interval.dataType) from=\(debugTimestamp(interval.fromTime)) fromUnix=\(Int64(interval.fromTime)) to=\(debugTimestamp(interval.toTime)) toUnix=\(Int64(interval.toTime))"
)
}
}
static func describeSamples(_ samples: [HKSample]) -> String {
guard !samples.isEmpty else { return "empty" }
let minStart = samples.map(\.startDate).min() ?? .distantPast
let maxEnd = samples.map(\.endDate).max() ?? .distantPast
return "\(debugTimestamp(minStart.timeIntervalSince1970))...\(debugTimestamp(maxEnd.timeIntervalSince1970))"
}
static func describeSleepRange(_ intervals: [NativeSleepInterval]) -> String {
guard let minTime = intervals.map(\.fromTime).min(),
let maxTime = intervals.map(\.toTime).max() else {
return "empty"
}
return "\(debugTimestamp(minTime))...\(debugTimestamp(maxTime))"
}
static func describeAnchorData(_ data: Data?) -> String {
guard let data else { return "anchor=none" }
return "anchor=size:\(data.count),hash:\(data.stableDebugHash)"
}
static func debugTimestamp(_ timeInterval: TimeInterval) -> String {
debugDateFormatter.string(from: Date(timeIntervalSince1970: timeInterval))
}
static let debugDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return formatter
}()
}
private extension Data {
var stableDebugHash: String {
let hash = reduce(UInt64(14_695_981_039_346_656_037)) { result, byte in
(result ^ UInt64(byte)).multipliedReportingOverflow(by: 1_099_511_628_211).partialValue
}
return String(hash, radix: 16)
}
}