LiveActivityBridge.swift 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  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
  101. .filter({ self.currentActivity?.activity.id != $0.id })
  102. {
  103. await unknownActivity.end(nil, dismissalPolicy: .immediate)
  104. }
  105. if let currentActivity = currentActivity {
  106. if currentActivity.needsRecreation(), UIApplication.shared.applicationState == .active {
  107. await endActivity()
  108. await pushUpdate(state)
  109. } else {
  110. let content = ActivityContent(
  111. state: state,
  112. staleDate: min(state.date, Date.now).addingTimeInterval(360) // 6 minutes in seconds
  113. )
  114. await currentActivity.activity.update(content)
  115. }
  116. } else {
  117. do {
  118. // Create initial non-stale content
  119. let nonStaleContent = ActivityContent(
  120. state: LiveActivityAttributes.ContentState(
  121. bg: "--",
  122. direction: nil,
  123. change: "--",
  124. date: Date.now,
  125. chart: [],
  126. chartDate: [],
  127. rotationDegrees: 0,
  128. highGlucose: 180,
  129. lowGlucose: 70,
  130. cob: 0,
  131. iob: 0,
  132. lockScreenView: "Simple",
  133. unit: "--"
  134. ),
  135. staleDate: Date.now.addingTimeInterval(60)
  136. )
  137. // Request a new activity
  138. let activity = try Activity.request(
  139. attributes: LiveActivityAttributes(startDate: Date.now),
  140. content: nonStaleContent,
  141. pushType: nil
  142. )
  143. currentActivity = ActiveActivity(activity: activity, startDate: Date.now)
  144. // Push the actual content
  145. await pushUpdate(state)
  146. } catch {
  147. print("Activity creation error: \(error)")
  148. }
  149. }
  150. }
  151. /// ends all live activities immediateny
  152. private func endActivity() async {
  153. if let currentActivity {
  154. await currentActivity.activity.end(nil, dismissalPolicy: .immediate)
  155. self.currentActivity = nil
  156. }
  157. // end any other activities
  158. for unknownActivity in Activity<LiveActivityAttributes>.activities {
  159. await unknownActivity.end(nil, dismissalPolicy: .immediate)
  160. }
  161. }
  162. }
  163. @available(iOS 16.2, *)
  164. extension LiveActivityBridge {
  165. func glucoseDidUpdate(_ glucose: [GlucoseData]) {
  166. guard settings.useLiveActivity else {
  167. if currentActivity != nil {
  168. Task {
  169. await self.endActivity()
  170. }
  171. }
  172. return
  173. }
  174. // backfill latest glucose if contained in this update
  175. if glucose.count > 1 {
  176. latestGlucose = glucose.dropFirst().first
  177. }
  178. defer {
  179. self.latestGlucose = glucose.first
  180. }
  181. guard let bg = glucose.first else {
  182. return
  183. }
  184. if let determination = determination {
  185. let content = LiveActivityAttributes.ContentState(
  186. new: bg,
  187. prev: latestGlucose,
  188. mmol: settings.units == .mmolL,
  189. chart: glucose,
  190. settings: settings,
  191. determination: determination
  192. )
  193. if let content = content {
  194. Task {
  195. await self.pushUpdate(content)
  196. }
  197. }
  198. }
  199. }
  200. }