LiveActivityBridge.swift 7.9 KB

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