LiveActivityBridge.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  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, SettingsObserver
  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. private var coreDataObserver: CoreDataObserver?
  40. init(resolver: Resolver) {
  41. systemEnabled = activityAuthorizationInfo.areActivitiesEnabled
  42. injectServices(resolver)
  43. setupNotifications()
  44. coreDataObserver = CoreDataObserver()
  45. registerHandler()
  46. monitorForLiveActivityAuthorizationChanges()
  47. setupGlucoseArray()
  48. broadcaster.register(SettingsObserver.self, observer: self)
  49. }
  50. private func setupNotifications() {
  51. let notificationCenter = Foundation.NotificationCenter.default
  52. notificationCenter.addObserver(self, selector: #selector(handleBatchInsert), name: .didPerformBatchInsert, object: nil)
  53. notificationCenter.addObserver(self, selector: #selector(cobOrIobDidUpdate), name: .didUpdateCobIob, object: nil)
  54. notificationCenter
  55. .addObserver(forName: UIApplication.didEnterBackgroundNotification, object: nil, queue: nil) { [weak self] _ in
  56. self?.forceActivityUpdate()
  57. }
  58. notificationCenter
  59. .addObserver(forName: UIApplication.didBecomeActiveNotification, object: nil, queue: nil) { [weak self] _ in
  60. self?.forceActivityUpdate()
  61. }
  62. notificationCenter.addObserver(
  63. self,
  64. selector: #selector(handleLiveActivityOrderChange),
  65. name: .liveActivityOrderDidChange,
  66. object: nil
  67. )
  68. }
  69. // TODO: - use a delegate or a custom notification here instead
  70. func settingsDidChange(_: FreeAPSSettings) {
  71. guard let latestGlucose = latestGlucose else { return }
  72. let content = LiveActivityAttributes.ContentState(
  73. new: latestGlucose,
  74. prev: latestGlucose,
  75. units: settings.units,
  76. chart: glucoseFromPersistence ?? [],
  77. settings: settings,
  78. determination: determination,
  79. override: isOverridesActive
  80. )
  81. if let content = content {
  82. Task {
  83. await pushUpdate(content)
  84. }
  85. }
  86. }
  87. private func registerHandler() {
  88. // 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
  89. coreDataObserver?.registerHandler(for: "OverrideStored") { [weak self] in
  90. guard let self = self else { return }
  91. self.overridesDidUpdate()
  92. }
  93. }
  94. @objc private func handleBatchInsert() {
  95. setupGlucoseArray()
  96. }
  97. @objc private func cobOrIobDidUpdate() {
  98. Task {
  99. await fetchAndMapDetermination()
  100. if let determination = determination {
  101. await self.pushDeterminationUpdate(determination)
  102. }
  103. }
  104. }
  105. @objc private func overridesDidUpdate() {
  106. Task {
  107. await fetchAndMapOverride()
  108. if let determination = determination {
  109. await self.pushDeterminationUpdate(determination)
  110. }
  111. }
  112. }
  113. @objc private func handleLiveActivityOrderChange() {
  114. Task {
  115. await self.updateLiveActivityOrder()
  116. }
  117. }
  118. @MainActor private func updateLiveActivityOrder() async {
  119. guard let latestGlucose = latestGlucose else { return }
  120. let content = LiveActivityAttributes.ContentState(
  121. new: latestGlucose,
  122. prev: latestGlucose,
  123. units: settings.units,
  124. chart: glucoseFromPersistence ?? [],
  125. settings: settings,
  126. determination: determination,
  127. override: isOverridesActive
  128. )
  129. if let content = content {
  130. await pushUpdate(content)
  131. }
  132. }
  133. private func setupGlucoseArray() {
  134. Task {
  135. // Fetch and map glucose to GlucoseData struct
  136. await fetchAndMapGlucose()
  137. // Fetch and map Determination to DeterminationData struct
  138. await fetchAndMapDetermination()
  139. // Fetch and map Override to OverrideData struct
  140. /// shows if there is an active Override
  141. await fetchAndMapOverride()
  142. // Push the update to the Live Activity
  143. glucoseDidUpdate(glucoseFromPersistence ?? [])
  144. }
  145. }
  146. private func monitorForLiveActivityAuthorizationChanges() {
  147. Task {
  148. for await activityState in activityAuthorizationInfo.activityEnablementUpdates {
  149. if activityState != systemEnabled {
  150. await MainActor.run {
  151. systemEnabled = activityState
  152. }
  153. }
  154. }
  155. }
  156. }
  157. /// creates and tries to present a new activity update from the current GlucoseStorage values if live activities are enabled in settings
  158. /// Ends existing live activities if live activities are not enabled in settings
  159. private func forceActivityUpdate() {
  160. // just before app resigns active, show a new activity
  161. // only do this if there is no current activity or the current activity is older than 1h
  162. if settings.useLiveActivity {
  163. if currentActivity?.needsRecreation() ?? true
  164. {
  165. glucoseDidUpdate(glucoseFromPersistence ?? [])
  166. }
  167. } else {
  168. Task {
  169. await self.endActivity()
  170. }
  171. }
  172. }
  173. /// attempts to present this live activity state, creating a new activity if none exists yet
  174. @MainActor private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
  175. // // End all activities that are not the current one
  176. for unknownActivity in Activity<LiveActivityAttributes>.activities
  177. .filter({ self.currentActivity?.activity.id != $0.id })
  178. {
  179. await unknownActivity.end(nil, dismissalPolicy: .immediate)
  180. }
  181. if let currentActivity = currentActivity {
  182. if currentActivity.needsRecreation(), UIApplication.shared.applicationState == .active {
  183. await endActivity()
  184. await pushUpdate(state)
  185. } else {
  186. let content = ActivityContent(
  187. state: state,
  188. staleDate: min(state.date, Date.now).addingTimeInterval(360) // 6 minutes in seconds
  189. )
  190. await currentActivity.activity.update(content)
  191. }
  192. } else {
  193. do {
  194. // always push a non-stale content as the first update
  195. // pushing a stale content as the frst content results in the activity not being shown at all
  196. // apparently this initial state is also what is shown after the live activity expires (after 8h)
  197. let expired = ActivityContent(
  198. state: LiveActivityAttributes.ContentState(
  199. bg: "--",
  200. direction: nil,
  201. change: "--",
  202. date: Date.now,
  203. detailedViewState: nil,
  204. showCOB: true,
  205. showIOB: true,
  206. showCurrentGlucose: true,
  207. showUpdatedLabel: true,
  208. itemOrder: ["currentGlucose", "iob", "cob", "updatedLabel"],
  209. isInitialState: true
  210. ),
  211. staleDate: Date.now.addingTimeInterval(60)
  212. )
  213. // Request a new activity
  214. let activity = try Activity.request(
  215. attributes: LiveActivityAttributes(startDate: Date.now),
  216. content: expired,
  217. pushType: nil
  218. )
  219. currentActivity = ActiveActivity(activity: activity, startDate: Date.now)
  220. // then show the actual content
  221. await pushUpdate(state)
  222. } catch {
  223. print("Activity creation error: \(error)")
  224. }
  225. }
  226. }
  227. @MainActor private func pushDeterminationUpdate(_ determination: DeterminationData) async {
  228. guard let latestGlucose = latestGlucose else { return }
  229. let content = LiveActivityAttributes.ContentState(
  230. new: latestGlucose,
  231. prev: latestGlucose,
  232. units: settings.units,
  233. chart: glucoseFromPersistence ?? [],
  234. settings: settings,
  235. determination: determination,
  236. override: isOverridesActive
  237. )
  238. if let content = content {
  239. await pushUpdate(content)
  240. }
  241. }
  242. /// ends all live activities immediateny
  243. private func endActivity() async {
  244. if let currentActivity {
  245. await currentActivity.activity.end(nil, dismissalPolicy: .immediate)
  246. self.currentActivity = nil
  247. }
  248. // end any other activities
  249. for unknownActivity in Activity<LiveActivityAttributes>.activities {
  250. await unknownActivity.end(nil, dismissalPolicy: .immediate)
  251. }
  252. }
  253. }
  254. @available(iOS 16.2, *)
  255. extension LiveActivityBridge {
  256. func glucoseDidUpdate(_ glucose: [GlucoseData]) {
  257. guard settings.useLiveActivity else {
  258. if currentActivity != nil {
  259. Task {
  260. await self.endActivity()
  261. }
  262. }
  263. return
  264. }
  265. // backfill latest glucose if contained in this update
  266. if glucose.count > 1 {
  267. latestGlucose = glucose.dropFirst().first
  268. }
  269. defer {
  270. self.latestGlucose = glucose.first
  271. }
  272. guard let bg = glucose.first else {
  273. return
  274. }
  275. if let determination = determination {
  276. let content = LiveActivityAttributes.ContentState(
  277. new: bg,
  278. prev: latestGlucose,
  279. units: settings.units,
  280. chart: glucose,
  281. settings: settings,
  282. determination: determination,
  283. override: isOverridesActive
  284. )
  285. if let content = content {
  286. Task {
  287. await self.pushUpdate(content)
  288. }
  289. }
  290. }
  291. }
  292. }