HealthKitHostApiImpl.swift 8.14 KB
import Foundation

final class HealthKitHostApiImpl: HealthKitHostApi {
  private let service: HealthKitService

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

  func checkHealthAppAuthorization(completion: @escaping (Result<Bool, Error>) -> Void) {
    Task {
      let shouldAuth = await service.shouldRequestAuthorization()
      let isAuthorized = service.isHealthDataAvailable && !shouldAuth
      completion(.success(isAuthorized))
    }
  }

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

  func requestHealthClientAuthorization() throws -> Bool {
    let result = runAuthorizationRequest()
    if result.success {
      service.startBackgroundObserversIfNeeded()
      Task {
        await service.refreshSharedWatchValues()
      }
    }
    if let error = result.error {
      throw error
    }
    return result.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 performHealthUpload() throws -> HealthUploadResult {
    let summary = runBlocking {
      await self.service.performLocalSync()
    }

    return HealthUploadResult(
      commonUploadSuccess: summary.commonCount >= 0,
      sleepUploadSuccess: summary.sleepCount >= 0,
      errorMessage: nil
    )
  }

  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))
    )
  }

  private func runAuthorizationRequest() -> (success: Bool, error: Error?) {
    var result: (Bool, Error?) = (false, nil)
    let semaphore = DispatchSemaphore(value: 0)
    service.requestAuthorization { success, error in
      result = (success, error)
      semaphore.signal()
    }
    waitForSemaphore(semaphore)
    return result
  }
}

private func runBlocking<T>(_ operation: @escaping () async -> T) -> T {
  let semaphore = DispatchSemaphore(value: 0)
  var result: T?
  Task {
    result = await operation()
    semaphore.signal()
  }
  waitForSemaphore(semaphore)
  return result!
}

private func runBlockingThrows<T>(_ operation: @escaping () async throws -> T) throws -> T {
  let semaphore = DispatchSemaphore(value: 0)
  var result: Result<T, Error>?
  Task {
    do {
      result = .success(try await operation())
    } catch {
      result = .failure(error)
    }
    semaphore.signal()
  }
  waitForSemaphore(semaphore)
  return try result!.get()
}

private func waitForSemaphore(_ semaphore: DispatchSemaphore) {
  if Thread.isMainThread {
    while semaphore.wait(timeout: .now() + 0.05) == .timedOut {
      RunLoop.main.run(mode: .default, before: Date(timeIntervalSinceNow: 0.05))
    }
  } else {
    semaphore.wait()
  }
}