276 lines
10 KiB
Swift
276 lines
10 KiB
Swift
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)
|
||
}
|
||
}
|
||
}
|
||
}
|