LiveActivityManager.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. import ActivityKit
  2. import Combine
  3. import CoreData
  4. import Foundation
  5. import Swinject
  6. import UIKit
  7. @available(iOS 16.2, *) private struct ActiveActivity {
  8. let activity: Activity<LiveActivityAttributes>
  9. /// Determines if the current activity needs to be recreated.
  10. ///
  11. /// - Returns: `true` if the activity is dismissed, ended, stale, or has been active for more than 60 minutes; otherwise,
  12. /// `false`.
  13. func needsRecreation() -> Bool {
  14. switch activity.activityState {
  15. case .dismissed,
  16. .ended,
  17. .stale:
  18. return true
  19. case .active:
  20. break
  21. @unknown default:
  22. return true
  23. }
  24. return -activity.attributes.startDate.timeIntervalSinceNow > TimeInterval(60 * 60)
  25. }
  26. }
  27. final class LiveActivityData: ObservableObject {
  28. /// Determination data used to update live activity state.
  29. @Published var determination: DeterminationData?
  30. /// The most recent IoB data
  31. @Published var iob: Decimal?
  32. /// Array of glucose readings fetched from persistent storage.
  33. @Published var glucoseFromPersistence: [GlucoseData]?
  34. /// The current override data (if any).
  35. @Published var override: OverrideData?
  36. /// The widget items displayed within the live activity.
  37. @Published var widgetItems: [LiveActivityAttributes.LiveActivityItem]?
  38. }
  39. /// A service managing live activity updates and state management.
  40. ///
  41. /// This class handles the creation, update, and termination of live activities based on various data sources
  42. /// (e.g. Core Data notifications, glucose updates, settings changes). It integrates with system notifications,
  43. /// dependency injection, and user defaults to ensure that the live activity reflects the current app state.
  44. ///
  45. /// Additionally, it supports a restart functionality (via `restartActivityFromLiveActivityIntent()`)
  46. /// via iOS shortcuts, similar to other iOS apps like xDrip4iOS or Sweet Dreams.
  47. @available(iOS 16.2, *) final class LiveActivityManager: Injectable, ObservableObject, SettingsObserver {
  48. @Injected() private var settingsManager: SettingsManager!
  49. @Injected() private var broadcaster: Broadcaster!
  50. @Injected() private var storage: FileStorage!
  51. @Injected() private var glucoseStorage: GlucoseStorage!
  52. @Injected() private var iobService: IOBService!
  53. private let activityAuthorizationInfo = ActivityAuthorizationInfo()
  54. /// Indicates whether system live activities are enabled.
  55. @Published private(set) var systemEnabled: Bool
  56. /// Returns the current Trio settings.
  57. private var settings: TrioSettings {
  58. settingsManager.settings
  59. }
  60. /// The current active live activity.
  61. private var currentActivity: ActiveActivity?
  62. private var data = LiveActivityData()
  63. /// A Core Data task context.
  64. let context = CoreDataStack.shared.newTaskContext()
  65. /// A dispatch queue for handling Core Data change notifications.
  66. private let queue = DispatchQueue(label: "LiveActivityBridge.queue", qos: .userInitiated)
  67. private var coreDataPublisher: AnyPublisher<Set<NSManagedObjectID>, Never>?
  68. private var subscriptions = Set<AnyCancellable>()
  69. /// Initializes a new instance of `LiveActivityBridge` and sets up observers, subscribers, and notifications.
  70. ///
  71. /// - Parameter resolver: The dependency injection resolver.
  72. init(resolver: Resolver) {
  73. coreDataPublisher =
  74. changedObjectsOnManagedObjectContextDidSavePublisher()
  75. .receive(on: queue)
  76. .share()
  77. .eraseToAnyPublisher()
  78. systemEnabled = activityAuthorizationInfo.areActivitiesEnabled
  79. injectServices(resolver)
  80. setupNotifications()
  81. registerHandler()
  82. monitorForLiveActivityAuthorizationChanges()
  83. broadcaster.register(SettingsObserver.self, observer: self)
  84. data.objectWillChange.sink { [weak self] in
  85. Task { @MainActor in
  86. // by the time this runs, the object change is done, so we see the new data here
  87. await self?.pushCurrentContent()
  88. }
  89. }.store(in: &subscriptions)
  90. loadInitialData()
  91. }
  92. /// Sets up application notifications that trigger live activity updates when the app state changes.
  93. private func setupNotifications() {
  94. let notificationCenter = Foundation.NotificationCenter.default
  95. notificationCenter
  96. .addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in
  97. Task { @MainActor in
  98. await self?.pushCurrentContent()
  99. }
  100. }
  101. notificationCenter
  102. .addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in
  103. Task { @MainActor in
  104. await self?.pushCurrentContent()
  105. }
  106. }
  107. notificationCenter.addObserver(
  108. self,
  109. selector: #selector(loadWidgetItems),
  110. name: .liveActivityOrderDidChange,
  111. object: nil
  112. )
  113. }
  114. /// Called when the app settings change.
  115. ///
  116. /// This method triggers an update to the live activity content state based on the new settings.
  117. /// - Parameter _: The updated `TrioSettings`.
  118. func settingsDidChange(_: TrioSettings) {
  119. Task { @MainActor in
  120. await self.pushCurrentContent()
  121. }
  122. }
  123. /// Registers handlers for Core Data changes related to overrides, glucose readings, and determinations.
  124. private func registerHandler() {
  125. coreDataPublisher?.filteredByEntityName("OverrideStored").sink { [weak self] _ in
  126. Task { await self?.loadOverrides() }
  127. }.store(in: &subscriptions)
  128. coreDataPublisher?.filteredByEntityName("GlucoseStored").sink { [weak self] _ in
  129. Task { await self?.loadGlucose() }
  130. }.store(in: &subscriptions)
  131. coreDataPublisher?.filteredByEntityName("OrefDetermination")
  132. .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .utility))
  133. .sink { [weak self] _ in
  134. Task { await self?.loadDetermination() }
  135. }.store(in: &subscriptions)
  136. iobService.iobPublisher
  137. .debounce(for: .seconds(2), scheduler: DispatchQueue.global(qos: .utility))
  138. .sink { [weak self] _ in
  139. self?.data.iob = self?.iobService.currentIOB
  140. }.store(in: &subscriptions)
  141. }
  142. /// Fetches and maps new determination data and updates the live activity content state.
  143. private func loadDetermination() async {
  144. do {
  145. data.determination = try await fetchAndMapDetermination()
  146. } catch {
  147. debug(
  148. .default,
  149. "[LiveActivityManager] \(DebuggingIdentifiers.failed) failed to fetch and map determination: \(error)"
  150. )
  151. }
  152. }
  153. /// Fetches and maps override data and updates the live activity content state.
  154. private func loadOverrides() async {
  155. do {
  156. data.override = try await fetchAndMapOverride()
  157. } catch {
  158. debug(.default, "[LiveActivityManager] \(DebuggingIdentifiers.failed) failed to fetch and map override: \(error)")
  159. }
  160. }
  161. /// Handles changes to the live activity order.
  162. ///
  163. /// Loads widget items from user defaults and triggers an update to the live activity order.
  164. @objc private func loadWidgetItems() {
  165. data.widgetItems = UserDefaults.standard.loadLiveActivityOrderFromUserDefaults() ?? LiveActivityAttributes
  166. .LiveActivityItem.defaultItems
  167. }
  168. /// Sets up the array of glucose data from persistent storage and triggers an update to the live activity.
  169. private func loadGlucose() async {
  170. do {
  171. data.glucoseFromPersistence = try await fetchAndMapGlucose()
  172. } catch {
  173. debug(
  174. .default,
  175. "[LiveActivityManager] \(DebuggingIdentifiers.failed) failed to fetch glucose with error: \(error)"
  176. )
  177. }
  178. }
  179. private func loadInitialData() {
  180. Task {
  181. await self.loadGlucose()
  182. await self.loadOverrides()
  183. await self.loadDetermination()
  184. self.loadWidgetItems()
  185. }
  186. }
  187. /// Monitors live activity authorization changes and updates the `systemEnabled` flag.
  188. private func monitorForLiveActivityAuthorizationChanges() {
  189. Task {
  190. for await activityState in activityAuthorizationInfo.activityEnablementUpdates {
  191. if activityState != systemEnabled {
  192. await MainActor.run {
  193. systemEnabled = activityState
  194. }
  195. }
  196. }
  197. }
  198. }
  199. /// Pushes an update to the live activity with the specified content state.
  200. ///
  201. /// If an existing activity requires recreation or is outdated, this method ends it and starts a new one.
  202. /// Otherwise, it updates the current live activity.
  203. ///
  204. /// - Parameter state: The new content state to push to the live activity.
  205. @MainActor private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
  206. if !settings.useLiveActivity || !systemEnabled {
  207. await endActivity()
  208. return
  209. }
  210. if currentActivity == nil {
  211. // try to restore an existing activity
  212. currentActivity = Activity<LiveActivityAttributes>.activities
  213. .max { $0.attributes.startDate < $1.attributes.startDate }.map {
  214. ActiveActivity(activity: $0)
  215. }
  216. if let currentActivity {
  217. debug(.default, "[LiveActivityManager] Restored live activity: \(currentActivity.activity.id)")
  218. }
  219. }
  220. // End all unknown activities except the current one
  221. for unknownActivity in Activity<LiveActivityAttributes>.activities
  222. .filter({ self.currentActivity?.activity.id != $0.id })
  223. {
  224. await unknownActivity.end(nil, dismissalPolicy: .immediate)
  225. }
  226. if let currentActivity {
  227. if currentActivity.needsRecreation(), UIApplication.shared.applicationState == .active {
  228. debug(.default, "[LiveActivityManager] Ending current activity for recreation: \(currentActivity.activity.id)")
  229. await endActivity()
  230. // After endActivity(), currentActivity is guaranteed to be nil
  231. // No recursive task, but explicitly restart
  232. debug(.default, "[LiveActivityManager] Re-pushing update after recreation.")
  233. await pushUpdate(state)
  234. } else {
  235. let content = ActivityContent(
  236. state: state,
  237. staleDate: min(state.date ?? Date.now, Date.now).addingTimeInterval(360)
  238. )
  239. // Before the update, check if currentActivity is still valid
  240. if let stillCurrent = self.currentActivity, stillCurrent.activity.id == currentActivity.activity.id {
  241. debug(.default, "[LiveActivityManager] Updating current activity: \(stillCurrent.activity.id)")
  242. await stillCurrent.activity.update(content)
  243. } else {
  244. debug(.default, "[LiveActivityManager] Skipped update: currentActivity changed during pushUpdate.")
  245. }
  246. }
  247. } else {
  248. // ... Activity is newly created ...
  249. do {
  250. let expired = ActivityContent(
  251. state: LiveActivityAttributes
  252. .ContentState(
  253. unit: settings.units.rawValue,
  254. bg: "--",
  255. direction: nil,
  256. change: "--",
  257. date: Date.now,
  258. highGlucose: settings.high,
  259. lowGlucose: settings.low,
  260. target: data.determination?.target ?? 100 as Decimal,
  261. glucoseColorScheme: settings.glucoseColorScheme.rawValue,
  262. useDetailedViewIOS: false,
  263. useDetailedViewWatchOS: false,
  264. detailedViewState: LiveActivityAttributes.ContentAdditionalState(
  265. chart: [],
  266. rotationDegrees: 0,
  267. cob: 0,
  268. iob: 0,
  269. tdd: 0,
  270. isOverrideActive: false,
  271. overrideName: "",
  272. overrideDate: Date.now,
  273. overrideDuration: 0,
  274. overrideTarget: 0,
  275. widgetItems: []
  276. ),
  277. isInitialState: true
  278. ),
  279. staleDate: Date.now.addingTimeInterval(60)
  280. )
  281. let activity = try Activity.request(
  282. attributes: LiveActivityAttributes(startDate: Date.now),
  283. content: expired,
  284. pushType: nil
  285. )
  286. currentActivity = ActiveActivity(activity: activity)
  287. debug(.default, "[LiveActivityManager] Created new activity: \(activity.id)")
  288. // Update the newly created activity with actual data
  289. let updateContent = ActivityContent(
  290. state: state,
  291. staleDate: Date.now.addingTimeInterval(5 * 60)
  292. )
  293. await activity.update(updateContent)
  294. debug(.default, "[LiveActivityManager] Set initial content for new activity: \(activity.id)")
  295. } catch {
  296. debug(
  297. .default,
  298. "[LiveActivityManager]: Error creating new activity: \(error)"
  299. )
  300. // Reset currentActivity on error to allow retry on next update
  301. currentActivity = nil
  302. }
  303. }
  304. }
  305. /// Ends the current live activity and ensures that all unknown activities are terminated.
  306. private func endActivity() async {
  307. debug(.default, "[LiveActivityManager] Ending all live activities...")
  308. if let currentActivity {
  309. debug(.default, "[LiveActivityManager] Ending current activity: \(currentActivity.activity.id)")
  310. await currentActivity.activity.end(nil, dismissalPolicy: .immediate)
  311. self.currentActivity = nil
  312. }
  313. for unknownActivity in Activity<LiveActivityAttributes>.activities {
  314. debug(.default, "[LiveActivityManager] Ending unknown activity: \(unknownActivity.id)")
  315. await unknownActivity.end(nil, dismissalPolicy: .immediate)
  316. }
  317. debug(.default, "[LiveActivityManager] All live activities ended.")
  318. }
  319. /// Restarts the live activity from a Live Activity Intent.
  320. ///
  321. /// This method mimics xdrip's `restartActivityFromLiveActivityIntent()` behavior by verifying that a valid content state
  322. /// exists,
  323. /// ending the current live activity, and starting a new one using the current state.
  324. @MainActor func restartActivityFromLiveActivityIntent() async {
  325. await endActivity()
  326. while (currentActivity != nil && currentActivity!.activity.activityState != .ended) || Activity<LiveActivityAttributes>
  327. .activities.contains(where: { $0.activityState != .ended })
  328. {
  329. debug(.default, "[LiveActivityManager] Waiting for Live Activity to end...")
  330. try? await Task.sleep(nanoseconds: 200_000_000) // 0.2s sleep
  331. }
  332. // Add additional delay to ensure iOS has fully cleaned up the previous activity
  333. debug(.default, "[LiveActivityManager] Waiting additional time for iOS to clean up...")
  334. try? await Task.sleep(nanoseconds: 1_000_000_000) // 1s additional delay
  335. await pushCurrentContent()
  336. debug(.default, "[LiveActivityManager] Restarted Live Activity from LiveActivityIntent (via iOS Shortcut)")
  337. }
  338. }
  339. @available(iOS 16.2, *) extension LiveActivityManager {
  340. @MainActor func pushCurrentContent() async {
  341. guard let glucose = data.glucoseFromPersistence, let bg = glucose.first else {
  342. debug(.default, "[LiveActivityManager] pushCurrentContent: no current glucose data available")
  343. return
  344. }
  345. let prevGlucose = data.glucoseFromPersistence?.dropFirst().first
  346. guard let determination = data.determination else {
  347. debug(.default, "[LiveActivityManager] pushCurrentContent: no determination available")
  348. return
  349. }
  350. let content = LiveActivityAttributes.ContentState(
  351. new: bg,
  352. prev: prevGlucose,
  353. units: settings.units,
  354. chart: glucose,
  355. settings: settings,
  356. determination: determination,
  357. iob: data.iob,
  358. override: data.override,
  359. widgetItems: data.widgetItems
  360. )
  361. await pushUpdate(content)
  362. }
  363. }