| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687 |
- import Combine
- import Foundation
- import LoopKit
- import Swinject
- /// Issues a `.notLooping` alarm if no successful loop completes within the
- /// configured grace period (default 20 minutes). On every successful loop
- /// the pending alarm is retracted and a fresh delayed alarm is rescheduled
- /// — using `Alert.Trigger.delayed(interval:)` so it fires via UN even if
- /// the app is suspended.
- ///
- /// Replaces the legacy `scheduleMissingLoopNotifiactions` direct-UN path
- /// in `BaseUserNotificationsManager`. The alert now flows through
- /// `TrioAlertManager` and inherits tier config from Pump Alarms
- /// (Critical tier by default).
- final class NotLoopingMonitor: Injectable {
- @Injected() private var apsManager: APSManager!
- @Injected() private var trioAlertManager: TrioAlertManager!
- @Injected() private var settingsManager: SettingsManager!
- /// Minutes of staleness before the alarm fires. Mirrors the legacy
- /// `firstInterval` (20 min) — the second 40-min reminder is dropped;
- /// retract-on-loop semantics make it redundant.
- private static let gracePeriodMinutes: Int = 20
- private static let alertID = Alert.Identifier(
- managerIdentifier: "trio.aps",
- alertIdentifier: "loop.notActive"
- )
- private var subscriptions = Set<AnyCancellable>()
- init(resolver: Resolver) {
- injectServices(resolver)
- subscribe(to: apsManager.lastLoopDateSubject.eraseToAnyPublisher())
- }
- /// Publisher-only seam for tests: assigns the alert manager directly and
- /// subscribes to a supplied loop-date publisher, avoiding the need to stub
- /// the full `APSManager` protocol.
- init(loopDates: AnyPublisher<Date, Never>, trioAlertManager: TrioAlertManager) {
- self.trioAlertManager = trioAlertManager
- subscribe(to: loopDates)
- }
- private func subscribe(to loopDates: AnyPublisher<Date, Never>) {
- loopDates
- .sink { [weak self] _ in self?.rescheduleAlarm() }
- .store(in: &subscriptions)
- }
- private func rescheduleAlarm() {
- // Retract first — clears pending UN, modal timer, and throttler so the
- // next issueAlert isn't blocked by 5-min duplicate suppression.
- trioAlertManager.retractAlert(identifier: Self.alertID)
- // Skip when Trio isn't expected to be auto-enacting: open loop, an
- // active manual temp basal, or a suspended pump. In all three the
- // user has implicitly told Trio "don't loop right now"; a "Not
- // Looping" alarm would just be noise. nil injection (publisher-only
- // test seam) passes so the existing tests still exercise the
- // retract/reschedule plumbing.
- guard settingsManager?.settings.closedLoop != false,
- apsManager?.isManualTempBasal != true,
- apsManager?.isSuspended != true
- else { return }
- let content = Alert.Content(
- title: String(localized: "Trio Not Active"),
- body: String(
- format: String(localized: "Last loop was more than %d min ago"),
- Self.gracePeriodMinutes
- ),
- acknowledgeActionButtonLabel: String(localized: "OK")
- )
- let alert = Alert(
- identifier: Self.alertID,
- foregroundContent: content,
- backgroundContent: content,
- trigger: .delayed(interval: TimeInterval(Self.gracePeriodMinutes * 60)),
- interruptionLevel: .critical,
- sound: .sound(name: "honk.caf")
- )
- trioAlertManager.issueAlert(alert)
- }
- }
|