WatchConnectivityService.swift 4.73 KB
import Foundation
import UIKit
import WatchConnectivity

/// iPhone-side WatchConnectivity wrapper.
/// This keeps Flutter-facing WearEngine APIs mapped to Apple Watch behavior.
final class WatchConnectivityService: NSObject {
  static let shared = WatchConnectivityService()

  private(set) var isPaired = false
  private(set) var isReachable = false
  private(set) var isWatchAppInstalled = false

  private override init() {
    super.init()
  }

  @discardableResult
  func activate() -> Bool {
    guard WCSession.isSupported() else { return false }
    let session = WCSession.default
    session.delegate = self
    session.activate()
    refreshState()
    return true
  }

  func refreshState() {
    guard WCSession.isSupported() else {
      isPaired = false
      isReachable = false
      isWatchAppInstalled = false
      return
    }
    let session = WCSession.default
    isPaired = session.isPaired
    isReachable = session.isReachable
    isWatchAppInstalled = session.isWatchAppInstalled
  }

  func hasAvailableDevices() -> Bool {
    activate()
    refreshState()
    return isPaired && isWatchAppInstalled
  }

  func currentDeviceInfo() -> WearDeviceInfo? {
    activate()
    refreshState()
    guard isPaired else { return nil }
    return WearDeviceInfo(
      deviceId: nil,
      deviceName: "Apple Watch",
      isConnected: isReachable && isWatchAppInstalled
    )
  }

  func sendTextMessage(_ message: String) -> Bool {
    activate()
    guard WCSession.default.isReachable else { return false }
    WCSession.default.sendMessage(["message": message], replyHandler: nil) { error in
      print("Watch message failed: \(error.localizedDescription)")
    }
    return true
  }

  func syncPayload(jsonPayload: String) -> Bool {
    activate()
    guard let data = jsonPayload.data(using: .utf8),
          let object = try? JSONSerialization.jsonObject(with: data),
          let payload = object as? [String: Any] else {
      return false
    }

    persistThemeIfPresent(payload: payload, originalJSON: jsonPayload)

    guard WCSession.isSupported(), WCSession.default.isPaired, WCSession.default.isWatchAppInstalled else {
      return false
    }

    WCSession.default.transferUserInfo(payload)
    try? WCSession.default.updateApplicationContext(payload)
    sendWatchThemeChangedMessageIfNeeded(payload: payload)
    return true
  }

  func sendWatchThemeChangedMessage() {
    activate()
    guard WCSession.default.isReachable else { return }
    WCSession.default.sendMessage(["command": "watchThemeChanged"], replyHandler: nil) { error in
      print("Watch theme change message failed: \(error.localizedDescription)")
    }
  }

  private func sendWatchThemeChangedMessageIfNeeded(payload: [String: Any]) {
    if payload["myWatchTheme"] != nil || payload["watchTheme"] != nil || payload["theme"] != nil {
      sendWatchThemeChangedMessage()
    }
  }

  private func persistThemeIfPresent(payload: [String: Any], originalJSON: String) {
    if let theme = payload["myWatchTheme"] as? String {
      WatchThemeStore.shared.saveThemeJSONString(theme)
      return
    }

    let themeObject = payload["watchTheme"] ?? payload["theme"]
    guard let themeObject,
          JSONSerialization.isValidJSONObject(themeObject),
          let data = try? JSONSerialization.data(withJSONObject: themeObject),
          let json = String(data: data, encoding: .utf8) else {
      if payload["isWatchTheme"] as? Bool == true {
        WatchThemeStore.shared.saveThemeJSONString(originalJSON)
      }
      return
    }
    WatchThemeStore.shared.saveThemeJSONString(json)
  }
}

extension WatchConnectivityService: WCSessionDelegate {
  func session(
    _ session: WCSession,
    activationDidCompleteWith activationState: WCSessionActivationState,
    error: Error?
  ) {
    refreshState()
    if let error {
      print("WatchConnectivity activation failed: \(error.localizedDescription)")
    }
  }

  func sessionDidBecomeInactive(_ session: WCSession) {
    refreshState()
  }

  func sessionDidDeactivate(_ session: WCSession) {
    refreshState()
    session.activate()
  }

  func sessionReachabilityDidChange(_ session: WCSession) {
    refreshState()
  }

  func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
    if message["command"] as? String == "statusPulseRefresh" {
      Task {
        await HealthKitService.shared.refreshSharedWatchValues()
      }
    }
  }

  func session(
    _ session: WCSession,
    didReceiveMessage message: [String: Any],
    replyHandler: @escaping ([String: Any]) -> Void
  ) {
    if message["command"] as? String == "statusPulseRefresh" {
      Task {
        await HealthKitService.shared.refreshSharedWatchValues()
        replyHandler(["success": true])
      }
    } else {
      replyHandler(["success": true])
    }
  }
}