LiveActivityBridge.swift 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  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:
  17. break
  18. @unknown default:
  19. return true
  20. }
  21. return -startDate.timeIntervalSinceNow > TimeInterval(60 * 60)
  22. }
  23. }
  24. @available(iOS 16.2, *)
  25. final class LiveActivityBridge: Injectable, ObservableObject, SettingsObserver {
  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. func settingsDidChange(_: FreeAPSSettings) {
  81. Task {
  82. await updateContentState(determination)
  83. }
  84. }
  85. private func registerHandler() {
  86. coreDataPublisher?.filterByEntityName("OverrideStored").sink { [weak self] _ in
  87. guard let self = self else { return }
  88. self.overridesDidUpdate()
  89. }.store(in: &subscriptions)
  90. coreDataPublisher?.filterByEntityName("OrefDetermination").sink { [weak self] _ in
  91. guard let self = self else { return }
  92. self.cobOrIobDidUpdate()
  93. }.store(in: &subscriptions)
  94. }
  95. private func registerSubscribers() {
  96. glucoseStorage.updatePublisher
  97. .receive(on: DispatchQueue.global(qos: .background))
  98. .sink { [weak self] _ in
  99. guard let self = self else { return }
  100. self.setupGlucoseArray()
  101. }
  102. .store(in: &subscriptions)
  103. }
  104. private func cobOrIobDidUpdate() {
  105. Task { @MainActor in
  106. self.determination = await fetchAndMapDetermination()
  107. if let determination = determination {
  108. await self.updateContentState(determination)
  109. }
  110. }
  111. }
  112. private func overridesDidUpdate() {
  113. Task { @MainActor in
  114. self.override = await fetchAndMapOverride()
  115. if let determination = determination {
  116. await self.updateContentState(determination)
  117. }
  118. }
  119. }
  120. @objc private func handleLiveActivityOrderChange() {
  121. Task {
  122. self.widgetItems = UserDefaults.standard.loadLiveActivityOrderFromUserDefaults() ?? LiveActivityAttributes
  123. .LiveActivityItem.defaultItems
  124. await self.updateLiveActivityOrder()
  125. }
  126. }
  127. @MainActor private func updateContentState<T>(_ update: T) async {
  128. guard let latestGlucose = latestGlucose else {
  129. return
  130. }
  131. var content: LiveActivityAttributes.ContentState?
  132. widgetItems = UserDefaults.standard.loadLiveActivityOrderFromUserDefaults() ?? LiveActivityAttributes
  133. .LiveActivityItem.defaultItems
  134. if let determination = update as? DeterminationData {
  135. content = LiveActivityAttributes.ContentState(
  136. new: latestGlucose,
  137. prev: latestGlucose,
  138. units: settings.units,
  139. chart: glucoseFromPersistence ?? [],
  140. settings: settings,
  141. determination: determination,
  142. override: override,
  143. widgetItems: widgetItems
  144. )
  145. } else if let override = update as? OverrideData {
  146. content = LiveActivityAttributes.ContentState(
  147. new: latestGlucose,
  148. prev: latestGlucose,
  149. units: settings.units,
  150. chart: glucoseFromPersistence ?? [],
  151. settings: settings,
  152. determination: determination,
  153. override: override,
  154. widgetItems: widgetItems
  155. )
  156. }
  157. if let content = content {
  158. await pushUpdate(content)
  159. }
  160. }
  161. @MainActor private func updateLiveActivityOrder() async {
  162. Task {
  163. await updateContentState(determination)
  164. }
  165. }
  166. private func setupGlucoseArray() {
  167. Task { @MainActor in
  168. self.glucoseFromPersistence = await fetchAndMapGlucose()
  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. @MainActor private func forceActivityUpdate() {
  184. if settings.useLiveActivity {
  185. if currentActivity?.needsRecreation() ?? true {
  186. glucoseDidUpdate(glucoseFromPersistence ?? [])
  187. }
  188. } else {
  189. Task {
  190. await self.endActivity()
  191. }
  192. }
  193. }
  194. @MainActor private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
  195. for unknownActivity in Activity<LiveActivityAttributes>.activities
  196. .filter({ self.currentActivity?.activity.id != $0.id })
  197. {
  198. await unknownActivity.end(nil, dismissalPolicy: .immediate)
  199. }
  200. if let currentActivity = currentActivity {
  201. if currentActivity.needsRecreation(), UIApplication.shared.applicationState == .active {
  202. await endActivity()
  203. await pushUpdate(state)
  204. } else {
  205. let content = ActivityContent(
  206. state: state,
  207. staleDate: min(state.date ?? Date.now, Date.now).addingTimeInterval(360) // 6 minutes in seconds
  208. )
  209. await currentActivity.activity.update(content)
  210. }
  211. } else {
  212. do {
  213. let expired = ActivityContent(
  214. state: LiveActivityAttributes
  215. .ContentState(
  216. unit: settings.units.rawValue,
  217. bg: "--",
  218. direction: nil,
  219. change: "--",
  220. date: Date.now,
  221. highGlucose: settings.high,
  222. lowGlucose: settings.low,
  223. target: determination?.target ?? 100 as Decimal,
  224. glucoseColorScheme: settings.glucoseColorScheme.rawValue,
  225. detailedViewState: nil,
  226. isInitialState: true
  227. ),
  228. staleDate: Date.now.addingTimeInterval(60)
  229. )
  230. let activity = try Activity.request(
  231. attributes: LiveActivityAttributes(startDate: Date.now),
  232. content: expired,
  233. pushType: nil
  234. )
  235. currentActivity = ActiveActivity(activity: activity, startDate: Date.now)
  236. await pushUpdate(state)
  237. } catch {
  238. debug(
  239. .default,
  240. "\(#file): Error creating new activity: \(error)"
  241. )
  242. }
  243. }
  244. }
  245. private func endActivity() async {
  246. if let currentActivity {
  247. await currentActivity.activity.end(nil, dismissalPolicy: .immediate)
  248. self.currentActivity = nil
  249. }
  250. for unknownActivity in Activity<LiveActivityAttributes>.activities {
  251. await unknownActivity.end(nil, dismissalPolicy: .immediate)
  252. }
  253. }
  254. }
  255. @available(iOS 16.2, *)
  256. extension LiveActivityBridge {
  257. @MainActor func glucoseDidUpdate(_ glucose: [GlucoseData]) {
  258. guard settings.useLiveActivity else {
  259. if currentActivity != nil {
  260. Task {
  261. await self.endActivity()
  262. }
  263. }
  264. return
  265. }
  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: override,
  284. widgetItems: widgetItems
  285. )
  286. if let content = content {
  287. Task {
  288. await self.pushUpdate(content)
  289. }
  290. }
  291. }
  292. }
  293. }