Stabilize iOS/watchOS/tvOS apps and add cross-platform audit remediation

This commit is contained in:
Trey t
2026-02-11 12:54:40 -06:00
parent e40275e694
commit acce712261
77 changed files with 2940 additions and 765 deletions

View File

@@ -9,27 +9,63 @@ 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: Encodable>(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() {
let watchModel = PhoneToWatchActions.reset
let data = try! JSONEncoder().encode(watchModel)
send(data)
// self.session.transferUserInfo(["package": data])
lastSentInExercisePayload = nil
send(action: PhoneToWatchActions.reset)
}
func sendStartWorkoutToWatch() {
let model = PhoneToWatchActions.startWorkout
let data = try! JSONEncoder().encode(model)
send(data)
// self.session.transferUserInfo(["package": data])
lastSentInExercisePayload = nil
send(action: PhoneToWatchActions.startWorkout)
}
func sendWorkoutCompleteToWatch() {
let model = PhoneToWatchActions.endWorkout
let data = try! JSONEncoder().encode(model)
send(data)
// self.session.transferUserInfo(["package": data])
lastSentInExercisePayload = nil
send(action: PhoneToWatchActions.endWorkout)
}
func sendCurrentExerciseToWatch() {
@@ -40,9 +76,7 @@ extension BridgeModule: WCSessionDelegate {
currentExerciseID: currentExercise.id ?? -1,
currentTimeLeft: currentExerciseTimeLeft,
workoutStartDate: workoutStartDate ?? Date())
let model = PhoneToWatchActions.inExercise(watchModel)
let data = try! JSONEncoder().encode(model)
send(data)
sendInExerciseAction(watchModel)
} else {
if let currentExercise = currentWorkoutInfo.currentExercise,
let reps = currentExercise.reps,
@@ -51,33 +85,38 @@ extension BridgeModule: WCSessionDelegate {
// 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())
let model = PhoneToWatchActions.inExercise(watchModel)
let data = try! JSONEncoder().encode(model)
self.send(data)
self.sendInExerciseAction(watchModel)
}
}
}
func session(_ session: WCSession, didReceiveMessageData messageData: Data) {
if let model = try? JSONDecoder().decode(WatchActions.self, from: messageData) {
switch model {
case .nextExercise:
nextExercise()
AudioEngine.shared.playFinished()
case .workoutComplete(let data):
DispatchQueue.main.async {
let model = try! JSONDecoder().decode(WatchFinishWorkoutModel.self, from: data)
self.healthKitUUID = model.healthKitUUID
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()
}
case .restartExercise:
restartExercise()
case .previousExercise:
previousExercise()
case .stopWorkout:
completeWorkout()
case .pauseWorkout:
pauseWorkout()
}
} catch {
watchBridgeLogger.error("Rejected WatchActions payload: \(error.localizedDescription, privacy: .public)")
}
}
@@ -85,25 +124,30 @@ extension BridgeModule: WCSessionDelegate {
func session(_ session: WCSession,
activationDidCompleteWith activationState: WCSessionActivationState,
error: Error?) {
switch activationState {
case .notActivated:
print("notActivated")
case .inactive:
print("inactive")
case .activated:
print("activated")
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
print(error.debugDescription)
})
}
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:
print("default")
@unknown default:
watchBridgeLogger.error("Unknown WCSession activation state")
}
}
}
#if os(iOS)
@@ -116,7 +160,13 @@ extension BridgeModule: WCSessionDelegate {
}
#endif
func send(_ data: Data) {
guard WCSession.isSupported() else {
return
}
guard session.activationState == .activated else {
enqueueWatchMessage(data)
session.activate()
return
}
#if os(iOS)
@@ -128,8 +178,15 @@ extension BridgeModule: WCSessionDelegate {
return
}
#endif
session.sendMessageData(data, replyHandler: nil) { error in
print("Cannot send message: \(String(describing: error))")
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])
}
}
}