All checks were successful
OpenClimb Docker Deploy / build-and-push (push) Successful in 2m28s
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(
|
|
"WARNING: 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("ERROR: 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("WARNING: 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(
|
|
"WARNING: 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)
|
|
}
|
|
}
|
|
}
|
|
}
|