From 61f1cf4777989a3c1ab976d3660643dc1349edeb Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Tue, 15 Mar 2022 18:39:18 -0500 Subject: [PATCH 1/7] Reorder build phases for Xcode 13 --- Loop.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 01264c9539..2d01776a7a 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -2861,9 +2861,9 @@ isa = PBXNativeTarget; buildConfigurationList = 4F7528921DFE1DC600C322D6 /* Build configuration list for PBXNativeTarget "LoopUI" */; buildPhases = ( + 4F7528881DFE1DC600C322D6 /* Headers */, 4F7528861DFE1DC600C322D6 /* Sources */, 4F7528871DFE1DC600C322D6 /* Frameworks */, - 4F7528881DFE1DC600C322D6 /* Headers */, 4F7528891DFE1DC600C322D6 /* Resources */, ); buildRules = ( From e234bf248bdc7f172c842964d9225d482127c36f Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Thu, 17 Mar 2022 14:55:56 -0500 Subject: [PATCH 2/7] LOOP-3980 Avoid cancelling temp basal on launch. (#499) * Avoid cancelling temp basal on launch * Add test for ignoring initial value of isClosedLoop --- Loop/Managers/LoopDataManager.swift | 1 + LoopTests/Managers/LoopDataManagerTests.swift | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 1e1e2f0746..6a8787f581 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -181,6 +181,7 @@ final class LoopDataManager: LoopSettingsAlerterDelegate { // Cancel any active temp basal when going into closed loop off mode // The dispatch is necessary in case this is coming from a didSet already on the settings struct. self.automaticDosingStatus.$isClosedLoop + .dropFirst() .removeDuplicates() .receive(on: DispatchQueue.main) .sink { if !$0 { diff --git a/LoopTests/Managers/LoopDataManagerTests.swift b/LoopTests/Managers/LoopDataManagerTests.swift index c7628bfe70..675e74923d 100644 --- a/LoopTests/Managers/LoopDataManagerTests.swift +++ b/LoopTests/Managers/LoopDataManagerTests.swift @@ -498,6 +498,62 @@ class LoopDataManagerDosingTests: XCTestCase { XCTAssertEqual(recommendedBolus!.amount, 1.52, accuracy: 0.01) } + func testIsClosedLoopAvoidsTriggeringTempBasalCancelOnCreation() { + let settings = LoopSettings( + dosingEnabled: false, + glucoseTargetRangeSchedule: glucoseTargetRangeSchedule, + maximumBasalRatePerHour: maxBasalRate, + maximumBolus: maxBolus, + suspendThreshold: suspendThreshold + ) + + let doseStore = MockDoseStore() + let glucoseStore = MockGlucoseStore() + let carbStore = MockCarbStore() + + let currentDate = Date() + + dosingDecisionStore = MockDosingDecisionStore() + automaticDosingStatus = AutomaticDosingStatus(isClosedLoop: false, isClosedLoopAllowed: true) + let existingTempBasal = DoseEntry( + type: .tempBasal, + startDate: currentDate.addingTimeInterval(-.minutes(2)), + endDate: currentDate.addingTimeInterval(.minutes(28)), + value: 1.0, + unit: .unitsPerHour, + deliveredUnits: nil, + description: "Mock Temp Basal", + syncIdentifier: "asdf", + scheduledBasalRate: nil, + insulinType: .novolog, + automatic: true, + manuallyEntered: false, + isMutable: true) + loopDataManager = LoopDataManager( + lastLoopCompleted: currentDate.addingTimeInterval(-.minutes(5)), + basalDeliveryState: .tempBasal(existingTempBasal), + settings: settings, + overrideHistory: TemporaryScheduleOverrideHistory(), + lastPumpEventsReconciliation: nil, // this date is only used to init the doseStore if a DoseStoreProtocol isn't passed in, so this date can be nil + analyticsServicesManager: AnalyticsServicesManager(), + localCacheDuration: .days(1), + doseStore: doseStore, + glucoseStore: glucoseStore, + carbStore: carbStore, + dosingDecisionStore: dosingDecisionStore, + settingsStore: MockSettingsStore(), + now: { currentDate }, + pumpInsulinType: .novolog, + automaticDosingStatus: automaticDosingStatus + ) + let mockDelegate = MockDelegate() + loopDataManager.delegate = mockDelegate + + // Dose enacting happens asynchronously, as does receiving isClosedLoop signals + waitOnMain(timeout: 5) + XCTAssertNil(mockDelegate.recommendation) + } + } extension LoopDataManagerDosingTests { From 0d3e81e7c65ba46110e9cef8713b5ddf274846e0 Mon Sep 17 00:00:00 2001 From: Rick Pasetto Date: Mon, 21 Mar 2022 11:58:31 -0700 Subject: [PATCH 3/7] COASTAL-651: Adds PersistedAlertStore for looking up outstanding alerts (#501) * Checkpoint: AlertSearcher * checkpoint * COASTAL-651: Adds PersistedAlertStore for looking up outstanding alerts Adds ability to query CoreData for all outstanding (i.e. "unretracted") alerts from a given `managerIdentifier`. https://tidepool.atlassian.net/browse/COASTAL-651 * unit tests * This makes issuing and retracting alerts synchronous, so when the functions return CoreData may be queried (This is not ideal, but in practice the performance hit will be minimal) * Better version of previous commit: make writes to AlertStore synchronous so subsequent reads get latest data. (Again, performance hit should be minimal) * PR Feedback --- Loop/Managers/Alerts/AlertManager.swift | 51 +++++++++++- Loop/Managers/Alerts/AlertStore.swift | 36 ++++++-- Loop/Managers/DeviceDataManager.swift | 13 +++ .../Managers/Alerts/AlertManagerTests.swift | 59 ++++++++++++- .../Managers/Alerts/AlertStoreTests.swift | 82 ++++++++++++++++--- 5 files changed, 222 insertions(+), 19 deletions(-) diff --git a/Loop/Managers/Alerts/AlertManager.swift b/Loop/Managers/Alerts/AlertManager.swift index 9d851ffc29..720489fe44 100644 --- a/Loop/Managers/Alerts/AlertManager.swift +++ b/Loop/Managers/Alerts/AlertManager.swift @@ -186,7 +186,7 @@ extension AlertManager { } private func playbackAlertsFromAlertStore() { - alertStore.lookupAllUnacknowledged { + alertStore.lookupAllUnacknowledgedUnretracted { switch $0 { case .failure(let error): self.log.error("Could not fetch unacknowledged alerts: %@", error.localizedDescription) @@ -251,6 +251,55 @@ extension AlertManager { } } +// MARK: PersistedAlertStore +extension AlertManager: PersistedAlertStore { + public func lookupAllUnretracted(managerIdentifier: String, completion: @escaping (Result<[PersistedAlert], Error>) -> Void) { + alertStore.lookupAllUnretracted(managerIdentifier: managerIdentifier) { + switch $0 { + case .failure(let error): + completion(.failure(error)) + case .success(let alerts): + do { + let result = try alerts.map { + PersistedAlert( + alert: try Alert(from: $0, adjustedForStorageTime: false), + issuedDate: $0.issuedDate, + retractedDate: $0.retractedDate, + acknowledgedDate: $0.acknowledgedDate + ) + } + completion(.success(result)) + } catch { + completion(.failure(error)) + } + } + } + } + + public func lookupAllUnacknowledgedUnretracted(managerIdentifier: String, completion: @escaping (Result<[PersistedAlert], Error>) -> Void) { + alertStore.lookupAllUnacknowledgedUnretracted(managerIdentifier: managerIdentifier) { + switch $0 { + case .failure(let error): + completion(.failure(error)) + case .success(let alerts): + do { + let result = try alerts.map { + PersistedAlert( + alert: try Alert(from: $0, adjustedForStorageTime: false), + issuedDate: $0.issuedDate, + retractedDate: $0.retractedDate, + acknowledgedDate: $0.acknowledgedDate + ) + } + completion(.success(result)) + } catch { + completion(.failure(error)) + } + } + } + } +} + // MARK: Extensions fileprivate extension SyncAlertObject { diff --git a/Loop/Managers/Alerts/AlertStore.swift b/Loop/Managers/Alerts/AlertStore.swift index 265d37467a..dc86e98e8c 100644 --- a/Loop/Managers/Alerts/AlertStore.swift +++ b/Loop/Managers/Alerts/AlertStore.swift @@ -83,7 +83,7 @@ public class AlertStore { } public func recordIssued(alert: Alert, at date: Date = Date(), completion: ((Result) -> Void)? = nil) { - self.managedObjectContext.perform { + self.managedObjectContext.performAndWait { _ = StoredAlert(from: alert, context: self.managedObjectContext, issuedDate: date) do { try self.managedObjectContext.save() @@ -126,14 +126,38 @@ public class AlertStore { completion: completion) } - public func lookupAllUnacknowledged(completion: @escaping (Result<[StoredAlert], Error>) -> Void) { + public func lookupAllUnretracted(managerIdentifier: String? = nil, completion: @escaping (Result<[StoredAlert], Error>) -> Void) { managedObjectContext.perform { do { let fetchRequest: NSFetchRequest = StoredAlert.fetchRequest() - fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [ + var predicates = [ + NSPredicate(format: "retractedDate == nil"), + ] + if let managerIdentifier = managerIdentifier { + predicates.insert(NSPredicate(format: "managerIdentifier = %@", managerIdentifier), at: 0) + } + fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) + fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "modificationCounter", ascending: true) ] + let result = try self.managedObjectContext.fetch(fetchRequest) + completion(.success(result)) + } catch { + completion(.failure(error)) + } + } + } + + public func lookupAllUnacknowledgedUnretracted(managerIdentifier: String? = nil, completion: @escaping (Result<[StoredAlert], Error>) -> Void) { + managedObjectContext.perform { + do { + let fetchRequest: NSFetchRequest = StoredAlert.fetchRequest() + var predicates = [ NSPredicate(format: "acknowledgedDate == nil"), NSPredicate(format: "retractedDate == nil"), - ]) + ] + if let managerIdentifier = managerIdentifier { + predicates.insert(NSPredicate(format: "managerIdentifier = %@", managerIdentifier), at: 0) + } + fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "modificationCounter", ascending: true) ] let result = try self.managedObjectContext.fetch(fetchRequest) completion(.success(result)) @@ -172,7 +196,7 @@ extension AlertStore { addingPredicate predicate: NSPredicate, with updateBlock: @escaping ManagedObjectUpdateBlock, completion: ((Result) -> Void)?) { - managedObjectContext.perform { + managedObjectContext.performAndWait { self.lookupAll(identifier: identifier, predicate: predicate) { switch $0 { case .success(let objects): @@ -194,7 +218,7 @@ extension AlertStore { addingPredicate predicate: NSPredicate, with updateBlock: @escaping ManagedObjectUpdateBlock, completion: ((Result) -> Void)?) { - managedObjectContext.perform { + managedObjectContext.performAndWait { self.lookupLatest(identifier: identifier, predicate: predicate) { switch $0 { case .success(let object): diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index e545cc3a12..9148eeb187 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -790,6 +790,19 @@ extension DeviceDataManager: AlertIssuer { } } +// MARK: - PersistedAlertStore +extension DeviceDataManager: PersistedAlertStore { + func lookupAllUnretracted(managerIdentifier: String, completion: @escaping (Swift.Result<[PersistedAlert], Error>) -> Void) { + precondition(alertManager != nil) + alertManager.lookupAllUnretracted(managerIdentifier: managerIdentifier, completion: completion) + } + + func lookupAllUnacknowledgedUnretracted(managerIdentifier: String, completion: @escaping (Swift.Result<[PersistedAlert], Error>) -> Void) { + precondition(alertManager != nil) + alertManager.lookupAllUnacknowledgedUnretracted(managerIdentifier: managerIdentifier, completion: completion) + } +} + // MARK: - CGMManagerDelegate extension DeviceDataManager: CGMManagerDelegate { func cgmManagerWantsDeletion(_ manager: CGMManager) { diff --git a/LoopTests/Managers/Alerts/AlertManagerTests.swift b/LoopTests/Managers/Alerts/AlertManagerTests.swift index 5155e0068b..5c726e8677 100644 --- a/LoopTests/Managers/Alerts/AlertManagerTests.swift +++ b/LoopTests/Managers/Alerts/AlertManagerTests.swift @@ -108,7 +108,11 @@ class AlertManagerTests: XCTestCase { } var storedAlerts = [StoredAlert]() - override public func lookupAllUnacknowledged(completion: @escaping (Result<[StoredAlert], Error>) -> Void) { + override public func lookupAllUnacknowledgedUnretracted(managerIdentifier: String?, completion: @escaping (Result<[StoredAlert], Error>) -> Void) { + completion(.success(storedAlerts)) + } + + override public func lookupAllUnretracted(managerIdentifier: String?, completion: @escaping (Result<[StoredAlert], Error>) -> Void) { completion(.success(storedAlerts)) } } @@ -287,6 +291,58 @@ class AlertManagerTests: XCTestCase { XCTAssertEqual(alert, mockIssuer.issuedAlert) } } + + func testPersistedAlertStoreLookupAllUnretracted() throws { + mockAlertStore.managedObjectContext.performAndWait { + let date = Date.distantPast + let content = Alert.Content(title: "title", body: "body", acknowledgeActionButtonLabel: "label") + let alert = Alert(identifier: Self.mockIdentifier, + foregroundContent: content, backgroundContent: content, trigger: .repeating(repeatInterval: 60.0)) + let storedAlert = StoredAlert(from: alert, context: mockAlertStore.managedObjectContext) + storedAlert.issuedDate = date + mockAlertStore.storedAlerts = [storedAlert] + alertManager = AlertManager(alertPresenter: mockPresenter, + handlers: [mockIssuer], + userNotificationCenter: mockUserNotificationCenter, + fileManager: mockFileManager, + alertStore: mockAlertStore) + alertManager.lookupAllUnretracted(managerIdentifier: Self.mockManagerIdentifier) { result in + try? XCTAssertEqual([PersistedAlert(alert: alert, issuedDate: date, retractedDate: nil, acknowledgedDate: nil)], + try XCTUnwrap(result.successValue)) + } + } + } + + func testPersistedAlertStoreLookupAllUnacknowledgedUnretracted() throws { + mockAlertStore.managedObjectContext.performAndWait { + let date = Date.distantPast + let content = Alert.Content(title: "title", body: "body", acknowledgeActionButtonLabel: "label") + let alert = Alert(identifier: Self.mockIdentifier, + foregroundContent: content, backgroundContent: content, trigger: .repeating(repeatInterval: 60.0)) + let storedAlert = StoredAlert(from: alert, context: mockAlertStore.managedObjectContext) + storedAlert.issuedDate = date + mockAlertStore.storedAlerts = [storedAlert] + alertManager = AlertManager(alertPresenter: mockPresenter, + handlers: [mockIssuer], + userNotificationCenter: mockUserNotificationCenter, + fileManager: mockFileManager, + alertStore: mockAlertStore) + alertManager.lookupAllUnacknowledgedUnretracted(managerIdentifier: Self.mockManagerIdentifier) { result in + try? XCTAssertEqual([PersistedAlert(alert: alert, issuedDate: date, retractedDate: nil, acknowledgedDate: nil)], + try XCTUnwrap(result.successValue)) + } + } + } + +} + +extension Swift.Result { + var successValue: Success? { + switch self { + case .failure: return nil + case .success(let s): return s + } + } } class MockUserNotificationCenter: UserNotificationCenter { @@ -324,4 +380,3 @@ class MockUserNotificationCenter: UserNotificationCenter { completionHandler(pendingRequests) } } - diff --git a/LoopTests/Managers/Alerts/AlertStoreTests.swift b/LoopTests/Managers/Alerts/AlertStoreTests.swift index 229740516c..37ecfb8481 100644 --- a/LoopTests/Managers/Alerts/AlertStoreTests.swift +++ b/LoopTests/Managers/Alerts/AlertStoreTests.swift @@ -552,19 +552,19 @@ class AlertStoreTests: XCTestCase { wait(for: [expect], timeout: Self.defaultTimeout) } - func testLookupAllUnacknowledgedEmpty() { + func testLookupAllUnacknowledgedUnretractedEmpty() { let expect = self.expectation(description: #function) - alertStore.lookupAllUnacknowledged(completion: expectSuccess { alerts in + alertStore.lookupAllUnacknowledgedUnretracted(completion: expectSuccess { alerts in XCTAssertTrue(alerts.isEmpty) expect.fulfill() }) wait(for: [expect], timeout: Self.defaultTimeout) } - func testLookupAllUnacknowledgedOne() { + func testLookupAllUnacknowledgedUnretractedOne() { let expect = self.expectation(description: #function) fillWith(startDate: Self.historicDate, data: [(alert1, false, false)]) { - self.alertStore.lookupAllUnacknowledged(completion: self.expectSuccess { alerts in + self.alertStore.lookupAllUnacknowledgedUnretracted(completion: self.expectSuccess { alerts in self.assertEqual([self.alert1], alerts) expect.fulfill() }) @@ -573,10 +573,10 @@ class AlertStoreTests: XCTestCase { } - func testLookupAllUnacknowledgedOneAcknowledged() { + func testLookupAllUnacknowledgedUnretractedOneAcknowledged() { let expect = self.expectation(description: #function) fillWith(startDate: Self.historicDate, data: [(alert1, true, false)]) { - self.alertStore.lookupAllUnacknowledged(completion: self.expectSuccess { alerts in + self.alertStore.lookupAllUnacknowledgedUnretracted(completion: self.expectSuccess { alerts in self.assertEqual([], alerts) expect.fulfill() }) @@ -584,14 +584,14 @@ class AlertStoreTests: XCTestCase { wait(for: [expect], timeout: Self.defaultTimeout) } - func testLookupAllUnacknowledgedSomeNot() { + func testLookupAllUnacknowledgedUnretractedSomeNot() { let expect = self.expectation(description: #function) fillWith(startDate: Self.historicDate, data: [ (alert1, true, false), (alert2, false, false), (alert1, false, false), ]) { - self.alertStore.lookupAllUnacknowledged(completion: self.expectSuccess { alerts in + self.alertStore.lookupAllUnacknowledgedUnretracted(completion: self.expectSuccess { alerts in self.assertEqual([self.alert2, self.alert1], alerts) expect.fulfill() }) @@ -599,14 +599,14 @@ class AlertStoreTests: XCTestCase { wait(for: [expect], timeout: Self.defaultTimeout) } - func testLookupAllUnacknowledgedSomeRetracted() { + func testLookupAllUnacknowledgedUnretractedSomeRetracted() { let expect = self.expectation(description: #function) fillWith(startDate: Self.historicDate, data: [ (alert1, false, true), (alert2, false, false), (alert1, false, true) ]) { - self.alertStore.lookupAllUnacknowledged(completion: self.expectSuccess { alerts in + self.alertStore.lookupAllUnacknowledgedUnretracted(completion: self.expectSuccess { alerts in self.assertEqual([self.alert2], alerts) expect.fulfill() }) @@ -614,6 +614,68 @@ class AlertStoreTests: XCTestCase { wait(for: [expect], timeout: Self.defaultTimeout) } + func testLookupAllUnretractedEmpty() { + let expect = self.expectation(description: #function) + alertStore.lookupAllUnretracted(completion: expectSuccess { alerts in + XCTAssertTrue(alerts.isEmpty) + expect.fulfill() + }) + wait(for: [expect], timeout: Self.defaultTimeout) + } + + func testLookupAllUnretractedOne() { + let expect = self.expectation(description: #function) + fillWith(startDate: Self.historicDate, data: [(alert1, false, false)]) { + self.alertStore.lookupAllUnretracted(completion: self.expectSuccess { alerts in + self.assertEqual([self.alert1], alerts) + expect.fulfill() + }) + } + wait(for: [expect], timeout: Self.defaultTimeout) + } + + + func testLookupAllUnretractedOneAcknowledged() { + let expect = self.expectation(description: #function) + fillWith(startDate: Self.historicDate, data: [(alert1, true, false)]) { + self.alertStore.lookupAllUnretracted(completion: self.expectSuccess { alerts in + self.assertEqual([self.alert1], alerts) + expect.fulfill() + }) + } + wait(for: [expect], timeout: Self.defaultTimeout) + } + + func testLookupAllUnretractedSomeAcknowledgedSomeNot() { + let expect = self.expectation(description: #function) + fillWith(startDate: Self.historicDate, data: [ + (alert1, true, false), + (alert2, false, false), + (alert1, false, false), + ]) { + self.alertStore.lookupAllUnretracted(completion: self.expectSuccess { alerts in + self.assertEqual([self.alert1, self.alert2, self.alert1], alerts) + expect.fulfill() + }) + } + wait(for: [expect], timeout: Self.defaultTimeout) + } + + func testLookupAllUnretractedSomeRetracted() { + let expect = self.expectation(description: #function) + fillWith(startDate: Self.historicDate, data: [ + (alert1, false, true), + (alert2, false, false), + (alert1, false, true) + ]) { + self.alertStore.lookupAllUnretracted(completion: self.expectSuccess { alerts in + self.assertEqual([self.alert2], alerts) + expect.fulfill() + }) + } + wait(for: [expect], timeout: Self.defaultTimeout) + } + func testLookupAllAcknowledgedUnretractedRepeatingAlertsAll() { let expect = self.expectation(description: #function) fillWith(startDate: Self.historicDate, data: [ From 59074723c39a51db45780e6a59c40d11b63d4c98 Mon Sep 17 00:00:00 2001 From: Rick Pasetto Date: Thu, 24 Mar 2022 12:54:09 -0700 Subject: [PATCH 4/7] COASTAL-668: Fix crash when alert acknowledgement fails (#502) If alert acknowledgement fails, we display another in-app modal alert but need to do so on the main queue. https://tidepool.atlassian.net/browse/COASTAL-668 --- Loop/Managers/Alerts/AlertManager.swift | 28 ++++++++++++++----------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Loop/Managers/Alerts/AlertManager.swift b/Loop/Managers/Alerts/AlertManager.swift index 720489fe44..73ab923ea5 100644 --- a/Loop/Managers/Alerts/AlertManager.swift +++ b/Loop/Managers/Alerts/AlertManager.swift @@ -106,19 +106,23 @@ extension AlertManager: AlertManagerResponder { } func presentAcknowledgementFailedAlert(error: Error) { - let message: String - if let localizedError = error as? LocalizedError { - message = [localizedError.localizedDescription, localizedError.recoverySuggestion].compactMap({$0}).joined(separator: "\n\n") - } else { - message = String(format: NSLocalizedString("%1$@ is unable to clear the alert from your device", comment: "Message for alert shown when alert acknowledgement fails for a device, and the device does not provide a LocalizedError. (1: app name)"), Bundle.main.bundleDisplayName) + DispatchQueue.main.async { + let message: String + if let localizedError = error as? LocalizedError { + message = [localizedError.localizedDescription, localizedError.recoverySuggestion].compactMap({$0}).joined(separator: "\n\n") + } else { + message = String(format: NSLocalizedString("%1$@ is unable to clear the alert from your device", comment: "Message for alert shown when alert acknowledgement fails for a device, and the device does not provide a LocalizedError. (1: app name)"), Bundle.main.bundleDisplayName) + } + self.log.info("Alert acknowledgement failed: %{public}@", message) + + let alert = UIAlertController( + title: NSLocalizedString("Unable To Clear Alert", comment: "Title for alert shown when alert acknowledgement fails"), + message: message, + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Default action for alert when alert acknowledgment fails"), style: .default)) + + self.alertPresenter.present(alert, animated: true) } - let alert = UIAlertController( - title: NSLocalizedString("Unable To Clear Alert", comment: "Title for alert shown when alert acknowledgement fails"), - message: message, - preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Default action for alert when alert acknowledgment fails"), style: .default)) - - self.alertPresenter.present(alert, animated: true) } } From 4e7f731e2a95eb2cd8132a3a6c1f1231a90e48a2 Mon Sep 17 00:00:00 2001 From: Rick Pasetto Date: Fri, 25 Mar 2022 09:26:50 -0700 Subject: [PATCH 5/7] COASTAL-625: Relax requirement that HUDProvider needs to provide LevelHUDView (#503) LevelHUDView is for providing a view that displays a thermometer level, which not all Pumps may provide (e.g. Coastal). This relaxes the requirement and just requires returning a BaseHUDView. https://tidepool.atlassian.net/browse/COASTAL-625 --- Common/Models/PumpManagerUI.swift | 2 +- Loop Status Extension/StatusViewController.swift | 2 +- Loop/View Controllers/StatusTableViewController.swift | 2 +- LoopUI/Views/PumpStatusHUDView.swift | 4 ++-- LoopUI/Views/StatusBarHUDView.swift | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Common/Models/PumpManagerUI.swift b/Common/Models/PumpManagerUI.swift index f37fd322b6..e9250d4939 100644 --- a/Common/Models/PumpManagerUI.swift +++ b/Common/Models/PumpManagerUI.swift @@ -12,7 +12,7 @@ import LoopKitUI typealias PumpManagerHUDViewRawValue = [String: Any] -func PumpManagerHUDViewFromRawValue(_ rawValue: PumpManagerHUDViewRawValue, pluginManager: PluginManager) -> LevelHUDView? { +func PumpManagerHUDViewFromRawValue(_ rawValue: PumpManagerHUDViewRawValue, pluginManager: PluginManager) -> BaseHUDView? { guard let identifier = rawValue["managerIdentifier"] as? String, let rawState = rawValue["hudProviderView"] as? HUDProvider.HUDViewRawState, diff --git a/Loop Status Extension/StatusViewController.swift b/Loop Status Extension/StatusViewController.swift index 3cf86144f3..c54a33e0f4 100644 --- a/Loop Status Extension/StatusViewController.swift +++ b/Loop Status Extension/StatusViewController.swift @@ -223,7 +223,7 @@ class StatusViewController: UIViewController, NCWidgetProviding { } // Pump Status - let pumpManagerHUDView: LevelHUDView + let pumpManagerHUDView: BaseHUDView if let hudViewContext = context.pumpManagerHUDViewContext, let contextHUDView = PumpManagerHUDViewFromRawValue(hudViewContext.pumpManagerHUDViewRawValue, pluginManager: self.pluginManager) { diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index b08a96c128..66b75e6ded 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -1509,7 +1509,7 @@ final class StatusTableViewController: LoopChartsTableViewController { } } - private func addPumpManagerViewToHUD(_ view: LevelHUDView) { + private func addPumpManagerViewToHUD(_ view: BaseHUDView) { if let hudView = hudView { view.stateColors = .pumpStatus hudView.addPumpManagerProvidedHUDView(view) diff --git a/LoopUI/Views/PumpStatusHUDView.swift b/LoopUI/Views/PumpStatusHUDView.swift index e0aa09d299..7b6aaeb889 100644 --- a/LoopUI/Views/PumpStatusHUDView.swift +++ b/LoopUI/Views/PumpStatusHUDView.swift @@ -15,7 +15,7 @@ public final class PumpStatusHUDView: DeviceStatusHUDView, NibLoadable { @IBOutlet public weak var basalRateHUD: BasalRateHUDView! - @IBOutlet public weak var pumpManagerProvidedHUD: LevelHUDView! + @IBOutlet public weak var pumpManagerProvidedHUD: BaseHUDView! override public var orderPriority: HUDViewOrderPriority { return 3 @@ -84,7 +84,7 @@ public final class PumpStatusHUDView: DeviceStatusHUDView, NibLoadable { pumpManagerProvidedHUD.removeFromSuperview() } - public func addPumpManagerProvidedHUDView(_ pumpManagerProvidedHUD: LevelHUDView) { + public func addPumpManagerProvidedHUDView(_ pumpManagerProvidedHUD: BaseHUDView) { self.pumpManagerProvidedHUD = pumpManagerProvidedHUD statusStackView.addArrangedSubview(self.pumpManagerProvidedHUD) } diff --git a/LoopUI/Views/StatusBarHUDView.swift b/LoopUI/Views/StatusBarHUDView.swift index b060a0e178..3bd851e2e2 100644 --- a/LoopUI/Views/StatusBarHUDView.swift +++ b/LoopUI/Views/StatusBarHUDView.swift @@ -66,7 +66,7 @@ public class StatusBarHUDView: UIView, NibLoadable { pumpStatusHUD.removePumpManagerProvidedHUD() } - public func addPumpManagerProvidedHUDView(_ pumpManagerProvidedHUD: LevelHUDView) { + public func addPumpManagerProvidedHUDView(_ pumpManagerProvidedHUD: BaseHUDView) { pumpStatusHUD.addPumpManagerProvidedHUDView(pumpManagerProvidedHUD) } } From 28818516b6659810ea50c3c0b2f74203ea5c21c0 Mon Sep 17 00:00:00 2001 From: Nathaniel Hamming Date: Thu, 7 Apr 2022 09:31:57 -0300 Subject: [PATCH 6/7] [COASTAL-674] record already retracted alert and look up all alerts matching identifier (#504) * record already retracted alert and look up all alerts matching identifier * response to PR comments --- Loop/Managers/Alerts/AlertManager.swift | 23 +++++++-- Loop/Managers/Alerts/AlertStore.swift | 37 +++++++++++++- Loop/Managers/DeviceDataManager.swift | 9 ++++ .../Managers/Alerts/AlertManagerTests.swift | 51 ++++++++++++++++++- .../Managers/Alerts/AlertStoreTests.swift | 34 ++++++++++++- 5 files changed, 146 insertions(+), 8 deletions(-) diff --git a/Loop/Managers/Alerts/AlertManager.swift b/Loop/Managers/Alerts/AlertManager.swift index 73ab923ea5..edbcc82337 100644 --- a/Loop/Managers/Alerts/AlertManager.swift +++ b/Loop/Managers/Alerts/AlertManager.swift @@ -257,11 +257,20 @@ extension AlertManager { // MARK: PersistedAlertStore extension AlertManager: PersistedAlertStore { + public func doesIssuedAlertExist(identifier: Alert.Identifier, completion: @escaping (Result) -> Void) { + alertStore.lookupAllMatching(identifier: identifier) { result in + switch result { + case .success(let storedAlerts): + completion(.success(!storedAlerts.isEmpty)) + case .failure(let error): + completion(.failure(error)) + } + } + } + public func lookupAllUnretracted(managerIdentifier: String, completion: @escaping (Result<[PersistedAlert], Error>) -> Void) { alertStore.lookupAllUnretracted(managerIdentifier: managerIdentifier) { switch $0 { - case .failure(let error): - completion(.failure(error)) case .success(let alerts): do { let result = try alerts.map { @@ -276,6 +285,8 @@ extension AlertManager: PersistedAlertStore { } catch { completion(.failure(error)) } + case .failure(let error): + completion(.failure(error)) } } } @@ -283,8 +294,6 @@ extension AlertManager: PersistedAlertStore { public func lookupAllUnacknowledgedUnretracted(managerIdentifier: String, completion: @escaping (Result<[PersistedAlert], Error>) -> Void) { alertStore.lookupAllUnacknowledgedUnretracted(managerIdentifier: managerIdentifier) { switch $0 { - case .failure(let error): - completion(.failure(error)) case .success(let alerts): do { let result = try alerts.map { @@ -299,9 +308,15 @@ extension AlertManager: PersistedAlertStore { } catch { completion(.failure(error)) } + case .failure(let error): + completion(.failure(error)) } } } + + public func recordRetractedAlert(_ alert: Alert, at date: Date) { + alertStore.recordRetractedAlert(alert, at: date) + } } // MARK: Extensions diff --git a/Loop/Managers/Alerts/AlertStore.swift b/Loop/Managers/Alerts/AlertStore.swift index dc86e98e8c..ec911bd1b7 100644 --- a/Loop/Managers/Alerts/AlertStore.swift +++ b/Loop/Managers/Alerts/AlertStore.swift @@ -97,6 +97,23 @@ public class AlertStore { } } } + + public func recordRetractedAlert(_ alert: Alert, at date: Date, completion: ((Result) -> Void)? = nil) { + self.managedObjectContext.performAndWait { + let storedAlert = StoredAlert(from: alert, context: self.managedObjectContext, issuedDate: date) + storedAlert.retractedDate = date + do { + try self.managedObjectContext.save() + self.log.default("Recorded retracted alert: %{public}@", alert.identifier.value) + self.purgeExpired() + self.delegate?.alertStoreHasUpdatedAlertData(self) + completion?(.success) + } catch { + self.log.error("Could not store retracted alert: %{public}@, %{public}@", alert.identifier.value, String(describing: error)) + completion?(.failure(error)) + } + } + } public func recordAcknowledgement(of identifier: Alert.Identifier, at date: Date = Date(), completion: ((Result) -> Void)? = nil) { @@ -125,7 +142,25 @@ public class AlertStore { }, completion: completion) } - + + public func lookupAllMatching(identifier: Alert.Identifier, completion: @escaping (Result<[StoredAlert], Error>) -> Void) { + managedObjectContext.perform { + do { + let fetchRequest: NSFetchRequest = StoredAlert.fetchRequest() + let predicates = [ + NSPredicate(format: "managerIdentifier = %@", identifier.managerIdentifier), + NSPredicate(format: "alertIdentifier = %@", identifier.alertIdentifier), + ] + fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates) + fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "modificationCounter", ascending: true) ] + let result = try self.managedObjectContext.fetch(fetchRequest) + completion(.success(result)) + } catch { + completion(.failure(error)) + } + } + } + public func lookupAllUnretracted(managerIdentifier: String? = nil, completion: @escaping (Result<[StoredAlert], Error>) -> Void) { managedObjectContext.perform { do { diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 9148eeb187..81246c9e08 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -792,6 +792,10 @@ extension DeviceDataManager: AlertIssuer { // MARK: - PersistedAlertStore extension DeviceDataManager: PersistedAlertStore { + func doesIssuedAlertExist(identifier: Alert.Identifier, completion: @escaping (Swift.Result) -> Void) { + precondition(alertManager != nil) + alertManager.doesIssuedAlertExist(identifier: identifier, completion: completion) + } func lookupAllUnretracted(managerIdentifier: String, completion: @escaping (Swift.Result<[PersistedAlert], Error>) -> Void) { precondition(alertManager != nil) alertManager.lookupAllUnretracted(managerIdentifier: managerIdentifier, completion: completion) @@ -801,6 +805,11 @@ extension DeviceDataManager: PersistedAlertStore { precondition(alertManager != nil) alertManager.lookupAllUnacknowledgedUnretracted(managerIdentifier: managerIdentifier, completion: completion) } + + func recordRetractedAlert(_ alert: Alert, at date: Date) { + precondition(alertManager != nil) + alertManager.recordRetractedAlert(alert, at: date) + } } // MARK: - CGMManagerDelegate diff --git a/LoopTests/Managers/Alerts/AlertManagerTests.swift b/LoopTests/Managers/Alerts/AlertManagerTests.swift index 5c726e8677..286bed5162 100644 --- a/LoopTests/Managers/Alerts/AlertManagerTests.swift +++ b/LoopTests/Managers/Alerts/AlertManagerTests.swift @@ -88,6 +88,14 @@ class AlertManagerTests: XCTestCase { issuedAlert = alert completion?(.success) } + + var retractedAlert: Alert? + var retractedAlertDate: Date? + override public func recordRetractedAlert(_ alert: Alert, at date: Date, completion: ((Result) -> Void)? = nil) { + retractedAlert = alert + retractedAlertDate = date + completion?(.success) + } var acknowledgedAlertIdentifier: Alert.Identifier? var acknowledgedAlertDate: Date? @@ -99,7 +107,6 @@ class AlertManagerTests: XCTestCase { } var retractededAlertIdentifier: Alert.Identifier? - var retractedAlertDate: Date? override public func recordRetraction(of identifier: Alert.Identifier, at date: Date = Date(), completion: ((Result) -> Void)? = nil) { retractededAlertIdentifier = identifier @@ -334,6 +341,48 @@ class AlertManagerTests: XCTestCase { } } + func testPersistedAlertStoreDoesIssuedAlertExist() throws { + mockAlertStore.managedObjectContext.performAndWait { + let date = Date.distantPast + let content = Alert.Content(title: "title", body: "body", acknowledgeActionButtonLabel: "label") + let alert = Alert(identifier: Self.mockIdentifier, + foregroundContent: content, backgroundContent: content, trigger: .repeating(repeatInterval: 60.0)) + let storedAlert = StoredAlert(from: alert, context: mockAlertStore.managedObjectContext) + storedAlert.issuedDate = date + mockAlertStore.storedAlerts = [storedAlert] + alertManager = AlertManager(alertPresenter: mockPresenter, + handlers: [mockIssuer], + userNotificationCenter: mockUserNotificationCenter, + fileManager: mockFileManager, + alertStore: mockAlertStore) + let identifierExists = Self.mockIdentifier + let identifierDoesNotExist = Alert.Identifier(managerIdentifier: "TestManagerIdentifier", alertIdentifier: "TestAlertIdentifier") + alertManager.doesIssuedAlertExist(identifier: identifierExists) { result in + try? XCTAssertEqual(true, try XCTUnwrap(result.successValue)) + } + alertManager.doesIssuedAlertExist(identifier: identifierDoesNotExist) { result in + try? XCTAssertEqual(false, try XCTUnwrap(result.successValue)) + } + } + } + + func testReportRetractedAlert() throws { + mockAlertStore.managedObjectContext.performAndWait { + let content = Alert.Content(title: "title", body: "body", acknowledgeActionButtonLabel: "label") + let alert = Alert(identifier: Self.mockIdentifier, + foregroundContent: content, backgroundContent: content, trigger: .repeating(repeatInterval: 60.0)) + mockAlertStore.storedAlerts = [] + alertManager = AlertManager(alertPresenter: mockPresenter, + handlers: [mockIssuer], + userNotificationCenter: mockUserNotificationCenter, + fileManager: mockFileManager, + alertStore: mockAlertStore) + let now = Date() + alertManager.recordRetractedAlert(alert, at: now) + XCTAssertEqual(mockAlertStore.retractedAlert, alert) + XCTAssertEqual(mockAlertStore.retractedAlertDate, now) + } + } } extension Swift.Result { diff --git a/LoopTests/Managers/Alerts/AlertStoreTests.swift b/LoopTests/Managers/Alerts/AlertStoreTests.swift index 37ecfb8481..15cbab81ac 100644 --- a/LoopTests/Managers/Alerts/AlertStoreTests.swift +++ b/LoopTests/Managers/Alerts/AlertStoreTests.swift @@ -337,7 +337,23 @@ class AlertStoreTests: XCTestCase { }) wait(for: [expect], timeout: Self.defaultTimeout) } - + + func testRecordRetractedAlert() { + let expect = self.expectation(description: #function) + let alertDate = Self.historicDate + alertStore.recordRetractedAlert(alert1, at: alertDate, completion: self.expectSuccess { + self.alertStore.fetch(identifier: Self.identifier1, completion: self.expectSuccess { storedAlerts in + XCTAssertEqual(1, storedAlerts.count) + XCTAssertEqual(Self.identifier1, storedAlerts.first?.identifier) + XCTAssertEqual(alertDate, storedAlerts.first?.issuedDate) + XCTAssertNil(storedAlerts.first?.acknowledgedDate) + XCTAssertEqual(alertDate, storedAlerts.first?.retractedDate) + expect.fulfill() + }) + }) + wait(for: [expect], timeout: Self.defaultTimeout) + } + func testEmptyQuery() { let expect = self.expectation(description: #function) alertStore.recordIssued(alert: alert1, at: Self.historicDate, completion: self.expectSuccess { @@ -715,7 +731,21 @@ class AlertStoreTests: XCTestCase { } wait(for: [expect], timeout: Self.defaultTimeout) } - + + func testLookUpAllMatching() { + let expect = self.expectation(description: #function) + fillWith(startDate: Self.historicDate, data: [ + (alert1, true, false), + (repeatingAlert, true, false) + ]) { + self.alertStore.lookupAllMatching(identifier: AlertStoreTests.repeatingAlertIdentifier, completion: self.expectSuccess { alerts in + XCTAssertEqual(alerts.count, 1) + self.assertEqual([self.repeatingAlert], alerts) + expect.fulfill() + }) + } + wait(for: [expect], timeout: Self.defaultTimeout) + } private func fillWith(startDate: Date, data: [(alert: Alert, acknowledged: Bool, retracted: Bool)], _ completion: @escaping () -> Void) { let increment = 1.0 From 4e8b7a31ce57673a08928c529357853c18571379 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 11 Apr 2022 11:18:33 -0500 Subject: [PATCH 7/7] Fix merge issues --- Loop/Managers/LoopDataManager.swift | 1 - LoopTests/Managers/LoopDataManagerTests.swift | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index d2290124a1..2229aeb2a7 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -180,7 +180,6 @@ final class LoopDataManager: LoopSettingsAlerterDelegate { self.automaticDosingStatus.$isClosedLoop .dropFirst() .removeDuplicates() - .dropFirst() .receive(on: DispatchQueue.main) .sink { if !$0 { self.mutateSettings { settings in diff --git a/LoopTests/Managers/LoopDataManagerTests.swift b/LoopTests/Managers/LoopDataManagerTests.swift index 4d3b15a5ee..76afa6955a 100644 --- a/LoopTests/Managers/LoopDataManagerTests.swift +++ b/LoopTests/Managers/LoopDataManagerTests.swift @@ -534,14 +534,13 @@ class LoopDataManagerDosingTests: XCTestCase { basalDeliveryState: .tempBasal(existingTempBasal), settings: settings, overrideHistory: TemporaryScheduleOverrideHistory(), - lastPumpEventsReconciliation: nil, // this date is only used to init the doseStore if a DoseStoreProtocol isn't passed in, so this date can be nil analyticsServicesManager: AnalyticsServicesManager(), localCacheDuration: .days(1), doseStore: doseStore, glucoseStore: glucoseStore, carbStore: carbStore, dosingDecisionStore: dosingDecisionStore, - settingsStore: MockSettingsStore(), + latestStoredSettingsProvider: MockLatestStoredSettingsProvider(), now: { currentDate }, pumpInsulinType: .novolog, automaticDosingStatus: automaticDosingStatus