LiveActivityBridge.swift 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import ActivityKit
  2. import Foundation
  3. import Swinject
  4. import UIKit
  5. extension LiveActivityAttributes.ContentState {
  6. static func formatGlucose(_ value: Int, mmol: Bool, forceSign: Bool) -> String {
  7. let formatter = NumberFormatter()
  8. formatter.numberStyle = .decimal
  9. formatter.maximumFractionDigits = 0
  10. if mmol {
  11. formatter.minimumFractionDigits = 1
  12. formatter.maximumFractionDigits = 1
  13. }
  14. if forceSign {
  15. formatter.positivePrefix = formatter.plusSign
  16. }
  17. formatter.roundingMode = .halfUp
  18. return formatter
  19. .string(from: mmol ? value.asMmolL as NSNumber : NSNumber(value: value))!
  20. }
  21. init?(new bg: BloodGlucose, prev: BloodGlucose?, mmol: Bool, chart: [Readings], settings: FreeAPSSettings) {
  22. guard let glucose = bg.glucose,
  23. bg.dateString.timeIntervalSinceNow > -TimeInterval(minutes: 6)
  24. else {
  25. return nil
  26. }
  27. let formattedBG = Self.formatGlucose(glucose, mmol: mmol, forceSign: false)
  28. let trendString: String?
  29. var rotationDegrees: Double = 0.0
  30. switch bg.direction {
  31. case .doubleUp,
  32. .singleUp,
  33. .tripleUp:
  34. trendString = "arrow.up"
  35. rotationDegrees = -90
  36. case .fortyFiveUp:
  37. trendString = "arrow.up.right"
  38. rotationDegrees = -45
  39. case .flat:
  40. trendString = "arrow.right"
  41. rotationDegrees = 0
  42. case .fortyFiveDown:
  43. trendString = "arrow.down.right"
  44. rotationDegrees = 45
  45. case .doubleDown,
  46. .singleDown,
  47. .tripleDown:
  48. trendString = "arrow.down"
  49. rotationDegrees = 90
  50. case .notComputable,
  51. Optional.none,
  52. .rateOutOfRange,
  53. .some(.none):
  54. trendString = nil
  55. rotationDegrees = 0
  56. }
  57. let change = prev?.glucose.map({
  58. Self.formatGlucose(glucose - $0, mmol: mmol, forceSign: true)
  59. }) ?? ""
  60. let chartBG = chart.map(\.glucose)
  61. let conversionFactor: Double = settings.units == .mmolL ? 18.0 : 1.0
  62. let convertedChartBG = chartBG.map { Double($0) / conversionFactor }
  63. let chartDate = chart.map(\.date)
  64. /// glucose limits from settings
  65. let highGlucose = settings.highGlucose
  66. let lowGlucose = settings.lowGlucose
  67. self.init(
  68. bg: formattedBG,
  69. trendSystemImage: trendString,
  70. change: change,
  71. date: bg.dateString,
  72. chart: convertedChartBG,
  73. chartDate: chartDate,
  74. rotationDegrees: rotationDegrees,
  75. highGlucose: Double(highGlucose),
  76. lowGlucose: Double(lowGlucose)
  77. )
  78. }
  79. }
  80. @available(iOS 16.2, *) private struct ActiveActivity {
  81. let activity: Activity<LiveActivityAttributes>
  82. let startDate: Date
  83. func needsRecreation() -> Bool {
  84. switch activity.activityState {
  85. case .dismissed,
  86. .ended:
  87. return true
  88. case .active,
  89. .stale: break
  90. @unknown default:
  91. return true
  92. }
  93. return -startDate.timeIntervalSinceNow >
  94. TimeInterval(60 * 60)
  95. }
  96. }
  97. @available(iOS 16.2, *) final class LiveActivityBridge: Injectable {
  98. @Injected() private var settingsManager: SettingsManager!
  99. @Injected() private var glucoseStorage: GlucoseStorage!
  100. @Injected() private var broadcaster: Broadcaster!
  101. private var settings: FreeAPSSettings {
  102. settingsManager.settings
  103. }
  104. private var currentActivity: ActiveActivity?
  105. private var latestGlucose: BloodGlucose?
  106. init(resolver: Resolver) {
  107. injectServices(resolver)
  108. broadcaster.register(GlucoseObserver.self, observer: self)
  109. Foundation.NotificationCenter.default.addObserver(
  110. forName: UIApplication.didEnterBackgroundNotification,
  111. object: nil,
  112. queue: nil
  113. ) { _ in
  114. self.forceActivityUpdate()
  115. }
  116. Foundation.NotificationCenter.default.addObserver(
  117. forName: UIApplication.didBecomeActiveNotification,
  118. object: nil,
  119. queue: nil
  120. ) { _ in
  121. self.forceActivityUpdate()
  122. }
  123. }
  124. /// creates and tries to present a new activity update from the current GlucoseStorage values if live activities are enabled in settings
  125. /// Ends existing live activities if live activities are not enabled in settings
  126. private func forceActivityUpdate() {
  127. // just before app resigns active, show a new activity
  128. // only do this if there is no current activity or the current activity is older than 1h
  129. if settings.useLiveActivity {
  130. if currentActivity?.needsRecreation() ?? true
  131. {
  132. glucoseDidUpdate(glucoseStorage.recent())
  133. }
  134. } else {
  135. Task {
  136. await self.endActivity()
  137. }
  138. }
  139. }
  140. /// attempts to present this live activity state, creating a new activity if none exists yet
  141. @MainActor private func pushUpdate(_ state: LiveActivityAttributes.ContentState) async {
  142. // hide duplicate/unknown activities
  143. for unknownActivity in Activity<LiveActivityAttributes>.activities
  144. .filter({ self.currentActivity?.activity.id != $0.id })
  145. {
  146. await unknownActivity.end(nil, dismissalPolicy: .immediate)
  147. }
  148. let content = ActivityContent(state: state, staleDate: state.date.addingTimeInterval(TimeInterval(6 * 60)))
  149. if let currentActivity {
  150. if currentActivity.needsRecreation(), UIApplication.shared.applicationState == .active {
  151. // activity is no longer visible or old. End it and try to push the update again
  152. await endActivity()
  153. await pushUpdate(state)
  154. } else {
  155. await currentActivity.activity.update(content)
  156. }
  157. } else {
  158. do {
  159. let activity = try Activity.request(
  160. attributes: LiveActivityAttributes(startDate: Date.now),
  161. content: content,
  162. pushType: nil
  163. )
  164. currentActivity = ActiveActivity(activity: activity, startDate: Date.now)
  165. } catch {
  166. print("activity creation error: \(error)")
  167. }
  168. }
  169. }
  170. /// ends all live activities immediateny
  171. private func endActivity() async {
  172. if let currentActivity {
  173. await currentActivity.activity.end(nil, dismissalPolicy: ActivityUIDismissalPolicy.immediate)
  174. self.currentActivity = nil
  175. }
  176. // end any other activities
  177. for unknownActivity in Activity<LiveActivityAttributes>.activities {
  178. await unknownActivity.end(nil, dismissalPolicy: .immediate)
  179. }
  180. }
  181. }
  182. @available(iOS 16.2, *)
  183. extension LiveActivityBridge: GlucoseObserver {
  184. func glucoseDidUpdate(_ glucose: [BloodGlucose]) {
  185. // backfill latest glucose if contained in this update
  186. if glucose.count > 1 {
  187. latestGlucose = glucose[glucose.count - 2]
  188. }
  189. defer {
  190. self.latestGlucose = glucose.last
  191. }
  192. // fetch glucose for chart from Core Data
  193. let coreDataStorage = CoreDataStorage()
  194. let sixHoursAgo = Calendar.current.date(byAdding: .hour, value: -6, to: Date()) ?? Date()
  195. let fetchGlucose = coreDataStorage.fetchGlucose(interval: sixHoursAgo as NSDate)
  196. guard let bg = glucose.last, let content = LiveActivityAttributes.ContentState(
  197. new: bg,
  198. prev: latestGlucose,
  199. mmol: settings.units == .mmolL,
  200. chart: fetchGlucose, settings: settings
  201. ) else {
  202. // no bg or value stale. Don't update the activity if there already is one, just let it turn stale so that it can still be used once current bg is available again
  203. return
  204. }
  205. Task {
  206. await self.pushUpdate(content)
  207. }
  208. }
  209. }