// // BridgeModule+Watch.swift // Werkout_ios // // Created by Trey Tartt on 6/19/24. // import Foundation import WatchConnectivity import AVFoundation import HealthKit import os import SharedCore private let watchBridgeLogger = Logger(subsystem: "com.werkout.ios", category: "watch-bridge") extension BridgeModule: WCSessionDelegate { private func send(action: Action) { do { let data = try JSONEncoder().encode(action) send(data) } catch { watchBridgeLogger.error("Failed to encode watch action: \(error.localizedDescription, privacy: .public)") } } private func sendInExerciseAction(_ model: WatchPackageModel) { do { let action = PhoneToWatchActions.inExercise(model) let payload = try JSONEncoder().encode(action) guard payload != lastSentInExercisePayload else { return } lastSentInExercisePayload = payload send(payload) } catch { watchBridgeLogger.error("Failed to encode in-exercise watch action: \(error.localizedDescription, privacy: .public)") } } private func flushQueuedWatchMessages() { let queuedMessages = queuedWatchMessages.dequeueAll() guard queuedMessages.isEmpty == false else { return } queuedMessages.forEach { send($0) } } private func enqueueWatchMessage(_ data: Data) { let droppedCount = queuedWatchMessages.enqueue(data) if droppedCount > 0 { watchBridgeLogger.warning("Dropping oldest queued watch message to enforce queue cap") } } func sendResetToWatch() { lastSentInExercisePayload = nil send(action: PhoneToWatchActions.reset) } func sendStartWorkoutToWatch() { lastSentInExercisePayload = nil send(action: PhoneToWatchActions.startWorkout) } func sendWorkoutCompleteToWatch() { lastSentInExercisePayload = nil send(action: PhoneToWatchActions.endWorkout) } func sendCurrentExerciseToWatch() { if let currentExercise = currentWorkoutInfo.currentExercise, let duration = currentExercise.duration , duration > 0 { let watchModel = WatchPackageModel(currentExerciseName: currentExercise.exercise.name, currentExerciseID: currentExercise.id ?? -1, currentTimeLeft: currentExerciseTimeLeft, workoutStartDate: workoutStartDate ?? Date()) sendInExerciseAction(watchModel) } else { if let currentExercise = currentWorkoutInfo.currentExercise, let reps = currentExercise.reps, reps > 0 { // if not a timer we need to set the watch display with number of reps // if timer it will set when timer updates let watchModel = WatchPackageModel(currentExerciseName: currentExercise.exercise.name, currentExerciseID: currentExercise.id ?? -1, currentTimeLeft: reps, workoutStartDate: self.workoutStartDate ?? Date()) self.sendInExerciseAction(watchModel) } } } func session(_ session: WCSession, didReceiveMessageData messageData: Data) { do { let model = try WatchPayloadValidation.decode(WatchActions.self, from: messageData) DispatchQueue.main.async { switch model { case .nextExercise: self.nextExercise() AudioEngine.shared.playFinished() case .workoutComplete(let data): do { let finishModel = try WatchPayloadValidation.decode(WatchFinishWorkoutModel.self, from: data) self.healthKitUUID = finishModel.healthKitUUID } catch { watchBridgeLogger.error("Rejected watch completion payload: \(error.localizedDescription, privacy: .public)") } case .restartExercise: self.restartExercise() case .previousExercise: self.previousExercise() case .stopWorkout: self.completeWorkout() case .pauseWorkout: self.pauseWorkout() } } } catch { watchBridgeLogger.error("Rejected WatchActions payload: \(error.localizedDescription, privacy: .public)") } } func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { DispatchQueue.main.async { switch activationState { case .notActivated: watchBridgeLogger.info("Watch session notActivated") case .inactive: watchBridgeLogger.info("Watch session inactive") case .activated: watchBridgeLogger.info("Watch session activated") self.flushQueuedWatchMessages() #if os(iOS) let workoutConfiguration = HKWorkoutConfiguration() workoutConfiguration.activityType = .functionalStrengthTraining workoutConfiguration.locationType = .indoor if WCSession.isSupported(), session.activationState == .activated, session.isWatchAppInstalled { HKHealthStore().startWatchApp(with: workoutConfiguration, completion: { (success, error) in if let error = error { watchBridgeLogger.error("Failed to start watch app: \(error.localizedDescription, privacy: .public)") } }) } #endif @unknown default: watchBridgeLogger.error("Unknown WCSession activation state") } } } #if os(iOS) func sessionDidBecomeInactive(_ session: WCSession) { } func sessionDidDeactivate(_ session: WCSession) { session.activate() } #endif func send(_ data: Data) { guard WCSession.isSupported() else { return } guard session.activationState == .activated else { enqueueWatchMessage(data) session.activate() return } #if os(iOS) guard session.isWatchAppInstalled else { return } #else guard session.isCompanionAppInstalled else { return } #endif if session.isReachable { session.sendMessageData(data, replyHandler: nil) { error in watchBridgeLogger.error("Cannot send watch message: \(error.localizedDescription, privacy: .public)") DispatchQueue.main.async { self.enqueueWatchMessage(data) } } } else { session.transferUserInfo(["package": data]) } } }