Przeglądaj źródła

Merge pull request #688 from 10nas/live_activity_cleanup_and_persistency_improvements

marv-out 10 miesięcy temu
rodzic
commit
2355f7b0e3

+ 1 - 1
Trio/Sources/Services/LiveActivity/LiveActivityAttributes+Helper.swift

@@ -55,7 +55,7 @@ extension LiveActivityAttributes.ContentState {
         return formatter.string(from: deltaAsDecimal as NSNumber) ?? "--"
     }
 
-    init?(
+    init(
         new bg: GlucoseData,
         prev _: GlucoseData?,
         units: GlucoseUnits,

+ 101 - 193
Trio/Sources/Services/LiveActivity/LiveActivityManager.swift

@@ -7,7 +7,6 @@ import UIKit
 
 @available(iOS 16.2, *) private struct ActiveActivity {
     let activity: Activity<LiveActivityAttributes>
-    let startDate: Date
 
     /// Determines if the current activity needs to be recreated.
     ///
@@ -23,10 +22,21 @@ import UIKit
         @unknown default:
             return true
         }
-        return -startDate.timeIntervalSinceNow > TimeInterval(60 * 60)
+        return -activity.attributes.startDate.timeIntervalSinceNow > TimeInterval(60 * 60)
     }
 }
 
+final class LiveActivityData: ObservableObject {
+    /// Determination data used to update live activity state.
+    @Published var determination: DeterminationData?
+    /// Array of glucose readings fetched from persistent storage.
+    @Published var glucoseFromPersistence: [GlucoseData]?
+    /// The current override data (if any).
+    @Published var override: OverrideData?
+    /// The widget items displayed within the live activity.
+    @Published var widgetItems: [LiveActivityAttributes.LiveActivityItem]?
+}
+
 /// A service managing live activity updates and state management.
 ///
 /// This class handles the creation, update, and termination of live activities based on various data sources
@@ -51,18 +61,10 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
         settingsManager.settings
     }
 
-    /// Determination data used to update live activity state.
-    var determination: DeterminationData?
     /// The current active live activity.
     private var currentActivity: ActiveActivity?
-    /// The most recent glucose reading.
-    private var latestGlucose: GlucoseData?
-    /// Array of glucose readings fetched from persistent storage.
-    var glucoseFromPersistence: [GlucoseData]?
-    /// The current override data (if any).
-    var override: OverrideData?
-    /// The widget items displayed within the live activity.
-    var widgetItems: [LiveActivityAttributes.LiveActivityItem]?
+
+    private var data = LiveActivityData()
 
     /// A Core Data task context.
     let context = CoreDataStack.shared.newTaskContext()
@@ -85,12 +87,16 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
         systemEnabled = activityAuthorizationInfo.areActivitiesEnabled
         injectServices(resolver)
         setupNotifications()
-        registerSubscribers()
         registerHandler()
         monitorForLiveActivityAuthorizationChanges()
-        setupGlucoseArray()
-        setupDetermination()
         broadcaster.register(SettingsObserver.self, observer: self)
+        data.objectWillChange.sink { [weak self] in
+            Task { @MainActor in
+                // by the time this runs, the object change is done, so we see the new data here
+                await self?.pushCurrentContent()
+            }
+        }.store(in: &subscriptions)
+        loadInitialData()
     }
 
     /// Sets up application notifications that trigger live activity updates when the app state changes.
@@ -99,18 +105,18 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
         notificationCenter
             .addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in
                 Task { @MainActor in
-                    self?.forceActivityUpdate()
+                    await self?.pushCurrentContent()
                 }
             }
         notificationCenter
             .addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in
                 Task { @MainActor in
-                    self?.forceActivityUpdate()
+                    await self?.pushCurrentContent()
                 }
             }
         notificationCenter.addObserver(
             self,
-            selector: #selector(handleLiveActivityOrderChange),
+            selector: #selector(loadWidgetItems),
             name: .liveActivityOrderDidChange,
             object: nil
         )
