LiveActivityManager.swift 18 KB

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