LiveActivityBridge.swift 12 KB

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