HealthKitHostApiImpl.swift 8.29 KB
import Foundation
import HealthKit

final class HealthKitHostApiImpl: HealthKitHostApi {
  private let service: HealthKitService

  init(service: HealthKitService = .shared) {
    self.service = service
  }

  func getHealthServerAuthUrl(completion: @escaping (Result<String, Error>) -> Void) {
    // Apple Health authorization is system-managed, not URL based.
    completion(.success(""))
  }

  func cancelHealthAppAuthorization() throws -> Bool {
    // iOS does not let apps revoke HealthKit permission programmatically.
    // Users must revoke access in Settings > Health > Data Access & Devices.
    false
  }
    
  func checkHealthAppAuthorization(completion: @escaping (Result<HealthAuthorization, any Error>) -> Void) {
    Task {
      guard service.isHealthDataAvailable else {
        completion(.success(HealthAuthorization(status: -1, hasData: false)))
        return
      }
        
      let requestStatus = await service.authorizationRequestStatus()
      let status: Int64
      var hasData = false
      if requestStatus == .shouldRequest {
        status = 0
      } else {
          hasData = await service.hasAnyReadableData()
        status = hasData ? 1 : 2
      }
        DebugLogger.log(desc: "checkHealthAppAuthorization result: requestStatus=\(status) hasData=\(hasData)")
      completion(.success(HealthAuthorization(status: status, hasData: hasData)))
    }
  }

  func requestHealthClientAuthorization(completion: @escaping (Result<Bool, any Error>) -> Void) {
      DebugLogger.log(desc:"requestHealthClientAuthorization")
    service.requestAuthorization { [service] success, error in
      if let error {
          DebugLogger.log(desc:"requestHealthClientAuthorization error: \(error)")
        completion(.failure(error))
        return
      }

      Task {
        let requestStatus = await service.authorizationRequestStatus()
          
         let status: Int64
          if requestStatus == .shouldRequest {
            status = 0
          } else {
              let hasData = await service.hasAnyReadableData()
            status = hasData ? 1 : 2
          }
          
        let granted = status == 1
          DebugLogger.log(desc:"requestHealthClientAuthorization granted: \(granted)")
        if granted {
          service.startBackgroundObserversIfNeeded()
          await service.refreshSharedWatchValues()
        }
        completion(.success(granted))
      }
    }
  }

    func performHealthUpload(completion: @escaping (Result<HealthUploadResult, any Error>) -> Void) {
        Task{
            let summary = await NativeHealthDataUploader.shared.uploadAll(service: self.service)
            let result = HealthUploadResult(
                commonUploadSuccess: summary.commonUploadSuccess,
                sleepUploadSuccess: summary.sleepUploadSuccess,
                errorMessage: summary.errorMessage
              )
            completion(.success(result))
        }
    }

  func fetchHrvData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void) {
    fetchCommon(startTime: startTime, endTime: endTime, service.fetchHrvData, completion: completion)
  }

  func fetchHeartRateData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void) {
    fetchCommon(startTime: startTime, endTime: endTime, service.fetchHeartRateData, completion: completion)
  }

  func fetchWalkingHeartRateData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void) {
    fetchCommon(startTime: startTime, endTime: endTime, service.fetchWalkingHeartRateData, completion: completion)
  }

  func fetchRestingHeartRateData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void) {
    fetchCommon(startTime: startTime, endTime: endTime, service.fetchRestingHeartRateData, completion: completion)
  }

  func fetchSleepingHeartRateData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void) {
    fetchCommon(startTime: startTime, endTime: endTime, service.fetchSleepingHeartRateData, completion: completion)
  }

  func fetchOxygenSaturationData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void) {
    fetchCommon(startTime: startTime, endTime: endTime, service.fetchOxygenSaturationData, completion: completion)
  }

  func fetchActiveEnergyData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void) {
    fetchCommon(startTime: startTime, endTime: endTime, service.fetchActiveEnergyData, completion: completion)
  }

  func fetchExerciseData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void) {
    fetchCommon(startTime: startTime, endTime: endTime, service.fetchExerciseData, completion: completion)
  }

  func fetchStandData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void) {
    fetchCommon(startTime: startTime, endTime: endTime, service.fetchStandData, completion: completion)
  }

  func fetchStepCountData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void) {
    fetchCommon(startTime: startTime, endTime: endTime, service.fetchStepCountData, completion: completion)
  }

  func fetchSleepData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthSleepUploadDataPoint], Error>) -> Void) {
    let range = makeDateRange(startTime: startTime, endTime: endTime)
    Task {
      do {
        let intervals = try await service.fetchSleepData(startDate: range.startDate, endDate: range.endDate)
        completion(.success(intervals.map { interval in
          HealthSleepUploadDataPoint(
            dataType: Int64(interval.dataType),
            fromTime: Int64(interval.fromTime.rounded()),
            toTime: Int64(interval.toTime.rounded())
          )
        }))
      } catch {
        completion(.failure(error))
      }
    }
  }

  func fetchSleepingWristTemperatureData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void) {
    fetchCommon(startTime: startTime, endTime: endTime, service.fetchSleepingWristTemperatureData, completion: completion)
  }

  func fetchRespiratoryRateData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void) {
    fetchCommon(startTime: startTime, endTime: endTime, service.fetchRespiratoryRateData, completion: completion)
  }

  func fetchIrregularHeartRhythmData(startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void) {
    fetchCommon(startTime: startTime, endTime: endTime, service.fetchIrregularHeartRhythmData, completion: completion)
  }

  func fetchActivityTargetData(startTime: Int64, endTime: Int64, completion: @escaping (Result<HealthActivityTargetData?, Error>) -> Void) {
    let range = makeDateRange(startTime: startTime, endTime: endTime)
    Task {
      do {
        let target = try await service.fetchActivityTargetData(startDate: range.startDate, endDate: range.endDate)
        completion(.success(target.map {
          HealthActivityTargetData(
            move: $0.move.map(Int64.init),
            stand: $0.stand.map(Int64.init)
          )
        }))
      } catch {
        completion(.failure(error))
      }
    }
  }

  private func fetchCommon(
    startTime: Int64,
    endTime: Int64,
    _ fetch: @escaping (Date, Date) async throws -> [NativeHealthDataPoint],
    completion: @escaping (Result<[HealthUploadDataPoint], Error>) -> Void
  ) {
    let range = makeDateRange(startTime: startTime, endTime: endTime)
    Task {
      do {
        let points = try await fetch(range.startDate, range.endDate)
        completion(.success(points.map { point in
          HealthUploadDataPoint(
            dataType: Int64(point.dataType.rawValue),
            time: Int64(point.time.rounded()),
            value: point.value
          )
        }))
      } catch {
        completion(.failure(error))
      }
    }
  }

  private func makeDateRange(startTime: Int64, endTime: Int64) -> (startDate: Date, endDate: Date) {
    (
      Date(timeIntervalSince1970: TimeInterval(startTime)),
      Date(timeIntervalSince1970: TimeInterval(endTime))
    )
  }
}