LiveActivityBridge.swift 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import ActivityKit
  2. import CoreData
  3. import Foundation
  4. import Swinject
  5. import UIKit
  6. @available(iOS 16.2, *) private struct ActiveActivity {
  7. let activity: Activity<LiveActivityAttributes>
  8. let startDate: Date
  9. func needsRecreation() -> Bool {
  10. switch activity.activityState {
  11. case .dismissed,
  12. .ended,
  13. .stale:
  14. return true
  15. case .active: break
  16. @unknown default:
  17. return true
  18. }
  19. return -startDate.timeIntervalSinceNow >
  20. TimeInterval(60 * 60)
  21. }
  22. }
  23. @available(iOS 16.2, *) final class LiveActivityBridge: Injectable, ObservableObject
  24. {
  25. @Injected() private var settingsManager: SettingsManager!
  26. @Injected() private var broadcaster: Broadcaster!
  27. @Injected() private var storage: FileStorage!
  28. private let activityAuthorizationInfo = ActivityAuthorizationInfo()
  29. @Published private(set) var systemEnabled: Bool
  30. private var settings: FreeAPSSettings {
  31. settingsManager.settings
  32. }
  33. var determination: DeterminationData?
  34. private var currentActivity: ActiveActivity?
  35. private var latestGlucose: GlucoseData?
  36. var glucoseFromPersistence: [GlucoseData]?
  37. var isOverridesActive: OverrideData?
  38. let context = CoreDataStack.shared.newTaskContext()
  39. init(resolver: Resolver) {
  40. systemEnabled = activityAuthorizationInfo.areActivitiesEnabled
  41. injectServices(resolver)
  42. setupNotifications()
  43. monitorForLiveActivityAuthorizationChanges()
  44. setupGlucoseArray()
  45. }
  46. private func setupNotifications() {
  47. let notificationCenter = Foundation.NotificationCenter.default
  48. notificationCenter.addObserver(self, selector: #selector(handleBatchInsert), name: .didPerformBatchInsert, object: nil)
  49. notificationCenter.addObserver(self, selector: #selector(cobOrIobDidUpdate), name: .didUpdateCobIob, object: nil)
  50. notificationCenter
  51. .addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in
  52. self?.forceActivityUpdate()
  53. }
  54. notificationCenter
  55. .addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in
  56. self?.forceActivityUpdate()
  57. }
  58. }
  59. @objc private func handleBatchInsert() {
  60. setupGlucoseArray()
  61. }
  62. @objc private func cobOrIobDidUpdate() {
  63. Task {
  64. await fetchAndMapDetermination()
  65. if let determination = determination {
  66. await self.pushDeterminationUpdate(determination)
  67. }
  68. }
  69. }
  70. private func setupGlucoseArray() {
  71. Task {
  72. // Fetch and map glucose to GlucoseData struct
  73. await fetchAndMapGlucose()
  74. // Fetch and map Determination to DeterminationData struct
  75. await fetchAndMapDetermination()
  76. // Fetch and map Override to OverrideData struct
  77. /// shows if there is an active Override
  78. await fetchAndMapOverride()
  79. // Push the update to the Live Activity
  80. glucoseDidUpdate(glucoseFromPersistence ?? [])
  81. }
  82. }
  83. private func monitorForLiveActivityAuthorizationChanges() {
  84. Task {
  85. for await activityState in activityAuthorizationInfo.activityEnablementUpdates {
  86. if activityState != systemEnabled {
  87. await MainActor.run {
  88. systemEnabled = activityState
  89. }
  90. }
  91. }
  92. }
  93. }
  94. /// creates and tries to present a new activity update from the current GlucoseStorage values if live activities are enabled in settings
  95. /// Ends existing live activities if live activities are not enabled in settings
  96. private func forceActivityUpdate() {
  97. // just before app resigns active, show a new activity
  98. // only do this if there is no current activity or the current activity is older than 1h
  99. if settings.useLiveActivity {
  100. if currentActivity?.needsRecreation() ?? true
  101. {
  102. glucoseDidUpdate(glucoseFromPersistence ?? [])
  103. }
  104. } else {
  105. Task {
  106. await self.endActivity()
  107. }
  108. }
  109. }
  110. /// attempts to present this live activity state, creating a new activity if none exists yet
  111. @MainActor private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
  112. // // End all activities that are not the current one
  113. for unknownActivity in Activity<LiveActivityAttributes>.activities
  114. .filter({ self.currentActivity?.activity.id != $0.id })
  115. {
  116. await unknownActivity.end(nil, dismissalPolicy: .immediate)
  117. }
  118. if let currentActivity = currentActivity {
  119. if currentActivity.needsRecreation(), UIApplication.shared.applicationState == .active {
  120. await endActivity()
  121. await pushUpdate(state)
  122. } else {
  123. let content = ActivityContent(
  124. state: state,
  125. staleDate: min(state.date, Date.now).addingTimeInterval(360) // 6 minutes in seconds
  126. )
  127. await currentActivity.activity.update(content)
  128. }
  129. } else {
  130. do {
  131. // always push a non-stale content as the first update
  132. // pushing a stale content as the frst content results in the activity not being shown at all
  133. // apparently this initial state is also what is shown after the live activity expires (after 8h)
  134. let expired = ActivityContent(
  135. state: LiveActivityAttributes.ContentState(
  136. bg: "--",
  137. direction: nil,
  138. change: "--",
  139. date: Date.now,
  140. detailedViewState: nil,
  141. isInitialState: true
  142. ),
  143. staleDate: Date.now.addingTimeInterval(60)
  144. )
  145. // Request a new activity
  146. let activity = try Activity.request(
  147. attributes: LiveActivityAttributes(startDate: Date.now),
  148. content: expired,
  149. pushType: nil
  150. )
  151. currentActivity = ActiveActivity(activity: activity, startDate: Date.now)
  152. // then show the actual content
  153. await pushUpdate(state)
  154. } catch {
  155. print("Activity creation error: \(error)")
  156. }
  157. }
  158. }
  159. @MainActor private func pushDeterminationUpdate(_ determination: DeterminationData) async {
  160. guard let latestGlucose = latestGlucose else { return }
  161. let content = LiveActivityAttributes.ContentState(
  162. new: latestGlucose,
  163. prev: latestGlucose,
  164. units: settings.units,
  165. chart: glucoseFromPersistence ?? [],
  166. settings: settings,
  167. determination: determination,
  168. override: isOverridesActive
  169. )
  170. if let content = content {
  171. await pushUpdate(content)
  172. }
  173. }
  174. /// ends all live activities immediateny
  175. private func endActivity() async {
  176. if let currentActivity {
  177. await currentActivity.activity.end(nil, dismissalPolicy: .immediate)
  178. self.currentActivity = nil
  179. }
  180. // end any other activities
  181. for unknownActivity in Activity<LiveActivityAttributes>.activities {
  182. await unknownActivity.end(nil, dismissalPolicy: .immediate)
  183. }
  184. }
  185. }
  186. @available(iOS 16.2, *)
  187. extension LiveActivityBridge {
  188. func glucoseDidUpdate(_ glucose: [GlucoseData]) {
  189. guard settings.useLiveActivity else {
  190. if currentActivity != nil {
  191. Task {
  192. await self.endActivity()
  193. }
  194. }
  195. return
  196. }
  197. // backfill latest glucose if contained in this update
  198. if glucose.count > 1 {
  199. latestGlucose = glucose.dropFirst().first
  200. }
  201. defer {
  202. self.latestGlucose = glucose.first
  203. }
  204. guard let bg = glucose.first else {
  205. return
  206. }
  207. if let determination = determination {
  208. let content = LiveActivityAttributes.ContentState(
  209. new: bg,
  210. prev: latestGlucose,
  211. units: settings.units,
  212. chart: glucose,
  213. settings: settings,
  214. determination: determination,
  215. override: isOverridesActive
  216. )
  217. if let content = content {
  218. Task {
  219. await self.pushUpdate(content)
  220. }
  221. }
  222. }
  223. }
  224. }