HealthKitHostApiImpl.swift 6.86 KB
import Foundation

final class HealthKitHostApiImpl: HealthKitHostApi {
  private let service: HealthKitService

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

  func checkHealthAppAuthorization() throws -> Bool {
    service.isHealthDataAvailable && !runBlocking {
      await self.service.shouldRequestAuthorization()
    }
  }

  func getHealthServerAuthUrl() throws -> String {
    // Apple Health authorization is system-managed, not URL based.
    ""
  }

  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) throws -> [HealthUploadDataPoint] {
    try fetchCommon(startTime: startTime, endTime: endTime, service.fetchHrvData)
  }

  func fetchHeartRateData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
    try fetchCommon(startTime: startTime, endTime: endTime, service.fetchHeartRateData)
  }

  func fetchWalkingHeartRateData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
    try fetchCommon(startTime: startTime, endTime: endTime, service.fetchWalkingHeartRateData)
  }

  func fetchRestingHeartRateData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
    try fetchCommon(startTime: startTime, endTime: endTime, service.fetchRestingHeartRateData)
  }

  func fetchSleepingHeartRateData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
    try fetchCommon(startTime: startTime, endTime: endTime, service.fetchSleepingHeartRateData)
  }

  func fetchOxygenSaturationData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
    try fetchCommon(startTime: startTime, endTime: endTime, service.fetchOxygenSaturationData)
  }

  func fetchActiveEnergyData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
    try fetchCommon(startTime: startTime, endTime: endTime, service.fetchActiveEnergyData)
  }

  func fetchExerciseData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
    try fetchCommon(startTime: startTime, endTime: endTime, service.fetchExerciseData)
  }

  func fetchStandData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
    try fetchCommon(startTime: startTime, endTime: endTime, service.fetchStandData)
  }

  func fetchStepCountData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
    try fetchCommon(startTime: startTime, endTime: endTime, service.fetchStepCountData)
  }

  func fetchSleepData(startTime: Int64, endTime: Int64) throws -> [HealthSleepUploadDataPoint] {
    let range = makeDateRange(startTime: startTime, endTime: endTime)
    let intervals = try runBlockingThrows {
      try await self.service.fetchSleepData(startDate: range.startDate, endDate: range.endDate)
    }
    return intervals.map { interval in
      HealthSleepUploadDataPoint(
        dataType: Int64(interval.dataType),
        fromTime: Int64(interval.fromTime.rounded()),
        toTime: Int64(interval.toTime.rounded())
      )
    }
  }

  func fetchSleepingWristTemperatureData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
    try fetchCommon(startTime: startTime, endTime: endTime, service.fetchSleepingWristTemperatureData)
  }

  func fetchRespiratoryRateData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
    try fetchCommon(startTime: startTime, endTime: endTime, service.fetchRespiratoryRateData)
  }

  func fetchIrregularHeartRhythmData(startTime: Int64, endTime: Int64) throws -> [HealthUploadDataPoint] {
    try fetchCommon(startTime: startTime, endTime: endTime, service.fetchIrregularHeartRhythmData)
  }

  func fetchActivityTargetData(startTime: Int64, endTime: Int64) throws -> HealthActivityTargetData? {
    let range = makeDateRange(startTime: startTime, endTime: endTime)
    let target = try runBlockingThrows {
      try await self.service.fetchActivityTargetData(startDate: range.startDate, endDate: range.endDate)
    }
    guard let target else { return nil }
    return HealthActivityTargetData(
      move: target.move.map(Int64.init),
      stand: target.stand.map(Int64.init)
    )
  }

  private func fetchCommon(
    startTime: Int64,
    endTime: Int64,
    _ fetch: @escaping (Date, Date) async throws -> [NativeHealthDataPoint]
  ) throws -> [HealthUploadDataPoint] {
    let range = makeDateRange(startTime: startTime, endTime: endTime)
    let points = try runBlockingThrows {
      try await fetch(range.startDate, range.endDate)
    }
    return points.map { point in
      HealthUploadDataPoint(
        dataType: Int64(point.dataType.rawValue),
        time: Int64(point.time.rounded()),
        value: point.value
      )
    }
  }

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