import ActivityKit import Foundation extension Notification.Name { static let liveActivityDismissed = Notification.Name("liveActivityDismissed") } @MainActor final class LiveActivityManager { static let shared = LiveActivityManager() private init() {} private var currentActivity: Activity? private var healthCheckTimer: Timer? private var lastHealthCheck: Date = Date() deinit { healthCheckTimer?.invalidate() } /// Check if there's an active session and restart Live Activity if needed func restartLiveActivityIfNeeded(activeSession: ClimbSession?, gymName: String?) async { // If we have an active session but no Live Activity, restart it guard let activeSession = activeSession, let gymName = gymName, activeSession.status == .active else { return } // Check if we have a tracked Live Activity that's still actually running if let currentActivity = currentActivity { let activities = Activity.activities let isStillActive = activities.contains { $0.id == currentActivity.id } if isStillActive { print("ℹ️ Live Activity still running: \(currentActivity.id)") return } else { print( "⚠️ Tracked Live Activity \(currentActivity.id) was dismissed, clearing reference" ) self.currentActivity = nil } } // Check if there are ANY active Live Activities for this session let existingActivities = Activity.activities if let existingActivity = existingActivities.first { print("ℹ️ Found existing Live Activity: \(existingActivity.id), using it") self.currentActivity = existingActivity return } print("🔄 No Live Activity found, restarting for existing session") await startLiveActivity(for: activeSession, gymName: gymName) } /// Call this when a ClimbSession starts to begin a Live Activity func startLiveActivity(for session: ClimbSession, gymName: String) async { print("🔴 Starting Live Activity for gym: \(gymName)") await endLiveActivity() // Start health checks once we have an active session startHealthChecks() // Calculate elapsed time if session already started let startTime = session.startTime ?? session.date let elapsed = Date().timeIntervalSince(startTime) let attributes = SessionActivityAttributes( gymName: gymName, startTime: startTime) let initialContentState = SessionActivityAttributes.ContentState( elapsed: elapsed, totalAttempts: 0, completedProblems: 0 ) do { let activity = try Activity.request( attributes: attributes, content: .init(state: initialContentState, staleDate: nil), pushType: nil ) self.currentActivity = activity print("✅ Live Activity started successfully: \(activity.id)") } catch { print("❌ Failed to start live activity: \(error)") print("Error details: \(error.localizedDescription)") // Check specific error types if error.localizedDescription.contains("authorization") { print("Authorization error - check Live Activity permissions in Settings") } else if error.localizedDescription.contains("content") { print("Content error - check ActivityAttributes structure") } else if error.localizedDescription.contains("frequencyLimited") { print("Frequency limited - too many Live Activities started recently") } } } /// Call this to update the Live Activity with new session progress func updateLiveActivity(elapsed: TimeInterval, totalAttempts: Int, completedProblems: Int) async { guard let currentActivity = currentActivity else { print("⚠️ No current activity to update") return } // Verify the activity is still valid before updating let activities = Activity.activities let isStillActive = activities.contains { $0.id == currentActivity.id } if !isStillActive { print( "⚠️ Tracked Live Activity \(currentActivity.id) is no longer active, clearing reference" ) self.currentActivity = nil return } print( "🔄 Updating Live Activity - Attempts: \(totalAttempts), Completed: \(completedProblems)" ) let updatedContentState = SessionActivityAttributes.ContentState( elapsed: elapsed, totalAttempts: totalAttempts, completedProblems: completedProblems ) await currentActivity.update(.init(state: updatedContentState, staleDate: nil)) print("✅ Live Activity updated successfully") } /// Call this when a ClimbSession ends to end the Live Activity func endLiveActivity() async { // Stop health checks first stopHealthChecks() // First end the tracked activity if it exists if let currentActivity { print("🔴 Ending tracked Live Activity: \(currentActivity.id)") await currentActivity.end(nil, dismissalPolicy: .immediate) self.currentActivity = nil print("✅ Tracked Live Activity ended successfully") } // Force end ALL active activities of our type to ensure cleanup print("🔍 Checking for any remaining active activities...") let activities = Activity.activities if activities.isEmpty { print("ℹ️ No additional activities found") } else { print("🔴 Found \(activities.count) additional active activities, ending them...") for activity in activities { print("🔴 Force ending activity: \(activity.id)") await activity.end(nil, dismissalPolicy: .immediate) } print("✅ All Live Activities ended successfully") } } /// Check if Live Activities are available and authorized func checkLiveActivityAvailability() -> String { let authorizationInfo = ActivityAuthorizationInfo() let status = authorizationInfo.areActivitiesEnabled let allActivities = Activity.activities let message = """ Live Activity Status: • Enabled: \(status) • Authorization: \(authorizationInfo.areActivitiesEnabled ? "Granted" : "Denied/Unknown") • Tracked Activity: \(currentActivity?.id.description ?? "None") • All Active Activities: \(allActivities.count) """ print(message) return message } /// Force check and cleanup dismissed Live Activities func cleanupDismissedActivities() async { let activities = Activity.activities if let currentActivity = currentActivity { let isStillActive = activities.contains { $0.id == currentActivity.id } if !isStillActive { print("🧹 Cleaning up dismissed Live Activity: \(currentActivity.id)") self.currentActivity = nil } } } /// Start periodic health checks for Live Activity func startHealthChecks() { stopHealthChecks() // Stop any existing timer print("🩺 Starting Live Activity health checks") healthCheckTimer = Timer.scheduledTimer(withTimeInterval: 30.0, repeats: true) { [weak self] _ in Task { @MainActor in await self?.performHealthCheck() } } } /// Stop periodic health checks func stopHealthChecks() { healthCheckTimer?.invalidate() healthCheckTimer = nil print("🛑 Stopped Live Activity health checks") } /// Perform a health check on the current Live Activity private func performHealthCheck() async { guard let currentActivity = currentActivity else { return } let now = Date() let timeSinceLastCheck = now.timeIntervalSince(lastHealthCheck) // Only perform health check if it's been at least 25 seconds guard timeSinceLastCheck >= 25 else { return } print("🩺 Performing Live Activity health check") lastHealthCheck = now let activities = Activity.activities let isStillActive = activities.contains { $0.id == currentActivity.id } if !isStillActive { print("💔 Health check failed - Live Activity was dismissed") self.currentActivity = nil // Notify that we need to restart NotificationCenter.default.post( name: .liveActivityDismissed, object: nil ) } else { print("✅ Live Activity health check passed") } } /// Get the current activity status for debugging func getCurrentActivityStatus() -> String { let activities = Activity.activities let trackedStatus = currentActivity != nil ? "Tracked" : "None" let actualCount = activities.count return "Status: \(trackedStatus) | Active Count: \(actualCount)" } /// Start periodic updates for Live Activity func startPeriodicUpdates(for session: ClimbSession, totalAttempts: Int, completedProblems: Int) { guard currentActivity != nil else { return } Task { while currentActivity != nil { let elapsed = Date().timeIntervalSince(session.startTime ?? session.date) await updateLiveActivity( elapsed: elapsed, totalAttempts: totalAttempts, completedProblems: completedProblems ) // Wait 30 seconds before next update try? await Task.sleep(nanoseconds: 30_000_000_000) } } } }