LiveActivityBridge.swift 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  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. let context = CoreDataStack.shared.newTaskContext()
  38. init(resolver: Resolver) {
  39. systemEnabled = activityAuthorizationInfo.areActivitiesEnabled
  40. injectServices(resolver)
  41. setupNotifications()
  42. monitorForLiveActivityAuthorizationChanges()
  43. setupGlucoseArray()
  44. }
  45. private func setupNotifications() {
  46. let notificationCenter = Foundation.NotificationCenter.default
  47. notificationCenter.addObserver(self, selector: #selector(handleBatchInsert), name: .didPerformBatchInsert, object: nil)
  48. notificationCenter
  49. .addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in
  50. self?.forceActivityUpdate()
  51. }
  52. notificationCenter
  53. .addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in
  54. self?.forceActivityUpdate()
  55. }
  56. }
  57. @objc private func handleBatchInsert() {
  58. setupGlucoseArray()
  59. }
  60. private func setupGlucoseArray() {
  61. Task {
  62. // Fetch and map glucose to GlucoseData struct
  63. await fetchAndMapGlucose()
  64. // Fetch and map Determination to DeterminationData struct
  65. await fetchAndMapDetermination()
  66. // Push the update to the Live Activity
  67. glucoseDidUpdate(glucoseFromPersistence ?? [])
  68. }
  69. }
  70. private func monitorForLiveActivityAuthorizationChanges() {
  71. Task {
  72. for await activityState in activityAuthorizationInfo.activityEnablementUpdates {
  73. if activityState != systemEnabled {
  74. await MainActor.run {
  75. systemEnabled = activityState
  76. }
  77. }
  78. }
  79. }
  80. }
  81. /// creates and tries to present a new activity update from the current GlucoseStorage values if live activities are enabled in settings
  82. /// Ends existing live activities if live activities are not enabled in settings
  83. private func forceActivityUpdate() {
  84. // just before app resigns active, show a new activity
  85. // only do this if there is no current activity or the current activity is older than 1h
  86. if settings.useLiveActivity {
  87. if currentActivity?.needsRecreation() ?? true
  88. {
  89. glucoseDidUpdate(glucoseFromPersistence ?? [])
  90. }
  91. } else {
  92. Task {
  93. await self.endActivity()
  94. }
  95. }
  96. }
  97. /// attempts to present this live activity state, creating a new activity if none exists yet
  98. @MainActor private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
  99. // // End all activities that are not the current one
  100. // for unknownActivity in Activity<LiveActivityAttributes>.activities.filter({ self.currentActivity?.activity.id != $0.id }) {
  101. // await unknownActivity.end(nil, dismissalPolicy: .immediate)
  102. // }
  103. if let currentActivity = currentActivity {
  104. if currentActivity.needsRecreation(), UIApplication.shared.applicationState == .active {
  105. await endActivity()
  106. await pushUpdate(state)
  107. } else {
  108. let content = ActivityContent(
  109. state: state,
  110. staleDate: min(state.date, Date.now).addingTimeInterval(360) // 6 minutes in seconds
  111. )
  112. await currentActivity.activity.update(content)
  113. }
  114. } else {
  115. do {
  116. // Create initial non-stale content
  117. let nonStaleContent = ActivityContent(
  118. state: LiveActivityAttributes.ContentState(
  119. bg: "--",
  120. direction: nil,
  121. change: "--",
  122. date: Date.now,
  123. chart: [],
  124. chartDate: [],
  125. rotationDegrees: 0,
  126. highGlucose: 180,
  127. lowGlucose: 70,
  128. cob: 0,
  129. iob: 0,
  130. lockScreenView: "Simple",
  131. unit: "--"
  132. ),
  133. staleDate: Date.now.addingTimeInterval(60)
  134. )
  135. // Request a new activity
  136. let activity = try Activity.request(
  137. attributes: LiveActivityAttributes(startDate: Date.now),
  138. content: nonStaleContent,
  139. pushType: nil
  140. )
  141. currentActivity = ActiveActivity(activity: activity, startDate: Date.now)
  142. // Push the actual content
  143. await pushUpdate(state)
  144. } catch {
  145. print("Activity creation error: \(error)")
  146. }
  147. }
  148. }
  149. /// ends all live activities immediateny
  150. private func endActivity() async {
  151. if let currentActivity {
  152. await currentActivity.activity.end(nil, dismissalPolicy: .immediate)
  153. self.currentActivity = nil
  154. }
  155. // end any other activities
  156. for unknownActivity in Activity<LiveActivityAttributes>.activities {
  157. await unknownActivity.end(nil, dismissalPolicy: .immediate)
  158. }
  159. }
  160. }
  161. @available(iOS 16.2, *)
  162. extension LiveActivityBridge {
  163. func glucoseDidUpdate(_ glucose: [GlucoseData]) {
  164. guard settings.useLiveActivity else {
  165. if currentActivity != nil {
  166. Task {
  167. await self.endActivity()
  168. }
  169. }
  170. return
  171. }
  172. // backfill latest glucose if contained in this update
  173. if glucose.count > 1 {
  174. latestGlucose = glucose.dropFirst().first
  175. }
  176. defer {
  177. self.latestGlucose = glucose.first
  178. }
  179. guard let bg = glucose.first else {
  180. return
  181. }
  182. if let determination = determination {
  183. let content = LiveActivityAttributes.ContentState(
  184. new: bg,
  185. prev: latestGlucose,
  186. mmol: settings.units == .mmolL,
  187. chart: glucose,
  188. settings: settings,
  189. determination: determination
  190. )
  191. if let content = content {
  192. Task {
  193. await self.pushUpdate(content)
  194. }
  195. }
  196. }
  197. }
  198. }