|
|
@@ -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 Sweat 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,6 +350,7 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /// Ends the current live activity and ensures that all unknown activities are terminated.
|
|
|
private func endActivity() async {
|
|
|
if let currentActivity {
|
|
|
await currentActivity.activity.end(nil, dismissalPolicy: .immediate)
|
|
|
@@ -307,10 +361,45 @@ final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
|
|
|
await unknownActivity.end(nil, dismissalPolicy: .immediate)
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /// 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")
|
|
|
+ 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()
|
|
|
+ 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 {
|