@@ -121,149 +127,75 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
     /// This method triggers an update to the live activity content state based on the new settings.
     /// - Parameter _: The updated `TrioSettings`.
     func settingsDidChange(_: TrioSettings) {
-        Task {
-            await updateContentState(determination)
+        Task { @MainActor in
+            await self.pushCurrentContent()
         }
     }
 
     /// Registers handlers for Core Data changes related to overrides, glucose readings, and determinations.
     private func registerHandler() {
         coreDataPublisher?.filteredByEntityName("OverrideStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            self.overridesDidUpdate()
+            Task { await self?.loadOverrides() }
         }.store(in: &subscriptions)
 
         coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
-            guard let self = self else { return }
-            self.setupGlucoseArray()
+            Task { await self?.loadGlucose() }
         }.store(in: &subscriptions)
 
         coreDataPublisher?.filteredByEntityName("OrefDetermination")
             .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .utility))
             .sink { [weak self] _ in
-                guard let self = self else { return }
-                self.cobOrIobDidUpdate()
+                Task { await self?.loadDetermination() }
             }.store(in: &subscriptions)
     }
 
-    /// Registers subscribers for updates from the glucose storage.
-    private func registerSubscribers() {
-        glucoseStorage.updatePublisher
-            .receive(on: queue)
-            .sink { [weak self] _ in
-                guard let self = self else { return }
-                self.setupGlucoseArray()
-            }
-            .store(in: &subscriptions)
-    }
-
     /// Fetches and maps new determination data and updates the live activity content state.
