HealthKitHostApiImpl.swift 2.19 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
    )
  }

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