WatchConnectivityService.swift 7.65 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
    if session.activationState == .notActivated {
      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",
      isPaired: isPaired,
      isReachable: isReachable,
      isWatchAppInstalled: isWatchAppInstalled
    )
  }

    func transferLoginInfo(userInfo: UserInfoModel?){
        guard activate() else { return }
        let params = loginPayload(userInfo: userInfo)
        do{
            try WCSession.default.updateApplicationContext(params)
            print("Send Watch updateApplicationContext: \(params)")
        }catch{
            print("Error Send Watch updateApplicationContext: \(params)")
        }
    }

    func sendLoginInfoMessage(userInfo: UserInfoModel?) -> Bool {
        guard let userInfo else {
            return sendCommandMessage(AppGroupMessageKey.logout)
        }

        guard let json = userInfo.jsonString else {
            return false
        }
        return sendCommandMessage(AppGroupMessageKey.login, json: json)
    }

    private func loginPayload(userInfo: UserInfoModel?) -> [String: Any] {
        guard var params = userInfo?.toJson(), !params.isEmpty else {
            return ["command": AppGroupMessageKey.logout]
        }
        params["command"] = AppGroupMessageKey.login
        return params
    }

  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 sendCommandMessage(_ message: String, json: String? = nil) -> Bool {
      activate()
      guard WCSession.default.isReachable else { return false }
        var params: [String: Any] = [
            "command": message
        ]
        params["json"] = json

      WCSession.default.sendMessage(params, replyHandler: nil) { error in
        print("Watch message send failed: \(error.localizedDescription)")
      }
      return true
    }

    func sendWatchThemeChangedMessage(
        theme: WatchTransformTheme,
        completion: @escaping (Result<Bool, Error>) -> Void
    ) {
        var payload: [String: Any] = [
            "theme_id": Int(theme.themeId),
            "theme_name": theme.themeName,
            "energetic_description": theme.energeticDescription,
            "normal_description": theme.normalDescription,
            "slight_stressful_description": theme.slightStressfulDescription,
            "stressful_description": theme.stressfulDescription,
        ]
        payload["energetic_image"] = theme.energeticImage?.data
        payload["normal_image"] = theme.normalImage?.data
        payload["slight_stressful_image"] = theme.slightStressfulImage?.data
        payload["stressful_image"] = theme.stressfulImage?.data

        sendReliableCommandMessage(
            AppGroupMessageKey.reloadTheme,
            payload: payload,
            completion: completion
        )
    }

    func deleteWatchTheme(_ completion: @escaping (Result<Bool, Error>) -> Void) {
        sendReliableCommandMessage(
            AppGroupMessageKey.deleteTheme,
            completion: completion
        )
    }

    private func commandPayload(
        _ message: String,
        json: String? = nil,
        payload: [String: Any]? = nil
    ) -> [String: Any] {
        var params: [String: Any] = [
            "command": message
        ]
        params["json"] = json
        if let payload {
            params["payload"] = payload
        }
        return params
    }

    private func sendReliableCommandMessage(
        _ message: String,
        json: String? = nil,
        payload: [String: Any]? = nil,
        completion: @escaping (Result<Bool, Error>) -> Void
    ) {
        guard activate() else {
            completion(.success(false))
            return
        }

        let params = commandPayload(message, json: json, payload: payload)
        WCSession.default.transferUserInfo(params)
        if WCSession.default.isReachable {
            WCSession.default.sendMessage(params, replyHandler: { _ in
                completion(.success(true))
            }) { error in
                print("Watch reliable message send failed: \(error.localizedDescription)")
                completion(.success(true))
            }
        } else {
            completion(.success(true))
        }
    }
}

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

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

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

  func sessionReachabilityDidChange(_ session: WCSession) {
    refreshState()
    if session.isReachable {
      _ = sendLoginInfoMessage(userInfo: AppShared.shared.myUserInfo)
    }
  }

  func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
    if message["command"] as? String == AppGroupMessageKey.watchDeviceToken,
       let token = message["token"] as? String {
      AppGroupConstants.defaults?.set(token, forKey: AppGroupConstants.Key.watchDeviceToken)
      Task {
        await AppShared.shared.reportDeviceInfo()
      }
    }
  }

  func session(
    _ session: WCSession,
    didReceiveMessage message: [String: Any],
    replyHandler: @escaping ([String: Any]) -> Void
  ) {
    if message["command"] as? String == AppGroupMessageKey.requestLoginState {
      let userInfo = AppShared.shared.myUserInfo
      var reply = loginPayload(userInfo: userInfo)
      if let userInfo, let json = userInfo.jsonString {
        reply["json"] = json
      }
      replyHandler(reply)
    } else if message["command"] as? String == AppGroupMessageKey.watchDeviceToken,
              let token = message["token"] as? String {
      AppGroupConstants.defaults?.set(token, forKey: AppGroupConstants.Key.watchDeviceToken)
      Task {
        await AppShared.shared.reportDeviceInfo()
      }
      replyHandler(["success": true])
    } else if message["command"] as? String == AppGroupMessageKey.statusPulseRefresh {
      replyHandler(["success": true])
    } else {
      replyHandler(["success": true])
    }
  }
}