Przeglądaj źródła

Merge branch 'dev' of github.com:nightscout/Trio-dev into bgtask-defer

Deniz Cengiz 1 rok temu
rodzic
commit
a51bcfa7c4

+ 20 - 4
Trio.xcodeproj/project.pbxproj

@@ -262,7 +262,7 @@
 		6B1A8D202B14D91600E76752 /* LiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B1A8D1F2B14D91600E76752 /* LiveActivity.swift */; };
 		6B1A8D242B14D91700E76752 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6B1A8D232B14D91700E76752 /* Assets.xcassets */; };
 		6B1A8D282B14D91700E76752 /* LiveActivityExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6B1A8D172B14D91600E76752 /* LiveActivityExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
-		6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */; };
+		6B1A8D2E2B156EEF00E76752 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B1A8D2D2B156EEF00E76752 /* LiveActivityManager.swift */; };
 		6B1F539F9FF75646D1606066 /* SnoozeDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36A708CDB546692C2230B385 /* SnoozeDataFlow.swift */; };
 		6BCF84DD2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */; };
 		6BCF84DE2B16843A003AD46E /* LiveActitiyAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */; };
@@ -486,6 +486,8 @@
 		DD498F2B2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2C2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
 		DD498F2D2D692BEA00AAEA30 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 8A9134292D63D9A1007F8874 /* Localizable.xcstrings */; };
+		DD4C57A82D73ADEA001BFF2C /* RestartLiveActivityIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */; };
+		DD4C57AA2D73B3E2001BFF2C /* RestartLiveActivityIntentRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4C57A92D73B3D9001BFF2C /* RestartLiveActivityIntentRequest.swift */; };
 		DD4FFF332D458EE600B6CFF9 /* GarminWatchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */; };
 		DD5DC9F12CF3D97C00AB8703 /* AdjustmentsStateModel+Overrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F02CF3D96E00AB8703 /* AdjustmentsStateModel+Overrides.swift */; };
 		DD5DC9F32CF3D9DD00AB8703 /* AdjustmentsStateModel+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */; };
