Files
Ascently/ios/OpenClimb/ViewModels/LiveActivityManager.swift

276 lines
10 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<SessionActivityAttributes>?
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<SessionActivityAttributes>.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<SessionActivityAttributes>.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<SessionActivityAttributes>.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<SessionActivityAttributes>.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<SessionActivityAttributes>.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<SessionActivityAttributes>.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<SessionActivityAttributes>.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<SessionActivityAttributes>.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<SessionActivityAttributes>.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)
}
}
}
}