Commit bbe2b7ca48642113cc8044ac799ad35d9fbcd869

Authored by 权海
1 parent 73fe2db2

feat(ui):切换到v2上传apple health数据

@@ -41,13 +41,7 @@ final class HealthDataReader { @@ -41,13 +41,7 @@ final class HealthDataReader {
41 startDate: startDate, 41 startDate: startDate,
42 endDate: endDate 42 endDate: endDate
43 ) 43 )
44 - async let stand = fetchDailyCumulativeSamples(  
45 - identifier: .appleStandTime,  
46 - dataType: .stand,  
47 - unit: .second(),  
48 - startDate: startDate,  
49 - endDate: endDate  
50 - ) 44 + async let stand = fetchStandData(startDate: startDate, endDate: endDate)
51 async let steps = fetchDailyCumulativeSamples( 45 async let steps = fetchDailyCumulativeSamples(
52 identifier: .stepCount, 46 identifier: .stepCount,
53 dataType: .steps, 47 dataType: .steps,
@@ -225,14 +219,7 @@ final class HealthDataReader { @@ -225,14 +219,7 @@ final class HealthDataReader {
225 } 219 }
226 220
227 func fetchStandData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] { 221 func fetchStandData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
228 - try await fetchDailyCumulativeSamples(identifier: .appleStandTime, dataType: .stand, unit: .second(), startDate: startDate, endDate: endDate)  
229 -// try await fetchQuantitySamples(  
230 -// identifier: .appleStandTime,  
231 -// dataType: .stand,  
232 -// unit: .minute(),  
233 -// startDate: startDate,  
234 -// endDate: endDate  
235 -// ) 222 + try await fetchDailyStandHours(startDate: startDate, endDate: endDate)
236 } 223 }
237 224
238 func fetchStepCountData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] { 225 func fetchStepCountData(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
@@ -296,7 +283,16 @@ final class HealthDataReader { @@ -296,7 +283,16 @@ final class HealthDataReader {
296 continuation.resume( 283 continuation.resume(
297 returning: NativeActivityTarget( 284 returning: NativeActivityTarget(
298 move: Int(summary.activeEnergyBurnedGoal.doubleValue(for: .kilocalorie())), 285 move: Int(summary.activeEnergyBurnedGoal.doubleValue(for: .kilocalorie())),
299 - stand: Int(summary.appleStandHoursGoal.doubleValue(for: .count())) 286 + stand: Int(summary.appleStandHoursGoal.doubleValue(for: .count())),
  287 + activityMoveMode: Self.activityMoveModeValue(from: summary),
  288 + activeEnergyBurned: summary.activeEnergyBurned.doubleValue(for: .kilocalorie()),
  289 + activeEnergyBurnedGoal: summary.activeEnergyBurnedGoal.doubleValue(for: .kilocalorie()),
  290 + appleMoveTime: Self.appleMoveTimeValue(from: summary),
  291 + appleMoveTimeGoal: Self.appleMoveTimeGoalValue(from: summary),
  292 + appleExerciseTime: summary.appleExerciseTime.doubleValue(for: .minute()),
  293 + exerciseTimeGoal: Self.exerciseTimeGoalValue(from: summary),
  294 + appleStandHours: summary.appleStandHours.doubleValue(for: .count()),
  295 + standHoursGoal: Self.standHoursGoalValue(from: summary)
300 ) 296 )
301 ) 297 )
302 } 298 }
@@ -304,6 +300,41 @@ final class HealthDataReader { @@ -304,6 +300,41 @@ final class HealthDataReader {
304 } 300 }
305 } 301 }
306 302
  303 + private static func activityMoveModeValue(from summary: HKActivitySummary) -> Int? {
  304 + if #available(iOS 14.0, *) {
  305 + return summary.activityMoveMode.rawValue
  306 + }
  307 + return nil
  308 + }
  309 +
  310 + private static func appleMoveTimeValue(from summary: HKActivitySummary) -> Double? {
  311 + if #available(iOS 14.0, *) {
  312 + return summary.appleMoveTime.doubleValue(for: .minute())
  313 + }
  314 + return nil
  315 + }
  316 +
  317 + private static func appleMoveTimeGoalValue(from summary: HKActivitySummary) -> Double? {
  318 + if #available(iOS 14.0, *) {
  319 + return summary.appleMoveTimeGoal.doubleValue(for: .minute())
  320 + }
  321 + return nil
  322 + }
  323 +
  324 + private static func exerciseTimeGoalValue(from summary: HKActivitySummary) -> Double? {
  325 + if #available(iOS 16.0, *) {
  326 + return summary.exerciseTimeGoal?.doubleValue(for: .minute())
  327 + }
  328 + return nil
  329 + }
  330 +
  331 + private static func standHoursGoalValue(from summary: HKActivitySummary) -> Double? {
  332 + if #available(iOS 16.0, *) {
  333 + return summary.standHoursGoal?.doubleValue(for: .count())
  334 + }
  335 + return nil
  336 + }
  337 +
