LiveActivityBridge.swift 10 KB

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