diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index de0ca75346..10b6006110 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -509,6 +509,7 @@ C1E2773E224177C000354103 /* ClockKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C1E2773D224177C000354103 /* ClockKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; C1E2774822433D7A00354103 /* MKRingProgressView.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C1E2774722433D7A00354103 /* MKRingProgressView.framework */; }; C1F2075C26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */; }; + C1F7822627CC056900C0919A /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F7822527CC056900C0919A /* SettingsManager.swift */; }; C1F8B243223E73FD00DD66CF /* BolusProgressTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F8B1D122375E4200DD66CF /* BolusProgressTableViewCell.swift */; }; C1FB428C217806A400FAB378 /* StateColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FB428B217806A300FAB378 /* StateColorPalette.swift */; }; C1FB428D21791D2500FAB378 /* PumpManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43C3B6F620BBCAA30026CAFA /* PumpManager.swift */; }; @@ -556,7 +557,7 @@ E95D380124EADE7C005E2F50 /* DoseStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95D380024EADE7C005E2F50 /* DoseStoreProtocol.swift */; }; E95D380324EADF36005E2F50 /* CarbStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95D380224EADF36005E2F50 /* CarbStoreProtocol.swift */; }; E95D380524EADF78005E2F50 /* GlucoseStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E95D380424EADF78005E2F50 /* GlucoseStoreProtocol.swift */; }; - E98A55ED24EDD6380008715D /* SettingsStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98A55EC24EDD6380008715D /* SettingsStoreProtocol.swift */; }; + E98A55ED24EDD6380008715D /* LatestStoredSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98A55EC24EDD6380008715D /* LatestStoredSettingsProvider.swift */; }; E98A55EF24EDD6E60008715D /* DosingDecisionStoreProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98A55EE24EDD6E60008715D /* DosingDecisionStoreProtocol.swift */; }; E98A55F124EDD85E0008715D /* MockDosingDecisionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98A55F024EDD85E0008715D /* MockDosingDecisionStore.swift */; }; E98A55F324EDD9530008715D /* MockSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E98A55F224EDD9530008715D /* MockSettingsStore.swift */; }; @@ -1417,6 +1418,7 @@ C1E2773D224177C000354103 /* ClockKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ClockKit.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS.sdk/System/Library/Frameworks/ClockKit.framework; sourceTree = DEVELOPER_DIR; }; C1E2774722433D7A00354103 /* MKRingProgressView.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = MKRingProgressView.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileExpirationAlerter.swift; sourceTree = ""; }; + C1F7822527CC056900C0919A /* SettingsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsManager.swift; sourceTree = ""; }; C1F8B1D122375E4200DD66CF /* BolusProgressTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusProgressTableViewCell.swift; sourceTree = ""; }; C1F8B1DB223862D500DD66CF /* BolusProgressTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BolusProgressTableViewCell.xib; sourceTree = ""; }; C1FB428B217806A300FAB378 /* StateColorPalette.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StateColorPalette.swift; sourceTree = ""; }; @@ -1461,7 +1463,7 @@ E95D380024EADE7C005E2F50 /* DoseStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoseStoreProtocol.swift; sourceTree = ""; }; E95D380224EADF36005E2F50 /* CarbStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbStoreProtocol.swift; sourceTree = ""; }; E95D380424EADF78005E2F50 /* GlucoseStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStoreProtocol.swift; sourceTree = ""; }; - E98A55EC24EDD6380008715D /* SettingsStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStoreProtocol.swift; sourceTree = ""; }; + E98A55EC24EDD6380008715D /* LatestStoredSettingsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestStoredSettingsProvider.swift; sourceTree = ""; }; E98A55EE24EDD6E60008715D /* DosingDecisionStoreProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DosingDecisionStoreProtocol.swift; sourceTree = ""; }; E98A55F024EDD85E0008715D /* MockDosingDecisionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDosingDecisionStore.swift; sourceTree = ""; }; E98A55F224EDD9530008715D /* MockSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSettingsStore.swift; sourceTree = ""; }; @@ -2089,6 +2091,7 @@ 432E73CA1D24B3D6009AD15D /* RemoteDataServicesManager.swift */, A9C62D852331703000535612 /* Service.swift */, A9C62D872331703000535612 /* ServicesManager.swift */, + C1F7822527CC056900C0919A /* SettingsManager.swift */, E9BB27AA23B85C3500FB4987 /* SleepStore.swift */, 43FCEEA8221A615B0013DD30 /* StatusChartsManager.swift */, 1D63DEA426E950D400F46FA5 /* SupportManager.swift */, @@ -2610,7 +2613,7 @@ E95D380024EADE7C005E2F50 /* DoseStoreProtocol.swift */, E95D380224EADF36005E2F50 /* CarbStoreProtocol.swift */, E95D380424EADF78005E2F50 /* GlucoseStoreProtocol.swift */, - E98A55EC24EDD6380008715D /* SettingsStoreProtocol.swift */, + E98A55EC24EDD6380008715D /* LatestStoredSettingsProvider.swift */, E98A55EE24EDD6E60008715D /* DosingDecisionStoreProtocol.swift */, ); path = "Store Protocols"; @@ -2913,7 +2916,7 @@ 43776F8B1B8022E90074EA36 = { CreatedOnToolsVersion = 7.0; LastSwiftMigration = 1020; - ProvisioningStyle = Manual; + ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.ApplicationGroups.iOS = { enabled = 1; @@ -2935,7 +2938,7 @@ 43A943711B926B7B0051FA24 = { CreatedOnToolsVersion = 7.0; LastSwiftMigration = 0800; - ProvisioningStyle = Manual; + ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.ApplicationGroups.iOS = { enabled = 0; @@ -2948,7 +2951,7 @@ 43A9437D1B926B7B0051FA24 = { CreatedOnToolsVersion = 7.0; LastSwiftMigration = 1020; - ProvisioningStyle = Manual; + ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.ApplicationGroups.iOS = { enabled = 0; @@ -3003,7 +3006,7 @@ 4F70C1DB1DE8DCA7006380B7 = { CreatedOnToolsVersion = 8.1; LastSwiftMigration = 1020; - ProvisioningStyle = Manual; + ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.ApplicationGroups.iOS = { enabled = 1; @@ -3016,7 +3019,7 @@ ProvisioningStyle = Automatic; }; E9B07F7B253BBA6500BAD8F8 = { - ProvisioningStyle = Manual; + ProvisioningStyle = Automatic; }; }; }; @@ -3431,7 +3434,7 @@ C1F2075C26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift in Sources */, B4FEEF7D24B8A71F00A8DF9B /* DeviceDataManager+DeviceStatus.swift in Sources */, E95D380324EADF36005E2F50 /* CarbStoreProtocol.swift in Sources */, - E98A55ED24EDD6380008715D /* SettingsStoreProtocol.swift in Sources */, + E98A55ED24EDD6380008715D /* LatestStoredSettingsProvider.swift in Sources */, C1FB428F217921D600FAB378 /* PumpManagerUI.swift in Sources */, A9B996F227238705002DC09C /* DosingDecisionStore.swift in Sources */, 43C513191E864C4E001547C7 /* GlucoseRangeSchedule.swift in Sources */, @@ -3441,6 +3444,7 @@ 430B29932041F5B300BA9F93 /* UserDefaults+Loop.swift in Sources */, 43CE7CDE1CA8B63E003CC1B0 /* Data.swift in Sources */, E9BB27AB23B85C3500FB4987 /* SleepStore.swift in Sources */, + C1F7822627CC056900C0919A /* SettingsManager.swift in Sources */, C16575712538A36B004AE16E /* CGMStalenessMonitor.swift in Sources */, B4CAD8612549D1530057946B /* LoopSettingsAlerter.swift in Sources */, 1D080CBD2473214A00356610 /* AlertStore.xcdatamodeld in Sources */, diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index bcb7fec0c2..6994271f0e 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -103,13 +103,11 @@ final class DeviceDataManager { let doseStore: DoseStore let glucoseStore: GlucoseStore - + private let cacheStore: PersistenceController - + let dosingDecisionStore: DosingDecisionStore - let settingsStore: SettingsStore - /// All the HealthKit types to be read by stores private var readTypes: Set { var readTypes: Set = [] @@ -165,6 +163,8 @@ final class DeviceDataManager { var loggingServicesManager: LoggingServicesManager + var settingsManager: SettingsManager + var remoteDataServicesManager: RemoteDataServicesManager { return servicesManager.remoteDataServicesManager } var criticalEventLogExportManager: CriticalEventLogExportManager! @@ -185,11 +185,13 @@ final class DeviceDataManager { init(pluginManager: PluginManager, alertManager: AlertManager, + settingsManager: SettingsManager, bluetoothProvider: BluetoothProvider, alertPresenter: AlertPresenter, - closedLoopStatus: ClosedLoopStatus) + closedLoopStatus: ClosedLoopStatus, + cacheStore: PersistenceController, + localCacheDuration: TimeInterval) { - let localCacheDuration = Bundle.main.localCacheDuration let fileManager = FileManager.default let documentsDirectory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first! @@ -212,8 +214,9 @@ final class DeviceDataManager { self.alertPresenter = alertPresenter self.healthStore = HKHealthStore() - self.cacheStore = PersistenceController.controllerInAppGroupDirectory() - + self.cacheStore = cacheStore + self.settingsManager = settingsManager + let absorptionTimes = LoopCoreConstants.defaultCarbAbsorptionTimes let sensitivitySchedule = UserDefaults.appGroup?.insulinSensitivitySchedule let overrideHistory = UserDefaults.appGroup?.overrideHistory ?? TemporaryScheduleOverrideHistory.init() @@ -259,7 +262,6 @@ final class DeviceDataManager { cgmStalenessMonitor.delegate = glucoseStore self.dosingDecisionStore = DosingDecisionStore(store: cacheStore, expireAfter: localCacheDuration) - self.settingsStore = SettingsStore(store: cacheStore, expireAfter: localCacheDuration) self.cgmHasValidSensorSession = false self.closedLoopStatus = closedLoopStatus @@ -296,7 +298,7 @@ final class DeviceDataManager { glucoseStore: glucoseStore, carbStore: carbStore, dosingDecisionStore: dosingDecisionStore, - settingsStore: settingsStore, + latestStoredSettingsProvider: settingsManager, alertIssuer: alertManager, pumpInsulinType: pumpManager?.status.insulinType, automaticDosingStatus: closedLoopStatus @@ -311,8 +313,10 @@ final class DeviceDataManager { doseStore: doseStore, dosingDecisionStore: dosingDecisionStore, glucoseStore: glucoseStore, - settingsStore: settingsStore + settingsStore: settingsManager.settingsStore ) + + settingsManager.remoteDataServicesManager = remoteDataServicesManager servicesManager = ServicesManager( pluginManager: pluginManager, @@ -321,7 +325,7 @@ final class DeviceDataManager { remoteDataServicesManager: remoteDataServicesManager ) - let criticalEventLogs: [CriticalEventLog] = [settingsStore, glucoseStore, carbStore, dosingDecisionStore, doseStore, deviceLog, alertManager.alertStore] + let criticalEventLogs: [CriticalEventLog] = [settingsManager.settingsStore, glucoseStore, carbStore, dosingDecisionStore, doseStore, deviceLog, alertManager.alertStore] criticalEventLogExportManager = CriticalEventLogExportManager(logs: criticalEventLogs, directory: FileManager.default.exportsDirectoryURL, historicalDuration: Bundle.main.localCacheDuration) @@ -337,7 +341,6 @@ final class DeviceDataManager { doseStore.delegate = self dosingDecisionStore.delegate = self glucoseStore.delegate = self - settingsStore.delegate = self setupPump() setupCGM() @@ -764,7 +767,6 @@ extension DeviceDataManager { func didBecomeActive() { updatePumpManagerBLEHeartbeatPreference() - loopManager.didBecomeActive() } func updatePumpManagerBLEHeartbeatPreference() { @@ -810,7 +812,7 @@ extension DeviceDataManager: CGMManagerDelegate { } self.cgmManager = nil self.displayGlucoseUnitObservers.cleanupDeallocatedElements() - self.loopManager.storeSettings() + self.settingsManager.storeSettings() } } @@ -857,7 +859,7 @@ extension DeviceDataManager: CGMManagerOnboardingDelegate { DispatchQueue.main.async { self.refreshDeviceData() - self.loopManager.storeSettings() + self.settingsManager.storeSettings() } } } @@ -969,7 +971,7 @@ extension DeviceDataManager: PumpManagerDelegate { DispatchQueue.main.async { self.pumpManager = nil self.deliveryUncertaintyAlertManager = nil - self.loopManager.storeSettings() + self.settingsManager.storeSettings() } } @@ -1039,7 +1041,7 @@ extension DeviceDataManager: PumpManagerOnboardingDelegate { DispatchQueue.main.async { self.refreshDeviceData() - self.loopManager.storeSettings() + self.settingsManager.storeSettings() } } } @@ -1095,15 +1097,6 @@ extension DeviceDataManager: GlucoseStoreDelegate { } -// MARK: - SettingsStoreDelegate -extension DeviceDataManager: SettingsStoreDelegate { - - func settingsStoreHasUpdatedSettingsData(_ settingsStore: SettingsStore) { - remoteDataServicesManager.settingsStoreHasUpdatedSettingsData(settingsStore) - } - -} - // MARK: - TestingPumpManager extension DeviceDataManager { func deleteTestingPumpData(completion: ((Error?) -> Void)? = nil) { @@ -1299,17 +1292,23 @@ extension DeviceDataManager { fatalError("\(#function) should be invoked only when simulated core data is enabled") } - self.loopManager.generateSimulatedHistoricalCoreData() { error in + settingsManager.settingsStore.generateSimulatedHistoricalSettingsObjects() { error in guard error == nil else { completion(error) return } - self.deviceLog.generateSimulatedHistoricalDeviceLogEntries() { error in + self.loopManager.generateSimulatedHistoricalCoreData() { error in guard error == nil else { completion(error) return } - self.alertManager.alertStore.generateSimulatedHistoricalStoredAlerts(completion: completion) + self.deviceLog.generateSimulatedHistoricalDeviceLogEntries() { error in + guard error == nil else { + completion(error) + return + } + self.alertManager.alertStore.generateSimulatedHistoricalStoredAlerts(completion: completion) + } } } } @@ -1329,7 +1328,13 @@ extension DeviceDataManager { completion(error) return } - self.loopManager.purgeHistoricalCoreData(completion: completion) + self.loopManager.purgeHistoricalCoreData { error in + guard error == nil else { + completion(error) + return + } + self.settingsManager.purgeHistoricalSettingsObjects(completion: completion) + } } } } @@ -1489,3 +1494,5 @@ extension DeviceDataManager { extension DeviceDataManager { var availableSupports: [SupportUI] { [cgmManager, pumpManager].compactMap { $0 as? SupportUI } } } + +extension DeviceDataManager: DeviceStatusProvider {} diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 851fd2f8ab..1633af96c3 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -12,6 +12,7 @@ import Combine import LoopKit import LoopKitUI import MockKit +import HealthKit public protocol AlertPresenter: AnyObject { /// Present the alert view controller, with or without animation. @@ -71,6 +72,7 @@ class LoopAppManager: NSObject { private var onboardingManager: OnboardingManager! private var alertPermissionsChecker: AlertPermissionsChecker! private var supportManager: SupportManager! + private var settingsManager: SettingsManager! private var state: State = .initialize @@ -80,8 +82,6 @@ class LoopAppManager: NSObject { lazy private var cancellables = Set() - // MARK: - Initialization - func initialize(windowProvider: WindowProvider, launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { dispatchPrecondition(condition: .onQueue(.main)) precondition(state == .initialize) @@ -95,6 +95,15 @@ class LoopAppManager: NSObject { registerBackgroundTasks() + + if FeatureFlags.remoteOverridesEnabled { + DispatchQueue.main.async { + UIApplication.shared.registerForRemoteNotifications() + } + } + + + self.state = state.next } @@ -145,6 +154,9 @@ class LoopAppManager: NSObject { OrientationLock.deviceOrientationController = self UNUserNotificationCenter.current().delegate = self + let localCacheDuration = Bundle.main.localCacheDuration + let cacheStore = PersistenceController.controllerInAppGroupDirectory() + self.pluginManager = PluginManager() self.bluetoothStateManager = BluetoothStateManager() self.alertManager = AlertManager(alertPresenter: self, @@ -153,11 +165,20 @@ class LoopAppManager: NSObject { self.loopAlertsManager = LoopAlertsManager(alertManager: alertManager, bluetoothProvider: bluetoothStateManager) self.trustedTimeChecker = TrustedTimeChecker(alertManager: alertManager) + + self.settingsManager = SettingsManager(cacheStore: cacheStore, expireAfter: localCacheDuration) + self.deviceDataManager = DeviceDataManager(pluginManager: pluginManager, alertManager: alertManager, + settingsManager: settingsManager, bluetoothProvider: bluetoothStateManager, alertPresenter: self, - closedLoopStatus: closedLoopStatus) + closedLoopStatus: closedLoopStatus, + cacheStore: cacheStore, + localCacheDuration: localCacheDuration) + + settingsManager.deviceStatusProvider = deviceDataManager + SharedLogging.instance = deviceDataManager.loggingServicesManager scheduleBackgroundTasks() @@ -242,15 +263,13 @@ class LoopAppManager: NSObject { if let rootViewController = rootViewController { ProfileExpirationAlerter.alertIfNeeded(viewControllerToPresentFrom: rootViewController) } - deviceDataManager?.didBecomeActive() + settingsManager?.didBecomeActive() } // MARK: - Remote Notification func setRemoteNotificationsDeviceToken(_ remoteNotificationsDeviceToken: Data) { - deviceDataManager?.loopManager.mutateSettings { - settings in settings.deviceToken = remoteNotificationsDeviceToken - } + settingsManager.hasNewDeviceToken(token: remoteNotificationsDeviceToken) } private func handleRemoteNotificationFromLaunchOptions() { @@ -447,4 +466,5 @@ extension LoopAppManager: UNUserNotificationCenterDelegate { completionHandler() } + } diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 7b4182ed7f..3852f74c06 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -31,7 +31,7 @@ final class LoopDataManager: LoopSettingsAlerterDelegate { private let glucoseStore: GlucoseStoreProtocol - let settingsStore: SettingsStoreProtocol + let latestStoredSettingsProvider: LatestStoredSettingsProvider weak var delegate: LoopDataManagerDelegate? @@ -72,7 +72,7 @@ final class LoopDataManager: LoopSettingsAlerterDelegate { glucoseStore: GlucoseStoreProtocol, carbStore: CarbStoreProtocol, dosingDecisionStore: DosingDecisionStoreProtocol, - settingsStore: SettingsStoreProtocol, + latestStoredSettingsProvider: LatestStoredSettingsProvider, now: @escaping () -> Date = { Date() }, alertIssuer: AlertIssuer? = nil, pumpInsulinType: InsulinType?, @@ -98,7 +98,7 @@ final class LoopDataManager: LoopSettingsAlerterDelegate { self.now = now - self.settingsStore = settingsStore + self.latestStoredSettingsProvider = latestStoredSettingsProvider self.lockedPumpInsulinType = Locked(pumpInsulinType) @@ -594,7 +594,7 @@ extension LoopDataManager { recommendedAutomaticDose = (recommendation: recommendation, date: now()) var dosingDecision = StoredDosingDecision(reason: reason.rawValue) - dosingDecision.settings = StoredDosingDecision.Settings(settingsStore.latestSettings) + dosingDecision.settings = StoredDosingDecision.Settings(latestStoredSettingsProvider.latestSettings) dosingDecision.controllerStatus = UIDevice.current.controllerStatus dosingDecision.pumpManagerStatus = delegate?.pumpManagerStatus dosingDecision.cgmManagerStatus = delegate?.cgmManagerStatus @@ -783,7 +783,7 @@ extension LoopDataManager { func storeManualBolusDosingDecision(_ bolusDosingDecision: BolusDosingDecision, withDate date: Date) { let dosingDecision = StoredDosingDecision(date: date, reason: bolusDosingDecision.reason.rawValue, - settings: StoredDosingDecision.Settings(settingsStore.latestSettings), + settings: StoredDosingDecision.Settings(latestStoredSettingsProvider.latestSettings), scheduleOverride: bolusDosingDecision.scheduleOverride, controllerStatus: UIDevice.current.controllerStatus, pumpManagerStatus: delegate?.pumpManagerStatus, @@ -802,40 +802,6 @@ extension LoopDataManager { dosingDecisionStore.storeDosingDecision(dosingDecision) {} } - func storeSettings(notificationSettings: NotificationSettings? = nil, - controllerDevice: StoredSettings.ControllerDevice? = nil, - cgmDevice: HKDevice? = nil, - pumpDevice: HKDevice? = nil) - { - guard let appGroup = UserDefaults.appGroup, let loopSettings = appGroup.loopSettings else { - return - } - - let settings = StoredSettings(date: now(), - dosingEnabled: loopSettings.dosingEnabled, - glucoseTargetRangeSchedule: loopSettings.glucoseTargetRangeSchedule, - preMealTargetRange: loopSettings.preMealTargetRange, - workoutTargetRange: loopSettings.legacyWorkoutTargetRange, - overridePresets: loopSettings.overridePresets, - scheduleOverride: loopSettings.scheduleOverride, - preMealOverride: loopSettings.preMealOverride, - maximumBasalRatePerHour: loopSettings.maximumBasalRatePerHour, - maximumBolus: loopSettings.maximumBolus, - suspendThreshold: loopSettings.suspendThreshold, - deviceToken: loopSettings.deviceToken?.hexadecimalString, - insulinType: delegate?.pumpManagerStatus?.insulinType, - defaultRapidActingModel: appGroup.defaultRapidActingModel.map(StoredInsulinModel.init), - basalRateSchedule: appGroup.basalRateSchedule, - insulinSensitivitySchedule: appGroup.insulinSensitivitySchedule, - carbRatioSchedule: appGroup.carbRatioSchedule, - notificationSettings: notificationSettings ?? settingsStore.latestSettings?.notificationSettings, - controllerDevice: controllerDevice ?? UIDevice.current.controllerDevice, - cgmDevice: cgmDevice ?? delegate?.cgmManagerStatus?.device, - pumpDevice: pumpDevice ?? delegate?.pumpManagerStatus?.device, - bloodGlucoseUnit: loopSettings.glucoseUnit) - settingsStore.storeSettings(settings) {} - } - // Actions /// Runs the "loop" @@ -856,8 +822,6 @@ extension LoopDataManager { self.logger.default("Loop running") NotificationCenter.default.post(name: .LoopRunning, object: self) - self.storeUpdatedSettings() - self.lastLoopError = nil let startDate = self.now() @@ -900,8 +864,9 @@ extension LoopDataManager { dispatchPrecondition(condition: .onQueue(dataAccessQueue)) var dosingDecision = StoredDosingDecision(reason: reason.rawValue) - dosingDecision.settings = StoredDosingDecision.Settings(settingsStore.latestSettings) - dosingDecision.scheduleOverride = settingsStore.latestSettings?.scheduleOverride + let latestSettings = latestStoredSettingsProvider.latestSettings + dosingDecision.settings = StoredDosingDecision.Settings(latestSettings) + dosingDecision.scheduleOverride = latestSettings?.scheduleOverride dosingDecision.controllerStatus = UIDevice.current.controllerStatus dosingDecision.pumpManagerStatus = delegate?.pumpManagerStatus dosingDecision.cgmManagerStatus = delegate?.cgmManagerStatus @@ -1105,10 +1070,6 @@ extension LoopDataManager { } private func notify(forChange context: LoopUpdateContext) { - if case .preferences = context { - storeSettings() - } - NotificationCenter.default.post(name: .LoopDataUpdated, object: self, userInfo: [ @@ -2027,38 +1988,6 @@ extension LoopDataManager { } } -extension LoopDataManager { - func didBecomeActive () { - storeUpdatedSettings() - } - - func storeUpdatedSettings() { - UNUserNotificationCenter.current().getNotificationSettings() { notificationSettings in - self.dataAccessQueue.async { - guard let latestSettings = self.settingsStore.latestSettings else { - return - } - - let notificationSettings = NotificationSettings(notificationSettings) - let controllerDevice = UIDevice.current.controllerDevice - let cgmDevice = self.delegate?.cgmManagerStatus?.device - let pumpDevice = self.delegate?.pumpManagerStatus?.device - - if notificationSettings != latestSettings.notificationSettings || - controllerDevice != latestSettings.controllerDevice || - cgmDevice != latestSettings.cgmDevice || - pumpDevice != latestSettings.pumpDevice - { - self.storeSettings(notificationSettings: notificationSettings, - controllerDevice: controllerDevice, - cgmDevice: cgmDevice, - pumpDevice: pumpDevice) - } - } - } - } -} - extension LoopDataManager { /// Generates a diagnostic report about the current state /// @@ -2234,23 +2163,6 @@ private extension StoredDosingDecision.Settings { } } -private extension NotificationSettings { - init(_ notificationSettings: UNNotificationSettings) { - self.init(authorizationStatus: NotificationSettings.AuthorizationStatus(notificationSettings.authorizationStatus), - soundSetting: NotificationSettings.NotificationSetting(notificationSettings.soundSetting), - badgeSetting: NotificationSettings.NotificationSetting(notificationSettings.badgeSetting), - alertSetting: NotificationSettings.NotificationSetting(notificationSettings.alertSetting), - notificationCenterSetting: NotificationSettings.NotificationSetting(notificationSettings.notificationCenterSetting), - lockScreenSetting: NotificationSettings.NotificationSetting(notificationSettings.lockScreenSetting), - carPlaySetting: NotificationSettings.NotificationSetting(notificationSettings.carPlaySetting), - alertStyle: NotificationSettings.AlertStyle(notificationSettings.alertStyle), - showPreviewsSetting: NotificationSettings.ShowPreviewsSetting(notificationSettings.showPreviewsSetting), - criticalAlertSetting: NotificationSettings.NotificationSetting(notificationSettings.criticalAlertSetting), - providesAppNotificationSettings: notificationSettings.providesAppNotificationSettings, - announcementSetting: NotificationSettings.NotificationSetting(notificationSettings.announcementSetting)) - } -} - // MARK: - Simulated Core Data extension LoopDataManager { @@ -2259,32 +2171,26 @@ extension LoopDataManager { fatalError("\(#function) should be invoked only when simulated core data is enabled") } - guard let glucoseStore = glucoseStore as? GlucoseStore, let carbStore = carbStore as? CarbStore, let doseStore = doseStore as? DoseStore, let settingsStore = settingsStore as? SettingsStore, let dosingDecisionStore = dosingDecisionStore as? DosingDecisionStore else { + guard let glucoseStore = glucoseStore as? GlucoseStore, let carbStore = carbStore as? CarbStore, let doseStore = doseStore as? DoseStore, let dosingDecisionStore = dosingDecisionStore as? DosingDecisionStore else { fatalError("Mock stores should not be used to generate simulated core data") } - settingsStore.generateSimulatedHistoricalSettingsObjects() { error in + glucoseStore.generateSimulatedHistoricalGlucoseObjects() { error in guard error == nil else { completion(error) return } - glucoseStore.generateSimulatedHistoricalGlucoseObjects() { error in + carbStore.generateSimulatedHistoricalCarbObjects() { error in guard error == nil else { completion(error) return } - carbStore.generateSimulatedHistoricalCarbObjects() { error in + dosingDecisionStore.generateSimulatedHistoricalDosingDecisionObjects() { error in guard error == nil else { completion(error) return } - dosingDecisionStore.generateSimulatedHistoricalDosingDecisionObjects() { error in - guard error == nil else { - completion(error) - return - } - doseStore.generateSimulatedHistoricalPumpEvents(completion: completion) - } + doseStore.generateSimulatedHistoricalPumpEvents(completion: completion) } } } @@ -2295,7 +2201,7 @@ extension LoopDataManager { fatalError("\(#function) should be invoked only when simulated core data is enabled") } - guard let glucoseStore = glucoseStore as? GlucoseStore, let carbStore = carbStore as? CarbStore, let doseStore = doseStore as? DoseStore, let settingsStore = settingsStore as? SettingsStore, let dosingDecisionStore = dosingDecisionStore as? DosingDecisionStore else { + guard let glucoseStore = glucoseStore as? GlucoseStore, let carbStore = carbStore as? CarbStore, let doseStore = doseStore as? DoseStore, let dosingDecisionStore = dosingDecisionStore as? DosingDecisionStore else { fatalError("Mock stores should not be used to generate simulated core data") } @@ -2314,14 +2220,7 @@ extension LoopDataManager { completion(error) return } - - glucoseStore.purgeHistoricalGlucoseObjects() { error in - guard error == nil else { - completion(error) - return - } - settingsStore.purgeHistoricalSettingsObjects(completion: completion) - } + glucoseStore.purgeHistoricalGlucoseObjects(completion: completion) } } } diff --git a/Loop/Managers/NotificationManager.swift b/Loop/Managers/NotificationManager.swift index 9b419ad813..2a9a81e36d 100644 --- a/Loop/Managers/NotificationManager.swift +++ b/Loop/Managers/NotificationManager.swift @@ -70,11 +70,6 @@ extension NotificationManager { guard settings.authorizationStatus == .authorized else { return } - if FeatureFlags.remoteOverridesEnabled { - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } - } } } center.setNotificationCategories(notificationCategories) diff --git a/Loop/Managers/SettingsManager.swift b/Loop/Managers/SettingsManager.swift new file mode 100644 index 0000000000..3b4c72d4b7 --- /dev/null +++ b/Loop/Managers/SettingsManager.swift @@ -0,0 +1,155 @@ +// +// SettingsManager.swift +// Loop +// +// Created by Pete Schwamb on 2/27/22. +// Copyright © 2022 LoopKit Authors. All rights reserved. +// + +import Foundation +import LoopKit +import UserNotifications +import UIKit +import HealthKit +import Combine + +protocol DeviceStatusProvider { + var pumpManagerStatus: PumpManagerStatus? { get } + var cgmManagerStatus: CGMManagerStatus? { get } +} + +class SettingsManager { + + let settingsStore: SettingsStore + + var remoteDataServicesManager: RemoteDataServicesManager? + + var deviceStatusProvider: DeviceStatusProvider? + + public var latestSettings: StoredSettings? { + return settingsStore.latestSettings + } + + // Push Notifications (do not persist) + private var deviceToken: Data? + + private var cancellables: Set = [] + + init(cacheStore: PersistenceController, expireAfter: TimeInterval) { + settingsStore = SettingsStore(store: cacheStore, expireAfter: expireAfter) + settingsStore.delegate = self + + NotificationCenter.default + .publisher(for: .LoopDataUpdated) + .receive(on: DispatchQueue.main) + .sink { [weak self] note in + let context = note.userInfo?[LoopDataManager.LoopUpdateContextKey] as! LoopDataManager.LoopUpdateContext.RawValue + if case .preferences = LoopDataManager.LoopUpdateContext(rawValue: context) { + self?.storeSettings() + } + } + .store(in: &cancellables) + } + + func storeSettings(notificationSettings: NotificationSettings? = nil, + controllerDevice: StoredSettings.ControllerDevice? = nil, + cgmDevice: HKDevice? = nil, + pumpDevice: HKDevice? = nil) + { + guard let appGroup = UserDefaults.appGroup, let loopSettings = appGroup.loopSettings, let deviceToken = deviceToken else { + return + } + + let settings = StoredSettings(date: Date(), + dosingEnabled: loopSettings.dosingEnabled, + glucoseTargetRangeSchedule: loopSettings.glucoseTargetRangeSchedule, + preMealTargetRange: loopSettings.preMealTargetRange, + workoutTargetRange: loopSettings.legacyWorkoutTargetRange, + overridePresets: loopSettings.overridePresets, + scheduleOverride: loopSettings.scheduleOverride, + preMealOverride: loopSettings.preMealOverride, + maximumBasalRatePerHour: loopSettings.maximumBasalRatePerHour, + maximumBolus: loopSettings.maximumBolus, + suspendThreshold: loopSettings.suspendThreshold, + deviceToken: deviceToken.hexadecimalString, + insulinType: deviceStatusProvider?.pumpManagerStatus?.insulinType, + defaultRapidActingModel: appGroup.defaultRapidActingModel.map(StoredInsulinModel.init), + basalRateSchedule: appGroup.basalRateSchedule, + insulinSensitivitySchedule: appGroup.insulinSensitivitySchedule, + carbRatioSchedule: appGroup.carbRatioSchedule, + notificationSettings: notificationSettings ?? settingsStore.latestSettings?.notificationSettings, + controllerDevice: controllerDevice ?? UIDevice.current.controllerDevice, + cgmDevice: cgmDevice ?? deviceStatusProvider?.cgmManagerStatus?.device, + pumpDevice: pumpDevice ?? deviceStatusProvider?.pumpManagerStatus?.device, + bloodGlucoseUnit: loopSettings.glucoseUnit) + + if let latestSettings = latestSettings, latestSettings == settings { + // Skipping unchanged settings store + return + } + + settingsStore.storeSettings(settings) {} + } + + func storeSettingsCheckingNotificationPermissions() { + UNUserNotificationCenter.current().getNotificationSettings() { notificationSettings in + guard let latestSettings = self.settingsStore.latestSettings else { + return + } + + let notificationSettings = NotificationSettings(notificationSettings) + let controllerDevice = UIDevice.current.controllerDevice + let cgmDevice = self.deviceStatusProvider?.cgmManagerStatus?.device + let pumpDevice = self.deviceStatusProvider?.pumpManagerStatus?.device + + if notificationSettings != latestSettings.notificationSettings || + controllerDevice != latestSettings.controllerDevice || + cgmDevice != latestSettings.cgmDevice || + pumpDevice != latestSettings.pumpDevice + { + self.storeSettings(notificationSettings: notificationSettings, + controllerDevice: controllerDevice, + cgmDevice: cgmDevice, + pumpDevice: pumpDevice) + } + } + } + + func didBecomeActive () { + storeSettingsCheckingNotificationPermissions() + } + + func hasNewDeviceToken(token: Data) { + self.deviceToken = token + storeSettings() + } + + func purgeHistoricalSettingsObjects(completion: @escaping (Error?) -> Void) { + settingsStore.purgeHistoricalSettingsObjects(completion: completion) + } +} + +// MARK: - SettingsStoreDelegate +extension SettingsManager: SettingsStoreDelegate { + func settingsStoreHasUpdatedSettingsData(_ settingsStore: SettingsStore) { + remoteDataServicesManager?.settingsStoreHasUpdatedSettingsData(settingsStore) + } +} + +private extension NotificationSettings { + init(_ notificationSettings: UNNotificationSettings) { + self.init(authorizationStatus: NotificationSettings.AuthorizationStatus(notificationSettings.authorizationStatus), + soundSetting: NotificationSettings.NotificationSetting(notificationSettings.soundSetting), + badgeSetting: NotificationSettings.NotificationSetting(notificationSettings.badgeSetting), + alertSetting: NotificationSettings.NotificationSetting(notificationSettings.alertSetting), + notificationCenterSetting: NotificationSettings.NotificationSetting(notificationSettings.notificationCenterSetting), + lockScreenSetting: NotificationSettings.NotificationSetting(notificationSettings.lockScreenSetting), + carPlaySetting: NotificationSettings.NotificationSetting(notificationSettings.carPlaySetting), + alertStyle: NotificationSettings.AlertStyle(notificationSettings.alertStyle), + showPreviewsSetting: NotificationSettings.ShowPreviewsSetting(notificationSettings.showPreviewsSetting), + criticalAlertSetting: NotificationSettings.NotificationSetting(notificationSettings.criticalAlertSetting), + providesAppNotificationSettings: notificationSettings.providesAppNotificationSettings, + announcementSetting: NotificationSettings.NotificationSetting(notificationSettings.announcementSetting)) + } +} + diff --git a/Loop/Managers/Store Protocols/LatestStoredSettingsProvider.swift b/Loop/Managers/Store Protocols/LatestStoredSettingsProvider.swift new file mode 100644 index 0000000000..ea5731a338 --- /dev/null +++ b/Loop/Managers/Store Protocols/LatestStoredSettingsProvider.swift @@ -0,0 +1,15 @@ +// +// LatestStoredSettingsProvider.swift +// Loop +// +// Created by Anna Quinlan on 8/19/20. +// Copyright © 2020 LoopKit Authors. All rights reserved. +// + +import LoopKit + +protocol LatestStoredSettingsProvider: AnyObject { + var latestSettings: StoredSettings? { get } +} + +extension SettingsManager: LatestStoredSettingsProvider { } diff --git a/Loop/Managers/Store Protocols/SettingsStoreProtocol.swift b/Loop/Managers/Store Protocols/SettingsStoreProtocol.swift deleted file mode 100644 index d73dfeff7c..0000000000 --- a/Loop/Managers/Store Protocols/SettingsStoreProtocol.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// SettingsStoreProtocol.swift -// Loop -// -// Created by Anna Quinlan on 8/19/20. -// Copyright © 2020 LoopKit Authors. All rights reserved. -// - -import LoopKit - -protocol SettingsStoreProtocol: AnyObject { - var latestSettings: StoredSettings? { get } - func storeSettings(_ settings: StoredSettings, completion: @escaping () -> Void) -} - -extension SettingsStore: SettingsStoreProtocol { } diff --git a/LoopCore/LoopSettings.swift b/LoopCore/LoopSettings.swift index fa4ea0297b..279b6c80c1 100644 --- a/LoopCore/LoopSettings.swift +++ b/LoopCore/LoopSettings.swift @@ -80,9 +80,6 @@ public struct LoopSettings: Equatable { return glucoseTargetRangeSchedule?.unit } - // MARK - Push Notifications - - public var deviceToken: Data? // MARK - Guardrails diff --git a/LoopTests/Managers/LoopDataManagerTests.swift b/LoopTests/Managers/LoopDataManagerTests.swift index 70dc4081fa..aee00aadeb 100644 --- a/LoopTests/Managers/LoopDataManagerTests.swift +++ b/LoopTests/Managers/LoopDataManagerTests.swift @@ -107,7 +107,7 @@ class LoopDataManagerDosingTests: XCTestCase { glucoseStore: glucoseStore, carbStore: carbStore, dosingDecisionStore: dosingDecisionStore, - settingsStore: MockSettingsStore(), + latestStoredSettingsProvider: MockLatestStoredSettingsProvider(), now: { currentDate }, pumpInsulinType: .novolog, automaticDosingStatus: automaticDosingStatus diff --git a/LoopTests/Mock Stores/MockSettingsStore.swift b/LoopTests/Mock Stores/MockSettingsStore.swift index 025ca12b29..53a414085f 100644 --- a/LoopTests/Mock Stores/MockSettingsStore.swift +++ b/LoopTests/Mock Stores/MockSettingsStore.swift @@ -9,7 +9,7 @@ import LoopKit @testable import Loop -class MockSettingsStore: SettingsStoreProtocol { +class MockLatestStoredSettingsProvider: LatestStoredSettingsProvider { var latestSettings: StoredSettings? { nil } func storeSettings(_ settings: StoredSettings, completion: @escaping () -> Void) { completion() diff --git a/LoopTests/ViewModels/BolusEntryViewModelTests.swift b/LoopTests/ViewModels/BolusEntryViewModelTests.swift index e3f7364cfe..7fdae3d973 100644 --- a/LoopTests/ViewModels/BolusEntryViewModelTests.swift +++ b/LoopTests/ViewModels/BolusEntryViewModelTests.swift @@ -829,7 +829,9 @@ extension BolusEntryViewModelTests { } func triggerLoopStateUpdated(with state: LoopState, function: String = #function) throws { - NotificationCenter.default.post(name: .LoopDataUpdated, object: nil) + NotificationCenter.default.post(name: .LoopDataUpdated, object: nil, userInfo: [ + LoopDataManager.LoopUpdateContextKey: LoopDataManager.LoopUpdateContext.tempBasal.rawValue + ]) try triggerLoopStateResult(with: state, function: function) }