-    private func cobOrIobDidUpdate() {
-        Task { @MainActor in
-            do {
-                self.determination = try await fetchAndMapDetermination()
-                if let determination = determination {
-                    await self.updateContentState(determination)
-                }
-            } catch {
-                debug(
-                    .default,
-                    "\(DebuggingIdentifiers.failed) failed to fetch and map determination: \(error)"
-                )
-            }
+    private func loadDetermination() async {
+        do {
+            data.determination = try await fetchAndMapDetermination()
+        } catch {
+            debug(
+                .default,
+                "[LiveActivityManager] \(DebuggingIdentifiers.failed) failed to fetch and map determination: \(error)"
+            )
         }
     }
 
     /// Fetches and maps override data and updates the live activity content state.
-    private func overridesDidUpdate() {
-        Task { @MainActor in
-            do {
-                self.override = try await fetchAndMapOverride()
-                if let determination = determination {
-                    await self.updateContentState(determination)
-                }
-            } catch {
-                debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch and map override: \(error)")
-            }
+    private func loadOverrides() async {
+        do {
+            data.override = try await fetchAndMapOverride()
+        } catch {
+            debug(.default, "[LiveActivityManager] \(DebuggingIdentifiers.failed) failed to fetch and map override: \(error)")
         }
     }
 
     /// Handles changes to the live activity order.
     ///
     /// Loads widget items from user defaults and triggers an update to the live activity order.
-    @objc private func handleLiveActivityOrderChange() {
-        Task {
-            self.widgetItems = UserDefaults.standard.loadLiveActivityOrderFromUserDefaults() ?? LiveActivityAttributes
-                .LiveActivityItem.defaultItems
-            await self.updateLiveActivityOrder()
-        }
-    }
-
-    /// Updates the live activity content state based on new determination or override data.
-    ///
-    /// - Parameter update: An object representing new `DeterminationData` or `OverrideData`.
-    @MainActor private func updateContentState<T>(_ update: T) async {
-        guard let latestGlucose = latestGlucose else {
-            return
-        }
-        var content: LiveActivityAttributes.ContentState?
-
-        widgetItems = UserDefaults.standard.loadLiveActivityOrderFromUserDefaults() ?? LiveActivityAttributes
+    @objc private func loadWidgetItems() {
+        data.widgetItems = UserDefaults.standard.loadLiveActivityOrderFromUserDefaults() ?? LiveActivityAttributes
             .LiveActivityItem.defaultItems
-
-        if let determination = update as? DeterminationData {
-            content = LiveActivityAttributes.ContentState(
-                new: latestGlucose,
-                prev: latestGlucose,
-                units: settings.units,
-                chart: glucoseFromPersistence ?? [],
-                settings: settings,
-                determination: determination,
-                override: override,
-                widgetItems: widgetItems
-            )
-        } else if let override = update as? OverrideData {
-            content = LiveActivityAttributes.ContentState(
-                new: latestGlucose,
-                prev: latestGlucose,
-                units: settings.units,
-                chart: glucoseFromPersistence ?? [],
-                settings: settings,
-                determination: determination,
-                override: override,
-                widgetItems: widgetItems
-            )
-        }
-
-        if let content = content {
-            await pushUpdate(content)
-        }
-    }
-
-    /// Triggers an update of the live activity order.
-    ///
-    /// This method refreshes the activity's content state to reflect any changes in the widget order.
-    @MainActor private func updateLiveActivityOrder() async {
-        Task {
-            await updateContentState(determination)
-        }
     }
 
     /// Sets up the array of glucose data from persistent storage and triggers an update to the live activity.
-    private func setupGlucoseArray() {
-        Task { @MainActor in
-            do {
-                self.glucoseFromPersistence = try await fetchAndMapGlucose()
-                glucoseDidUpdate(glucoseFromPersistence ?? [])
-            } catch {
-                debug(.default, "\(DebuggingIdentifiers.failed) failed to fetch glucose with error: \(error)")
-            }
+    private func loadGlucose() async {
+        do {
+            data.glucoseFromPersistence = try await fetchAndMapGlucose()
+        } catch {
+            debug(
+                .default,
+                "[LiveActivityManager] \(DebuggingIdentifiers.failed) failed to fetch glucose with error: \(error)"
+            )
         }
     }
 
-    private func setupDetermination() {
-        Task { @MainActor in
-            self.determination = try await fetchAndMapDetermination()
+    private func loadInitialData() {
+        Task {
+            await self.loadGlucose()
+            await self.loadOverrides()
+            await self.loadDetermination()
+            self.loadWidgetItems()
         }
     }
 
@@ -280,20 +212,6 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
         }
     }
 
-    /// Forces an update to the live activity.
-    ///
-    /// If live activities are enabled and the current activity requires recreation, this method triggers a new glucose update.
-    /// Otherwise, it ends the current live activity.
-    @MainActor private func forceActivityUpdate() {
-        if settings.useLiveActivity {
-            glucoseDidUpdate(glucoseFromPersistence ?? [])
-        } else {
-            Task {
-                await self.endActivity()
-            }
-        }
-    }
-
     /// Pushes an update to the live activity with the specified content state.
     ///
     /// If an existing activity requires recreation or is outdated, this method ends it and starts a new one.
@@ -301,6 +219,18 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
     ///
     /// - Parameter state: The new content state to push to the live activity.
     @MainActor private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
+        if currentActivity == nil {
+            // try to restore an existing activity
+            currentActivity = Activity<LiveActivityAttributes>.activities
+                .max { $0.attributes.startDate < $1.attributes.startDate }.map {
+                    ActiveActivity(activity: $0)
+                }
+
+            if let currentActivity {
+                debug(.default, "[LiveActivityManager] Restored live activity: \(currentActivity.activity.id)")
+            }
+        }
+
         // End all unknown activities except the current one
         for unknownActivity in Activity<LiveActivityAttributes>.activities
             .filter({ self.currentActivity?.activity.id != $0.id })
@@ -308,11 +238,8 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
             await unknownActivity.end(nil, dismissalPolicy: .immediate)
         }
 
-        // Defensive: capture the current activity at function start
-        let activityAtStart = currentActivity
-
-        if let currentActivity = activityAtStart {
-            if currentActivity.needsRecreation(), UIApplication.shared.applicationState == .active {
+        if let currentActivity {
+            if currentActivity.needsRecreation() && UIApplication.shared.applicationState == .active {
                 debug(.default, "[LiveActivityManager] Ending current activity for recreation: \(currentActivity.activity.id)")
                 await endActivity()
                 // After endActivity(), currentActivity is guaranteed to be nil
@@ -345,7 +272,7 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
                             date: Date.now,
                             highGlucose: settings.high,
                             lowGlucose: settings.low,
-                            target: determination?.target ?? 100 as Decimal,
+                            target: data.determination?.target ?? 100 as Decimal,
                             glucoseColorScheme: settings.glucoseColorScheme.rawValue,
                             detailedViewState: nil,
                             isInitialState: true
@@ -358,7 +285,7 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
                     content: expired,
                     pushType: nil
                 )
-                currentActivity = ActiveActivity(activity: activity, startDate: Date.now)
+                currentActivity = ActiveActivity(activity: activity)
                 debug(.default, "[LiveActivityManager] Created new activity: \(activity.id)")
 
                 // Update the newly created activity with actual data
@@ -367,11 +294,11 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
                     staleDate: Date.now.addingTimeInterval(5 * 60)
                 )
                 await activity.update(updateContent)
-                debug(.default, "[LiveActivityManager] Updated new activity with actual data")
+                debug(.default, "[LiveActivityManager] Set initial content for new activity: \(activity.id)")
             } catch {
                 debug(
                     .default,
-                    "\(#file): Error creating new activity: \(error)"
+                    "[LiveActivityManager]: Error creating new activity: \(error)"
                 )
                 // Reset currentActivity on error to allow retry on next update
                 currentActivity = nil
@@ -381,20 +308,20 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
 
     /// Ends the current live activity and ensures that all unknown activities are terminated.
     private func endActivity() async {
-        debug(.default, "Ending all live activities...")
+        debug(.default, "[LiveActivityManager] Ending all live activities...")
 
         if let currentActivity {
-            debug(.default, "Ending current activity: \(currentActivity.activity.id)")
+            debug(.default, "[LiveActivityManager] Ending current activity: \(currentActivity.activity.id)")
             await currentActivity.activity.end(nil, dismissalPolicy: .immediate)
             self.currentActivity = nil
         }
 
         for unknownActivity in Activity<LiveActivityAttributes>.activities {
-            debug(.default, "Ending unknown activity: \(unknownActivity.id)")
+            debug(.default, "[LiveActivityManager] Ending unknown activity: \(unknownActivity.id)")
             await unknownActivity.end(nil, dismissalPolicy: .immediate)
         }
 
-        debug(.default, "All live activities ended.")
+        debug(.default, "[LiveActivityManager] All live activities ended.")
     }
 
     /// Restarts the live activity from a Live Activity Intent.
@@ -407,64 +334,45 @@ final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver
         while (currentActivity != nil && currentActivity!.activity.activityState != .ended) || Activity<LiveActivityAttributes>
             .activities.contains(where: { $0.activityState != .ended })
         {
-            debug(.default, "Waiting for Live Activity to end...")
+            debug(.default, "[LiveActivityManager] Waiting for Live Activity to end...")
             try? await Task.sleep(nanoseconds: 200_000_000) // 0.2s sleep
         }
 
         // Add additional delay to ensure iOS has fully cleaned up the previous activity
-        debug(.default, "Waiting additional time for iOS to clean up...")
+        debug(.default, "[LiveActivityManager] Waiting additional time for iOS to clean up...")
         try? await Task.sleep(nanoseconds: 1_000_000_000) // 1s additional delay
 
-        forceActivityUpdate()
+        await pushCurrentContent()
 
-        debug(.default, "Restarted Live Activity from LiveActivityIntent (via iOS Shortcut)")
+        debug(.default, "[LiveActivityManager] Restarted Live Activity from LiveActivityIntent (via iOS Shortcut)")
     }
 }
 
 @available(iOS 16.2, *)
 extension LiveActivityManager {
-    /// Updates the live activity when new glucose data is available.
-    ///
-    /// This function adjusts the live activity content based on new glucose readings and triggers an update to the live activity.
-    /// - Parameter glucose: An array of `GlucoseData` objects.
-    @MainActor func glucoseDidUpdate(_ glucose: [GlucoseData]) {
-        guard settings.useLiveActivity else {
-            if currentActivity != nil {
-                Task {
-                    await self.endActivity()
-                }
-            }
+    @MainActor func pushCurrentContent() async {
+        guard let glucose = data.glucoseFromPersistence, let bg = glucose.first else {
+            debug(.default, "[LiveActivityManager] pushCurrentContent: no current glucose data available")
             return
         }
+        let prevGlucose = data.glucoseFromPersistence?.dropFirst().first
 
-        if glucose.count > 1 {
-            latestGlucose = glucose.dropFirst().first
-        }
-        defer {
-            self.latestGlucose = glucose.first
-        }
-
-        guard let bg = glucose.first else {
+        guard let determination = data.determination else {
+            debug(.default, "[LiveActivityManager] pushCurrentContent: no determination available")
             return
         }
 
-        if let determination = determination {
-            let content = LiveActivityAttributes.ContentState(
-                new: bg,
-                prev: latestGlucose,
-                units: settings.units,
-                chart: glucose,
-                settings: settings,
-                determination: determination,
-                override: override,
-                widgetItems: widgetItems
-            )
+        let content = LiveActivityAttributes.ContentState(
+            new: bg,
+            prev: prevGlucose,
+            units: settings.units,
+            chart: glucose,
+            settings: settings,
+            determination: determination,
+            override: data.override,
+            widgetItems: data.widgetItems
+        )
 
-            if let content = content {
-                Task {
-                    await self.pushUpdate(content)
-                }
-            }
-        }
+        await pushUpdate(content)
     }
 }