@@ -968,7 +970,7 @@
 		6B1A8D1F2B14D91600E76752 /* LiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivity.swift; sourceTree = "<group>"; };
 		6B1A8D232B14D91700E76752 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
 		6B1A8D252B14D91700E76752 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
-		6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityBridge.swift; sourceTree = "<group>"; };
+		6B1A8D2D2B156EEF00E76752 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = "<group>"; };
 		6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActitiyAttributes.swift; sourceTree = "<group>"; };
 		715120D12D3C2B84005D9FB6 /* GlucoseNotificationsOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseNotificationsOption.swift; sourceTree = "<group>"; };
 		71D44AAA2CA5F5EA0036EE9E /* AlertPermissionsChecker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertPermissionsChecker.swift; sourceTree = "<group>"; };
@@ -1191,6 +1193,8 @@
 		DD32CFA12CC824E1003686D6 /* TrioRemoteControl+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TrioRemoteControl+Helpers.swift"; sourceTree = "<group>"; };
 		DD3A3CE62D29C93F00AE478E /* Helper+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+Extensions.swift"; sourceTree = "<group>"; };
 		DD3A3CE82D29C97800AE478E /* Helper+ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Helper+ButtonStyles.swift"; sourceTree = "<group>"; };
+		DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartLiveActivityIntent.swift; sourceTree = "<group>"; };
+		DD4C57A92D73B3D9001BFF2C /* RestartLiveActivityIntentRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestartLiveActivityIntentRequest.swift; sourceTree = "<group>"; };
 		DD4FFF322D458EE600B6CFF9 /* GarminWatchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarminWatchState.swift; sourceTree = "<group>"; };
 		DD5DC9F02CF3D96E00AB8703 /* AdjustmentsStateModel+Overrides.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsStateModel+Overrides.swift"; sourceTree = "<group>"; };
 		DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsStateModel+TempTargets.swift"; sourceTree = "<group>"; };
@@ -2428,7 +2432,7 @@
 		6B1A8D2C2B156EC100E76752 /* LiveActivity */ = {
 			isa = PBXGroup;
 			children = (
-				6B1A8D2D2B156EEF00E76752 /* LiveActivityBridge.swift */,
+				6B1A8D2D2B156EEF00E76752 /* LiveActivityManager.swift */,
 				6BCF84DC2B16843A003AD46E /* LiveActitiyAttributes.swift */,
 				BDF34F922C10D0E100D51995 /* LiveActivityAttributes+Helper.swift */,
 				BDF34F882C10C65E00D51995 /* Data */,
@@ -2663,6 +2667,7 @@
 		CE7CA3422A064973004BE681 /* Shortcuts */ = {
 			isa = PBXGroup;
 			children = (
+				DD4C57A42D73ADDA001BFF2C /* LiveActivity */,
 				118DF7692C5ECBC60067FEB7 /* Override */,
 				110AEDE22C5193D100615CC9 /* Bolus */,
 				CE1856F32ADC4835007E39C7 /* Carbs */,
@@ -2931,6 +2936,15 @@
 			path = Helper;
 			sourceTree = "<group>";
 		};
+		DD4C57A42D73ADDA001BFF2C /* LiveActivity */ = {
+			isa = PBXGroup;
+			children = (
+				DD4C57A92D73B3D9001BFF2C /* RestartLiveActivityIntentRequest.swift */,
+				DD4C57A72D73ADEA001BFF2C /* RestartLiveActivityIntent.swift */,
+			);
+			path = LiveActivity;
+			sourceTree = "<group>";
+		};
 		DD5DC9EF2CF3D95400AB8703 /* AdjustmentsStateModel+Extensions */ = {
 			isa = PBXGroup;
 			children = (
@@ -3599,6 +3613,7 @@
 				38DF1786276A73D400B3528F /* TagCloudView.swift in Sources */,
 				38B4F3CD25E5031100E76A18 /* Broadcaster.swift in Sources */,
 				383420D925FFEB3F002D46C1 /* Popup.swift in Sources */,
+				DD4C57AA2D73B3E2001BFF2C /* RestartLiveActivityIntentRequest.swift in Sources */,
 				DD1745402C55BFC100211FAC /* AlgorithmAdvancedSettingsRootView.swift in Sources */,
 				58645BA52CA2D347008AFCE7 /* ForecastSetup.swift in Sources */,
 				110AEDEE2C51A0AE00615CC9 /* ShortcutsConfigStateModel.swift in Sources */,
@@ -3782,7 +3797,7 @@
 				DD940BAC2CA75889000830A5 /* DynamicGlucoseColor.swift in Sources */,
 				3883581C25EE79BB00E024B2 /* TextFieldWithToolBar.swift in Sources */,
 				58D08B302C8DEA7500AA37D3 /* ForecastView.swift in Sources */,
-				6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */,
+				6B1A8D2E2B156EEF00E76752 /* LiveActivityManager.swift in Sources */,
 				581516A92BCEEDF800BF67D7 /* NSPredicates.swift in Sources */,
 				DD4FFF332D458EE600B6CFF9 /* GarminWatchState.swift in Sources */,
 				DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */,
@@ -3939,6 +3954,7 @@
 				389ECE052601144100D86C4F /* ConcurrentMap.swift in Sources */,
 				110AEDEC2C51A0AE00615CC9 /* ShortcutsConfigDataFlow.swift in Sources */,
 				CE7CA3562A064973004BE681 /* StateIntentRequest.swift in Sources */,
+				DD4C57A82D73ADEA001BFF2C /* RestartLiveActivityIntent.swift in Sources */,
 				19E1F7EA29D082ED005C8D20 /* IconConfigProvider.swift in Sources */,
 				DD09D4822C5986F6003FEA5D /* CalendarEventSettingsRootView.swift in Sources */,
 				CE48C86428CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift in Sources */,

+ 1 - 1
Trio/Sources/Application/TrioApp.swift

@@ -58,7 +58,7 @@ import Swinject
         _ = resolver.resolve(PluginManager.self)!
         _ = resolver.resolve(AlertPermissionsChecker.self)!
         if #available(iOS 16.2, *) {
-            _ = resolver.resolve(LiveActivityBridge.self)!
+            _ = resolver.resolve(LiveActivityManager.self)!
         }
     }
 

+ 2 - 2
Trio/Sources/Assemblies/ServiceAssembly.swift

@@ -24,8 +24,8 @@ final class ServiceAssembly: Assembly {
         container.register(ContactImageManager.self) { r in BaseContactImageManager(resolver: r) }
         container.register(AlertPermissionsChecker.self) { r in AlertPermissionsChecker(resolver: r) }
         if #available(iOS 16.2, *) {
-            container.register(LiveActivityBridge.self) { r in
-                LiveActivityBridge(resolver: r)
+            container.register(LiveActivityManager.self) { r in
+                LiveActivityManager(resolver: r)
             }
         }
     }

+ 6 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -129578,6 +129578,9 @@
         }
       }
     },
