HealthKitHostApiImpl.swift 9.47 KB
import Foundation
import HealthKit

final class HealthKitHostApiImpl: HealthKitHostApi {
  private let service: HealthKitService

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

    
  func checkHealthAppAuthorization(completion: @escaping (Result<HealthAuthorization, any Error>) -> Void) {
    Task {
      let authorization: HealthAuthorization
      let requestStatus: HKAuthorizationRequestStatus?

      if !service.isHealthDataAvailable {
        requestStatus = nil
        authorization = HealthAuthorization(status: -1, hasData: false)
      } else {
        requestStatus = await service.authorizationRequestStatus()
        if requestStatus == .shouldRequest {
          authorization = HealthAuthorization(status: 0, hasData: false)
        } else {
          // `.unnecessary` means another authorization request is not needed;
          // HealthKit does not reveal whether read access was granted or denied.
          // Probe readable samples to preserve the existing status contract.
          let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date())
            ?? Date(timeIntervalSinceNow: -30 * 24 * 60 * 60)
          let hasData = await service.hasAnyReadableData(startingAt: oneMonthAgo)
          authorization = HealthAuthorization(status: hasData ? 1 : 2, hasData: hasData)
        }
      }

      DebugLogger.debugLog(
        "checkHealthAppAuthorization result: requestStatus=\(String(describing: requestStatus)) status=\(authorization.status) hasData=\(authorization.hasData)"
      )
      await MainActor.run {
        completion(.success(authorization))
      }
    }
  }

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

      Task {
        // HealthKit deliberately does not expose read authorization per type.
        // A successful authorization request means the sheet completed; an
        // empty store must not be treated as denied permission.
        let granted = success
        DebugLogger.debugLog("requestHealthClientAuthorization granted: \(granted)")
        if granted {
          service.restartBackgroundObserversAfterAuthorization()
          _ = await AnchoredHealthDataUploader.shared.uploadAll()
        }
        completion(.success(granted))
      }
    }
  }

    func performHealthUpload(completion: @escaping (Result<HealthUploadResult, any Error>) -> Void) {
        Task{
            let summary = await AnchoredHealthDataUploader.shared.uploadAll()
            let result = HealthUploadResult(
                commonUploadSuccess: summary.commonUploadSuccess,
                sleepUploadSuccess: summary.sleepUploadSuccess,
                errorMessage: summary.errorMessage
              )
            completion(.success(result))
        }
    }
   //MARK: -DEBUG
    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 getDebugCurrentUploadedData() throws -> [HealthUploadDataPoint] {
      []
    }

  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),
//            activityMoveMode: $0.activityMoveMode.map(Int64.init),
//            activeEnergyBurned: $0.activeEnergyBurned,
//            activeEnergyBurnedGoal: $0.activeEnergyBurnedGoal,
//            appleMoveTime: $0.appleMoveTime,
//            appleMoveTimeGoal: $0.appleMoveTimeGoal,
//            appleExerciseTime: $0.appleExerciseTime,
//            exerciseTimeGoal: $0.exerciseTimeGoal,
//            appleStandHours: $0.appleStandHours,
//            standHoursGoal: $0.standHoursGoal
          )
        }))
      } 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))
    )
  }
}