307 private func fetchHeartRateFamily(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] { 338 private func fetchHeartRateFamily(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
308 async let heartRate = fetchQuantitySamples( 339 async let heartRate = fetchQuantitySamples(
309 identifier: .heartRate, 340 identifier: .heartRate,
@@ -416,6 +447,41 @@ final class HealthDataReader { @@ -416,6 +447,41 @@ final class HealthDataReader {
416 } 447 }
417 } 448 }
418 449
  450 + private func fetchDailyStandHours(startDate: Date, endDate: Date) async throws -> [NativeHealthDataPoint] {
  451 + let calendar = Calendar.current
  452 + var start = calendar.dateComponents([.era, .year, .month, .day], from: startDate)
  453 + var end = calendar.dateComponents([.era, .year, .month, .day], from: endDate)
  454 + start.calendar = calendar
  455 + end.calendar = calendar
  456 +
  457 + let predicate = HKQuery.predicate(forActivitySummariesBetweenStart: start, end: end)
  458 +
  459 + return try await withCheckedThrowingContinuation { continuation in
  460 + let query = HKActivitySummaryQuery(predicate: predicate) { _, summaries, error in
  461 + if let error {
  462 + continuation.resume(throwing: error)
  463 + return
  464 + }
  465 +
  466 + let points = (summaries ?? [])
  467 + .compactMap { summary -> NativeHealthDataPoint? in
  468 + guard let date = calendar.date(from: summary.dateComponents(for: calendar)) else {
  469 + return nil
  470 + }
  471 + return NativeHealthDataPoint(
  472 + dataType: .stand,
  473 + time: date.timeIntervalSince1970,
  474 + value: summary.appleStandHours.doubleValue(for: .count())
  475 + )
  476 + }
  477 + .sorted { $0.time < $1.time }
  478 +
  479 + continuation.resume(returning: points)
  480 + }
  481 + healthStore.execute(query)
  482 + }
  483 + }
  484 +
419 private func fetchSleepIntervals(startDate: Date, endDate: Date) async throws -> [NativeSleepInterval] { 485 private func fetchSleepIntervals(startDate: Date, endDate: Date) async throws -> [NativeSleepInterval] {
420 guard let type = NativeHealthTypeCatalog.category(.sleepAnalysis) else { 486 guard let type = NativeHealthTypeCatalog.category(.sleepAnalysis) else {
421 throw NativeHealthKitError.invalidType("sleepAnalysis") 487 throw NativeHealthKitError.invalidType("sleepAnalysis")
@@ -52,6 +52,63 @@ struct NativeSleepInterval: Codable { @@ -52,6 +52,63 @@ struct NativeSleepInterval: Codable {
52 struct NativeActivityTarget { 52 struct NativeActivityTarget {
53 let move: Int? 53 let move: Int?
54 let stand: Int? 54 let stand: Int?
  55 + let activityMoveMode: Int?
  56 + // 千卡
  57 + let activeEnergyBurned: Double?
  58 + let activeEnergyBurnedGoal: Double?
  59 + // 公里
  60 + let appleMoveTime: Double?
  61 + let appleMoveTimeGoal: Double?
  62 + // 分钟
  63 + let appleExerciseTime: Double?
  64 + let exerciseTimeGoal: Double?
  65 + //小时
  66 + let appleStandHours: Double?
  67 + let standHoursGoal: Double?
  68 +}
  69 +
  70 +extension NativeActivityTarget {
  71 + var uploadBody: [String: Any] {
  72 + var body: [String: Any] = [:]
  73 + for child in Mirror(reflecting: self).children {
  74 + guard let label = child.label, let value = Self.unwrapOptional(child.value) else {
  75 + continue
  76 + }
  77 + body[label.camelCaseToSnakeCase] = value
  78 + }
  79 + return body
  80 + }
  81 +
  82 + var uploadBodyForDebug: String {
  83 + let pairs = uploadBody
  84 + .map { "\($0.key)=\($0.value)" }
  85 + .sorted()
  86 + .joined(separator: ", ")
  87 + return "{\(pairs)}"
  88 + }
  89 +
  90 + private static func unwrapOptional(_ value: Any) -> Any? {
  91 + let mirror = Mirror(reflecting: value)
  92 + guard mirror.displayStyle == .optional else {
  93 + return value
  94 + }
  95 + return mirror.children.first?.value
  96 + }
  97 +}
  98 +
  99 +private extension String {
  100 + var camelCaseToSnakeCase: String {
  101 + unicodeScalars.reduce(into: "") { result, scalar in
  102 + if CharacterSet.uppercaseLetters.contains(scalar) {
  103 + if !result.isEmpty {
  104 + result.append("_")
  105 + }
  106 + result.append(String(scalar).lowercased())
  107 + } else {
  108 + result.append(String(scalar))
  109 + }
  110 + }
  111 + }
55 } 112 }
56 113
57 enum NativeSleepStage { 114 enum NativeSleepStage {
@@ -269,7 +269,7 @@ private extension NativeHealthDataUploader { @@ -269,7 +269,7 @@ private extension NativeHealthDataUploader {
269 debugLog("[activityTarget] no target data") 269 debugLog("[activityTarget] no target data")
270 return UploadTaskResult(success: true, uploadedCount: 0, errorMessage: nil) 270 return UploadTaskResult(success: true, uploadedCount: 0, errorMessage: nil)
271 } 271 }
272 - debugLog("[activityTarget] uploading, move=\(target.move.map(String.init) ?? "<nil>"), stand=\(target.stand.map(String.init) ?? "<nil>")") 272 + debugLog("[activityTarget] uploading, body=\(target.uploadBodyForDebug)")
273 try await uploadActivityTarget(target) 273 try await uploadActivityTarget(target)
274 debugLog("[activityTarget] upload success") 274 debugLog("[activityTarget] upload success")
275 return UploadTaskResult(success: true, uploadedCount: 1, errorMessage: nil) 275 return UploadTaskResult(success: true, uploadedCount: 1, errorMessage: nil)
@@ -338,7 +338,7 @@ private extension NativeHealthDataUploader { @@ -338,7 +338,7 @@ private extension NativeHealthDataUploader {
338 "value": $0.value, 338 "value": $0.value,
339 ] as [String: Any] 339 ] as [String: Any]
340 } 340 }
341 - try await request(path: "/client/doublefeel/health/data_upload/common/", method: "POST", body: ["data_list": list]) 341 + try await request(path: "/client/doublefeel/health/v2/data_upload/common/", method: "POST", body: ["data_list": list])
342 } 342 }
343 343
344 func uploadSleep(_ data: [NativeSleepInterval]) async throws { 344 func uploadSleep(_ data: [NativeSleepInterval]) async throws {
@@ -349,18 +349,15 @@ private extension NativeHealthDataUploader { @@ -349,18 +349,15 @@ private extension NativeHealthDataUploader {
349 "to_time": $0.toTime, 349 "to_time": $0.toTime,
350 ] as [String: Any] 350 ] as [String: Any]
351 } 351 }
352 - try await request(path: "/client/doublefeel/health/data_upload/sleep/", method: "POST", body: ["data_list": list]) 352 + try await request(path: "/client/doublefeel/health/v2/data_upload/sleep/", method: "POST", body: ["data_list": list])
353 } 353 }
354 354
355 func uploadActivityTarget(_ target: NativeActivityTarget) async throws { 355 func uploadActivityTarget(_ target: NativeActivityTarget) async throws {
356 - var body: [String: Any] = [:]  
357 - body["move"] = target.move  
358 - body["stand"] = target.stand  
359 - try await request(path: "/client/doublefeel/health/activity_target/", method: "POST", body: body) 356 + try await request(path: "/client/doublefeel/health/v2/activity_target/", method: "POST", body: target.uploadBody)
360 } 357 }
361 358
362 func fetchLastUploadTime() async throws -> NativeHealthUploadTimeList { 359 func fetchLastUploadTime() async throws -> NativeHealthUploadTimeList {
363 - let data = try await request(path: "/client/doublefeel/health/data_upload/common/", method: "GET") 360 + let data = try await request(path: "/client/doublefeel/health/v2/data_upload/common/", method: "GET")
364 return try NativeHealthUploadTimeList.decode(from: data) 361 return try NativeHealthUploadTimeList.decode(from: data)
365 } 362 }
366 363
@@ -215,22 +215,66 @@ struct HealthSleepUploadDataPoint: Hashable { @@ -215,22 +215,66 @@ struct HealthSleepUploadDataPoint: Hashable {
215 struct HealthActivityTargetData: Hashable { 215 struct HealthActivityTargetData: Hashable {
216 var move: Int64? = nil 216 var move: Int64? = nil
217 var stand: Int64? = nil 217 var stand: Int64? = nil
  218 + var activityMoveMode: Int64? = nil
  219 + var activeEnergyBurned: Double? = nil
  220 + var activeEnergyBurnedGoal: Double? = nil
  221 + var appleMoveTime: Double? = nil
  222 + var appleMoveTimeGoal: Double? = nil
  223 + var appleExerciseTime: Double? = nil
  224 + var appleExerciseTimeGoal: Double? = nil
  225 + var exerciseTimeGoal: Double? = nil
  226 + var appleStandHours: Double? = nil
  227 + var appleStandHoursGoal: Double? = nil
  228 + var standHoursGoal: Double? = nil
218 229
219 230
220 // swift-format-ignore: AlwaysUseLowerCamelCase 231 // swift-format-ignore: AlwaysUseLowerCamelCase
221 static func fromList(_ pigeonVar_list: [Any?]) -> HealthActivityTargetData? { 232 static func fromList(_ pigeonVar_list: [Any?]) -> HealthActivityTargetData? {
222 let move: Int64? = nilOrValue(pigeonVar_list[0]) 233 let move: Int64? = nilOrValue(pigeonVar_list[0])
223 let stand: Int64? = nilOrValue(pigeonVar_list[1]) 234 let stand: Int64? = nilOrValue(pigeonVar_list[1])
  235 + let activityMoveMode: Int64? = nilOrValue(pigeonVar_list[2])
  236 + let activeEnergyBurned: Double? = nilOrValue(pigeonVar_list[3])
  237 + let activeEnergyBurnedGoal: Double? = nilOrValue(pigeonVar_list[4])
  238 + let appleMoveTime: Double? = nilOrValue(pigeonVar_list[5])
  239 + let appleMoveTimeGoal: Double? = nilOrValue(pigeonVar_list[6])
  240 + let appleExerciseTime: Double? = nilOrValue(pigeonVar_list[7])
  241 + let appleExerciseTimeGoal: Double? = nilOrValue(pigeonVar_list[8])
  242 + let exerciseTimeGoal: Double? = nilOrValue(pigeonVar_list[9])
  243 + let appleStandHours: Double? = nilOrValue(pigeonVar_list[10])
  244 + let appleStandHoursGoal: Double? = nilOrValue(pigeonVar_list[11])
  245 + let standHoursGoal: Double? = nilOrValue(pigeonVar_list[12])
224 246
225 return HealthActivityTargetData( 247 return HealthActivityTargetData(
226 move: move, 248 move: move,
227 - stand: stand 249 + stand: stand,
  250 + activityMoveMode: activityMoveMode,
  251 + activeEnergyBurned: activeEnergyBurned,
  252 + activeEnergyBurnedGoal: activeEnergyBurnedGoal,
  253 + appleMoveTime: appleMoveTime,
  254 + appleMoveTimeGoal: appleMoveTimeGoal,
  255 + appleExerciseTime: appleExerciseTime,
  256 + appleExerciseTimeGoal: appleExerciseTimeGoal,
  257 + exerciseTimeGoal: exerciseTimeGoal,
  258 + appleStandHours: appleStandHours,
  259 + appleStandHoursGoal: appleStandHoursGoal,
  260 + standHoursGoal: standHoursGoal
228 ) 261 )
229 } 262 }
230 func toList() -> [Any?] { 263 func toList() -> [Any?] {
231 return [ 264 return [
232 move, 265 move,
233 stand, 266 stand,
  267 + activityMoveMode,
  268 + activeEnergyBurned,
  269 + activeEnergyBurnedGoal,
  270 + appleMoveTime,
  271 + appleMoveTimeGoal,
  272 + appleExerciseTime,
  273 + appleExerciseTimeGoal,
  274 + exerciseTimeGoal,
  275 + appleStandHours,
  276 + appleStandHoursGoal,
  277 + standHoursGoal,
234 ] 278 ]
235 } 279 }
236 static func == (lhs: HealthActivityTargetData, rhs: HealthActivityTargetData) -> Bool { 280 static func == (lhs: HealthActivityTargetData, rhs: HealthActivityTargetData) -> Bool {
@@ -161,7 +161,16 @@ final class HealthKitHostApiImpl: HealthKitHostApi { @@ -161,7 +161,16 @@ final class HealthKitHostApiImpl: HealthKitHostApi {
161 completion(.success(target.map { 161 completion(.success(target.map {
162 HealthActivityTargetData( 162 HealthActivityTargetData(
163 move: $0.move.map(Int64.init), 163 move: $0.move.map(Int64.init),
164 - stand: $0.stand.map(Int64.init) 164 + stand: $0.stand.map(Int64.init),
  165 + activityMoveMode: $0.activityMoveMode.map(Int64.init),
  166 + activeEnergyBurned: $0.activeEnergyBurned,
  167 + activeEnergyBurnedGoal: $0.activeEnergyBurnedGoal,
  168 + appleMoveTime: $0.appleMoveTime,
  169 + appleMoveTimeGoal: $0.appleMoveTimeGoal,
  170 + appleExerciseTime: $0.appleExerciseTime,
  171 + exerciseTimeGoal: $0.exerciseTimeGoal,
  172 + appleStandHours: $0.appleStandHours,
  173 + standHoursGoal: $0.standHoursGoal
165 ) 174 )
166 })) 175 }))
167 } catch { 176 } catch {