+    "Restart Live Activity" : {
+
+    },
     "Result" : {
       "comment" : "For the  Bolus View pop-up",
       "localizations" : {
@@ -167977,6 +167980,9 @@
         }
       }
     },
+    "Trio Live Activity restarted successfully." : {
+
+    },
     "Trio Not Active" : {
       "comment" : "Trio Not Active",
       "localizations" : {

+ 1 - 1
Trio/Sources/Modules/LiveActivitySettings/View/LiveActivitySettingsRootView.swift

@@ -155,7 +155,7 @@ extension LiveActivitySettings {
                 }
             }
             .listSectionSpacing(sectionSpacing)
-            .onReceive(resolver.resolve(LiveActivityBridge.self)!.$systemEnabled, perform: {
+            .onReceive(resolver.resolve(LiveActivityManager.self)!.$systemEnabled, perform: {
                 self.systemLiveActivitySetting = $0
             })
             .sheet(isPresented: $shouldDisplayHint) {

+ 1 - 1
Trio/Sources/Services/LiveActivity/Data/DataManager.swift

@@ -3,7 +3,7 @@ import Foundation
 // Fetch Data for Glucose and Determination from Core Data and map them to the Structs in order to pass them thread safe to the glucoseDidUpdate/ pushUpdate function
 
 @available(iOS 16.2, *)
-extension LiveActivityBridge {
+extension LiveActivityManager {
     func fetchAndMapGlucose() async throws -> [GlucoseData] {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: GlucoseStored.self,

+ 111 - 3
Trio/Sources/Services/LiveActivity/LiveActivityBridge.swift

@@ -9,6 +9,9 @@ import UIKit
     let activity: Activity<LiveActivityAttributes>
     let startDate: Date
 
+    /// Determines if the current activity needs to be recreated.
+    ///
+    /// - Returns: `true` if the activity is dismissed, ended, stale, or has been active for more than 60 minutes; otherwise, `false`.
     func needsRecreation() -> Bool {
         switch activity.activityState {
         case .dismissed,
@@ -24,34 +27,54 @@ import UIKit
     }
 }
 
+/// 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
+/// (e.g. Core Data notifications, glucose updates, settings changes). It integrates with system notifications,
+/// dependency injection, and user defaults to ensure that the live activity reflects the current app state.
+///
+/// Additionally, it supports a restart functionality (via `restartActivityFromLiveActivityIntent()`)
+/// via iOS shortcuts, similar to other iOS apps like xDrip4iOS or Sweet Dreams.
 @available(iOS 16.2, *)
-final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
+final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver {
     @Injected() private var settingsManager: SettingsManager!
     @Injected() private var broadcaster: Broadcaster!
     @Injected() private var storage: FileStorage!
     @Injected() private var glucoseStorage: GlucoseStorage!
 
     private let activityAuthorizationInfo = ActivityAuthorizationInfo()
+    /// Indicates whether system live activities are enabled.
     @Published private(set) var systemEnabled: Bool
 
+    /// Returns the current Trio settings.
     private var settings: TrioSettings {
         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]?
 
+    /// A Core Data task context.
     let context = CoreDataStack.shared.newTaskContext()
 
-    // Queue for handling Core Data change notifications
+    /// A dispatch queue for handling Core Data change notifications.
     private let queue = DispatchQueue(label: "LiveActivityBridge.queue", qos: .userInitiated)
     private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
     private var subscriptions = Set<AnyCancellable>()
 
+    /// Initializes a new instance of `LiveActivityBridge` and sets up observers, subscribers, and notifications.
+    ///
+    /// - Parameter resolver: The dependency injection resolver.
     init(resolver: Resolver) {
         coreDataPublisher =
             changedObjectsOnManagedObjectContextDidSavePublisher()
@@ -69,6 +92,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         broadcaster.register(SettingsObserver.self, observer: self)
     }
 
+    /// Sets up application notifications that trigger live activity updates when the app state changes.
     private func setupNotifications() {
         let notificationCenter = Foundation.NotificationCenter.default
         notificationCenter
@@ -91,12 +115,17 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         )
     }
 
+    /// Called when the app settings change.
+    ///
+    /// 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)
         }
     }
 
+    /// 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 }
@@ -116,6 +145,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
             }.store(in: &subscriptions)
     }
 
+    /// Registers subscribers for updates from the glucose storage.
     private func registerSubscribers() {
         glucoseStorage.updatePublisher
             .receive(on: queue)
@@ -126,6 +156,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
             .store(in: &subscriptions)
     }
 
+    /// Fetches and maps new determination data and updates the live activity content state.
     private func cobOrIobDidUpdate() {
         Task { @MainActor in
             do {
@@ -142,6 +173,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// Fetches and maps override data and updates the live activity content state.
     private func overridesDidUpdate() {
         Task { @MainActor in
             do {
@@ -155,6 +187,9 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// 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
@@ -163,6 +198,9 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// 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
@@ -201,12 +239,16 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// 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 {
@@ -218,6 +260,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// Monitors live activity authorization changes and updates the `systemEnabled` flag.
     private func monitorForLiveActivityAuthorizationChanges() {
         Task {
             for await activityState in activityAuthorizationInfo.activityEnablementUpdates {
@@ -230,6 +273,10 @@ final class LiveActivityBridge: 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 {
             if currentActivity?.needsRecreation() ?? true {
@@ -242,6 +289,12 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
         }
     }
 
+    /// 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.
+    /// Otherwise, it updates the current live activity.
+    ///
+    /// - Parameter state: The new content state to push to the live activity.
     @MainActor private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
         for unknownActivity in Activity<LiveActivityAttributes>.activities
             .filter({ self.currentActivity?.activity.id != $0.id })
@@ -297,20 +350,75 @@ final class LiveActivityBridge: 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...")
+
         if let currentActivity {
+            debug(.default, "Ending current activity: \(currentActivity.activity.id)")
             await currentActivity.activity.end(nil, dismissalPolicy: .immediate)
             self.currentActivity = nil
         }
 
+        for activity in Activity<LiveActivityAttributes>.activities {
+            debug(.default, "Ending lingering activity: \(activity.id)")
+            await activity.end(nil, dismissalPolicy: .immediate)
+        }
+
         for unknownActivity in Activity<LiveActivityAttributes>.activities {
+            debug(.default, "Ending unknown activity: \(unknownActivity.id)")
             await unknownActivity.end(nil, dismissalPolicy: .immediate)
         }
+
+        debug(.default, "All live activities ended.")
+    }
+
+    /// Restarts the live activity from a Live Activity Intent.
+    ///
+    /// This method mimics xdrip’s `restartActivityFromLiveActivityIntent()` behavior by verifying that a valid content state exists,
+    /// ending the current live activity, and starting a new one using the current state.
+    @MainActor func restartActivityFromLiveActivityIntent() async {
+        guard let latestGlucose = latestGlucose,
+              let determination = determination
+        else {
+            debug(.default, "Cannot restart live activity because required persistent state is not available. Fetching data...")
+            return
+        }
+
+        guard let contentState = LiveActivityAttributes.ContentState(
+            new: latestGlucose,
+            prev: latestGlucose,
+            units: settings.units,
+            chart: glucoseFromPersistence ?? [],
+            settings: settings,
+            determination: determination,
+            override: override,
+            widgetItems: widgetItems
+        ) else {
+            debug(.default, "Cannot restart live activity because content state cannot be created")
+            return
+        }
+
+        await endActivity()
+
+        while (currentActivity != nil && currentActivity!.activity.activityState != .ended) || Activity<LiveActivityAttributes>
+            .activities.contains(where: { $0.activityState != .ended })
+        {
+            debug(.default, "Waiting for Live Activity to end...")
+            try? await Task.sleep(nanoseconds: 200_000_000) // 0.2s sleep
+        }
+
+        await pushUpdate(contentState)
+        debug(.default, "Restarted Live Activity from LiveActivityIntent (via iOS Shortcut)")
     }
 }
 
 @available(iOS 16.2, *)
-extension LiveActivityBridge {
+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 {

+ 9 - 0
Trio/Sources/Shortcuts/AppShortcuts.swift

@@ -66,5 +66,14 @@ struct AppShortcuts: AppShortcutsProvider {
             shortTitle: "Cancel Temp Target",
             systemImageName: "xmark.circle.fill"
         )
+        AppShortcut(
+            intent: RestartLiveActivityIntent(),
+            phrases: [
+                "Restart \(.applicationName) Live Activity",
+                "Restarts the Live Activity for \(.applicationName)"
+            ],
+            shortTitle: "Restart Live Activity",
+            systemImageName: "arrow.clockwise.circle.fill"
+        )
     }
 }

+ 1 - 0
Trio/Sources/Shortcuts/BaseIntentsRequest.swift

@@ -14,6 +14,7 @@ import Swinject
     @Injected() var glucoseStorage: GlucoseStorage!
     @Injected() var apsManager: APSManager!
     @Injected() var overrideStorage: OverrideStorage!
+    @Injected() var liveActivityManager: LiveActivityManager!
 
     let resolver: Resolver
 

+ 29 - 0
Trio/Sources/Shortcuts/LiveActivity/RestartLiveActivityIntent.swift

@@ -0,0 +1,29 @@
+import AppIntents
+import Foundation
+
+/// App Intent used to restart the live activity via Apple Shortcuts automation.
+/// When invoked, this intent instantiates a RestartLiveActivityIntentRequest, which has its
+/// dependencies injected via Swinject, and calls the restart functionality.
+@available(iOS 16.2, *) struct RestartLiveActivityIntent: LiveActivityIntent {
+    /// Title of the action in the Shortcuts app.
+    static var title = LocalizedStringResource("Restart Live Activity", table: "ShortcutsDetail")
+
+    /// Description of the action in the Shortcuts app.
+    static var description = IntentDescription(.init("Restarts Trio's Live Activity", table: "ShortcutsDetail"))
+
+    /// Performs the intent by triggering the live activity restart.
+    ///
+    /// This method creates an instance of RestartLiveActivityIntentRequest (which inherits from BaseIntentsRequest)
+    /// so that dependency injection provides the required services, then calls its restart functionality.
+    ///
+    /// - Returns: An intent result indicating success.
+    @MainActor func perform() async throws -> some ReturnsValue<String> {
+        let request = RestartLiveActivityIntentRequest()
+        do {
+            try await request.performRestart()
+        } catch {
+            debug(.default, "Error restarting Live Activity: \(error)")
+        }
+        return .result(value: String(localized: "Trio Live Activity restarted successfully."))
+    }
+}

+ 43 - 0
Trio/Sources/Shortcuts/LiveActivity/RestartLiveActivityIntentRequest.swift

@@ -0,0 +1,43 @@
+import AppIntents
+import Foundation
+import UIKit
+
+/// Request object that uses dependency injection to perform a live activity restart.
+/// This class inherits from BaseIntentsRequest so that its dependencies (including liveActivityManager)
+/// are automatically injected.
+@available(iOS 16.2, *) final class RestartLiveActivityIntentRequest: BaseIntentsRequest {
+    /// Triggers the live activity restart via the injected LiveActivityManager.
+    ///
+    /// - Throws: An error if the restart process fails.
+    /// - Returns: Void upon successful restart.
+    @MainActor func performRestart() async throws {
+        var backgroundTaskID: UIBackgroundTaskIdentifier = .invalid
+
+        // Start background task
+        backgroundTaskID = UIApplication.shared.beginBackgroundTask(withName: "Restart Live Activity") {
+            Task { @MainActor in
+                if backgroundTaskID != .invalid {
+                    UIApplication.shared.endBackgroundTask(backgroundTaskID)
+                    backgroundTaskID = .invalid
+                    debug(.default, "Background task expired and ended.")
+                }
+            }
+        }
+
+        guard backgroundTaskID != .invalid else {
+            debug(.default, "Failed to start background task.")
+            return
+        }
+
+        debug(.default, "Background task started: \(backgroundTaskID)")
+
+        await liveActivityManager.restartActivityFromLiveActivityIntent()
+
+        // Ensure background task ends properly
+        if backgroundTaskID != .invalid {
+            UIApplication.shared.endBackgroundTask(backgroundTaskID)
+            debug(.default, "Background task ended successfully.")
+            backgroundTaskID = .invalid
+        }
+    }
+}