Skip to content

Commit 8dc157c

Browse files
authored
LOOP-1200: Persist Alerts to CoreData (#89)
* LOOP-1200: Persist DeviceAlerts to CoreData Also: Squash removePendingAlert and removeDeliveredAlert into retractAlert * PR Feedback: Renaming * checkpoint * Checkpoint: back to codegen'd CoreData object (seems to work) * Remove bits about expiration until we know how/when we want to do it. * Shuffled some code around a little. * oops these changes weren't staged. * Add unit tests, remove query support (for another PR) * PR Feedback, more unit tests * PR feedback * Add a log line for when an alert is not found to update * PR Feedback: "flatten" Trigger so that it is queryable Stored trigger type as an Int16, trigger interval as an optional Double (NSNumber). * Add query using modification counter (#92) * Add query using modification counter * PR Feedback * LOOP-1200: Dump Alert database into Issue Report (#93) * LOOP-1200: Dump Alert database into Issue Report Added query by date, plus unit tests. * PR Feedback, plus some rework/refactor This takes @darin's suggestion, with some modifications :). Now, the Filter is part of the Anchor, which makes it so anchoring continues to apply the filter as appropriate. * LOOP-1200: alert persistence followups (#94) * Add query using modification counter * PR Feedback * LOOP-1200: Dump Alert database into Issue Report Added query by date, plus unit tests. * PR Feedback, plus some rework/refactor This takes @darin's suggestion, with some modifications :). Now, the Filter is part of the Anchor, which makes it so anchoring continues to apply the filter as appropriate. * LOOP-1200: Split alertIdentifier and managerIdentifier After talking with Paul, we determined that we will need to be able to either filter or group alerts by device, so it would be better to keep managerIdentifier separate now so it can be used in data queries.
1 parent dafbfda commit 8dc157c

14 files changed

+936
-135
lines changed

Loop.xcodeproj/project.pbxproj

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@
2222
/* End PBXAggregateTarget section */
2323

2424
/* Begin PBXBuildFile section */
25+
1D05219B2469E9DF000EBBDE /* StoredAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D05219A2469E9DF000EBBDE /* StoredAlert.swift */; };
26+
1D05219D2469F1F5000EBBDE /* AlertStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D05219C2469F1F5000EBBDE /* AlertStore.swift */; };
27+
1D080CBD2473214A00356610 /* AlertStore.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 1D080CBB2473214A00356610 /* AlertStore.xcdatamodeld */; };
28+
1D4A3E2D2478628500FD601B /* StoredAlert+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4A3E2B2478628500FD601B /* StoredAlert+CoreDataClass.swift */; };
29+
1D4A3E2E2478628500FD601B /* StoredAlert+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D4A3E2C2478628500FD601B /* StoredAlert+CoreDataProperties.swift */; };
30+
1D80313D24746274002810DF /* AlertStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D80313C24746274002810DF /* AlertStoreTests.swift */; };
2531
1DA649A7244126CD00F61E75 /* UserNotificationDeviceAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA649A6244126CD00F61E75 /* UserNotificationDeviceAlertPresenter.swift */; };
2632
1DA649A9244126DA00F61E75 /* InAppModalDeviceAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA649A8244126DA00F61E75 /* InAppModalDeviceAlertPresenter.swift */; };
2733
1DA7A84224476EAD008257F0 /* DeviceAlertManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA7A84124476EAD008257F0 /* DeviceAlertManagerTests.swift */; };
@@ -612,6 +618,12 @@
612618
/* End PBXCopyFilesBuildPhase section */
613619

614620
/* Begin PBXFileReference section */
621+
1D05219A2469E9DF000EBBDE /* StoredAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredAlert.swift; sourceTree = "<group>"; };
622+
1D05219C2469F1F5000EBBDE /* AlertStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertStore.swift; sourceTree = "<group>"; };
623+
1D080CBC2473214A00356610 /* AlertStore.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = AlertStore.xcdatamodel; sourceTree = "<group>"; };
624+
1D4A3E2B2478628500FD601B /* StoredAlert+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoredAlert+CoreDataClass.swift"; sourceTree = "<group>"; };
625+
1D4A3E2C2478628500FD601B /* StoredAlert+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StoredAlert+CoreDataProperties.swift"; sourceTree = "<group>"; };
626+
1D80313C24746274002810DF /* AlertStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertStoreTests.swift; sourceTree = "<group>"; };
615627
1DA649A6244126CD00F61E75 /* UserNotificationDeviceAlertPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserNotificationDeviceAlertPresenter.swift; sourceTree = "<group>"; };
616628
1DA649A8244126DA00F61E75 /* InAppModalDeviceAlertPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppModalDeviceAlertPresenter.swift; sourceTree = "<group>"; };
617629
1DA7A84124476EAD008257F0 /* DeviceAlertManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceAlertManagerTests.swift; sourceTree = "<group>"; };
@@ -1246,32 +1258,38 @@
12461258
/* End PBXFrameworksBuildPhase section */
12471259

12481260
/* Begin PBXGroup section */
1249-
1DA6499D2441266400F61E75 /* DeviceAlert */ = {
1261+
1DA6499D2441266400F61E75 /* Alerts */ = {
12501262
isa = PBXGroup;
12511263
children = (
1264+
1D4A3E2B2478628500FD601B /* StoredAlert+CoreDataClass.swift */,
1265+
1D4A3E2C2478628500FD601B /* StoredAlert+CoreDataProperties.swift */,
1266+
1D080CBB2473214A00356610 /* AlertStore.xcdatamodeld */,
1267+
1D05219C2469F1F5000EBBDE /* AlertStore.swift */,
12521268
1DB1065024467E18005542BD /* DeviceAlertManager.swift */,
12531269
1DA649A8244126DA00F61E75 /* InAppModalDeviceAlertPresenter.swift */,
1270+
1D05219A2469E9DF000EBBDE /* StoredAlert.swift */,
12541271
1DA649A6244126CD00F61E75 /* UserNotificationDeviceAlertPresenter.swift */,
12551272
);
1256-
path = DeviceAlert;
1273+
path = Alerts;
12571274
sourceTree = "<group>";
12581275
};
12591276
1DA7A83F24476E8C008257F0 /* Managers */ = {
12601277
isa = PBXGroup;
12611278
children = (
1262-
1DA7A84024476E98008257F0 /* DeviceAlert */,
1279+
1DA7A84024476E98008257F0 /* Alerts */,
12631280
);
12641281
path = Managers;
12651282
sourceTree = "<group>";
12661283
};
1267-
1DA7A84024476E98008257F0 /* DeviceAlert */ = {
1284+
1DA7A84024476E98008257F0 /* Alerts */ = {
12681285
isa = PBXGroup;
12691286
children = (
1287+
1D80313C24746274002810DF /* AlertStoreTests.swift */,
12701288
1DA7A84124476EAD008257F0 /* DeviceAlertManagerTests.swift */,
12711289
1DA7A84324477698008257F0 /* InAppModalDeviceAlertPresenterTests.swift */,
12721290
1DFE9E162447B6270082C280 /* UserNotificationDeviceAlertPresenterTests.swift */,
12731291
);
1274-
path = DeviceAlert;
1292+
path = Alerts;
12751293
sourceTree = "<group>";
12761294
};
12771295
4328E0121CFBE1B700E199AA /* Controllers */ = {
@@ -1681,7 +1699,7 @@
16811699
43F5C2E41B93C5D4003EB13D /* Managers */ = {
16821700
isa = PBXGroup;
16831701
children = (
1684-
1DA6499D2441266400F61E75 /* DeviceAlert */,
1702+
1DA6499D2441266400F61E75 /* Alerts */,
16851703
439897361CD2F80600223065 /* AnalyticsServicesManager.swift */,
16861704
439BED291E76093C00B0AED5 /* CGMManager.swift */,
16871705
43DBF0521C93EC8200B3C386 /* DeviceDataManager.swift */,
@@ -2751,6 +2769,7 @@
27512769
4372E48B213CB5F00068E043 /* Double.swift in Sources */,
27522770
430B29932041F5B300BA9F93 /* UserDefaults+Loop.swift in Sources */,
27532771
43CE7CDE1CA8B63E003CC1B0 /* Data.swift in Sources */,
2772+
1D080CBD2473214A00356610 /* AlertStore.xcdatamodeld in Sources */,
27542773
A999D40A24663DC7004C89D4 /* CarbStore.swift in Sources */,
27552774
C1F8B243223E73FD00DD66CF /* BolusProgressTableViewCell.swift in Sources */,
27562775
89D6953E23B6DF8A002B3066 /* PotentialCarbEntryTableViewCell.swift in Sources */,
@@ -2777,6 +2796,7 @@
27772796
4346D1E71C77F5FE00ABAFE3 /* ChartTableViewCell.swift in Sources */,
27782797
437CEEE41CDE5C0A003C8C80 /* UIImage.swift in Sources */,
27792798
C1201E2C23ECDBD0002DA84A /* WatchContextRequestUserInfo.swift in Sources */,
2799+
1D4A3E2D2478628500FD601B /* StoredAlert+CoreDataClass.swift in Sources */,
27802800
892ADE082446E1C2007CE08C /* SuspendThresholdEditor.swift in Sources */,
27812801
892D7C5123B54A15008A9656 /* CarbEntryViewController.swift in Sources */,
27822802
A999D40424663CE1004C89D4 /* DoseStore.swift in Sources */,
@@ -2802,6 +2822,7 @@
28022822
4F6663941E905FD2009E74FC /* ChartColorPalette+Loop.swift in Sources */,
28032823
4328E0351CFC0AE100E199AA /* WatchDataManager.swift in Sources */,
28042824
4345E3FC21F04911009E00E5 /* UIColor+HIG.swift in Sources */,
2825+
1D4A3E2E2478628500FD601B /* StoredAlert+CoreDataProperties.swift in Sources */,
28052826
43D381621EBD9759007F8C8F /* HeaderValuesTableViewCell.swift in Sources */,
28062827
A9C62D892331703100535612 /* LoggingServicesManager.swift in Sources */,
28072828
89E267FF229267DF00A3F2AF /* Optional.swift in Sources */,
@@ -2836,13 +2857,15 @@
28362857
89CA2B3D226E6B13004D9350 /* LocalTestingScenariosManager.swift in Sources */,
28372858
43F78D261C8FC000002152D1 /* DoseMath.swift in Sources */,
28382859
43511CE221FD80E400566C63 /* RetrospectiveCorrection.swift in Sources */,
2860+
1D05219B2469E9DF000EBBDE /* StoredAlert.swift in Sources */,
28392861
438D42F91D7C88BC003244B0 /* PredictionInputEffect.swift in Sources */,
28402862
A9C62D8223316FF600535612 /* UserDefaults+Services.swift in Sources */,
28412863
892A5D692230C41D008961AB /* RangeReplaceableCollection.swift in Sources */,
28422864
4F70C2101DE8FAC5006380B7 /* StatusExtensionDataManager.swift in Sources */,
28432865
43DFB62320D4CAE7008A7BAE /* PumpManager.swift in Sources */,
28442866
892A5D59222F0A27008961AB /* Debug.swift in Sources */,
28452867
431A8C401EC6E8AB00823B9C /* CircleMaskView.swift in Sources */,
2868+
1D05219D2469F1F5000EBBDE /* AlertStore.swift in Sources */,
28462869
439897371CD2F80600223065 /* AnalyticsServicesManager.swift in Sources */,
28472870
A9C62D842331700E00535612 /* DiagnosticLog+Subsystem.swift in Sources */,
28482871
895FE0952201234000FCF18A /* OverrideSelectionViewController.swift in Sources */,
@@ -3036,6 +3059,7 @@
30363059
isa = PBXSourcesBuildPhase;
30373060
buildActionMask = 2147483647;
30383061
files = (
3062+
1D80313D24746274002810DF /* AlertStoreTests.swift in Sources */,
30393063
A9A63F8E246B271600588D5B /* NSTimeInterval.swift in Sources */,
30403064
A9A63F8D246B261100588D5B /* DosingDecisionStoreTests.swift in Sources */,
30413065
A9E6DFEF246A0474005B1A1C /* LoopErrorTests.swift in Sources */,
@@ -4201,6 +4225,19 @@
42014225
defaultConfigurationName = Release;
42024226
};
42034227
/* End XCConfigurationList section */
4228+
4229+
/* Begin XCVersionGroup section */
4230+
1D080CBB2473214A00356610 /* AlertStore.xcdatamodeld */ = {
4231+
isa = XCVersionGroup;
4232+
children = (
4233+
1D080CBC2473214A00356610 /* AlertStore.xcdatamodel */,
4234+
);
4235+
currentVersion = 1D080CBC2473214A00356610 /* AlertStore.xcdatamodel */;
4236+
path = AlertStore.xcdatamodeld;
4237+
sourceTree = "<group>";
4238+
versionGroupType = wrapper.xcdatamodel;
4239+
};
4240+
/* End XCVersionGroup section */
42044241
};
42054242
rootObject = 43776F841B8022E90074EA36 /* Project object */;
42064243
}
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
//
2+
// AlertStore.swift
3+
// Loop
4+
//
5+
// Created by Rick Pasetto on 5/11/20.
6+
// Copyright © 2020 LoopKit Authors. All rights reserved.
7+
//
8+
9+
import CoreData
10+
import LoopKit
11+
12+
public class AlertStore {
13+
14+
public enum AlertStoreError: Error {
15+
case notFound
16+
}
17+
18+
// Available for tests only
19+
let managedObjectContext: NSManagedObjectContext
20+
21+
private let persistentContainer: NSPersistentContainer
22+
23+
private let log = DiagnosticLog(category: "AlertStore")
24+
25+
public init(storageFileURL: URL? = nil) {
26+
managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
27+
managedObjectContext.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy
28+
managedObjectContext.automaticallyMergesChangesFromParent = true
29+
30+
let storeDescription = NSPersistentStoreDescription()
31+
if let storageFileURL = storageFileURL {
32+
storeDescription.url = storageFileURL
33+
} else {
34+
storeDescription.type = NSInMemoryStoreType
35+
}
36+
storeDescription.shouldMigrateStoreAutomatically = true
37+
storeDescription.shouldInferMappingModelAutomatically = true
38+
persistentContainer = NSPersistentContainer(name: "AlertStore")
39+
persistentContainer.persistentStoreDescriptions = [storeDescription]
40+
persistentContainer.loadPersistentStores { _, error in
41+
if let error = error {
42+
fatalError("Unable to load persistent stores: \(error)")
43+
}
44+
}
45+
managedObjectContext.persistentStoreCoordinator = persistentContainer.persistentStoreCoordinator
46+
}
47+
}
48+
49+
// MARK: Alert Recording
50+
51+
extension AlertStore {
52+
53+
public func recordIssued(alert: DeviceAlert, at date: Date = Date(), completion: ((Result<Void, Error>) -> Void)? = nil) {
54+
self.managedObjectContext.perform {
55+
_ = StoredAlert(from: alert, context: self.managedObjectContext, issuedDate: date)
56+
do {
57+
try self.managedObjectContext.save()
58+
self.log.default("Recorded alert: %{public}@", alert.identifier.value)
59+
completion?(.success)
60+
} catch {
61+
self.log.error("Could not store alert: %{public}@, %{public}@", alert.identifier.value, String(describing: error))
62+
completion?(.failure(error))
63+
}
64+
}
65+
}
66+
67+
public func recordAcknowledgement(of identifier: DeviceAlert.Identifier, at date: Date = Date(),
68+
completion: ((Result<Void, Error>) -> Void)? = nil) {
69+
recordUpdateOfLatest(of: identifier, with: { $0.acknowledgedDate = date }, completion: completion)
70+
}
71+
72+
public func recordRetraction(of identifier: DeviceAlert.Identifier, at date: Date = Date(),
73+
completion: ((Result<Void, Error>) -> Void)? = nil) {
74+
recordUpdateOfLatest(of: identifier, with: { $0.retractedDate = date }, completion: completion)
75+
}
76+
77+
private func recordUpdateOfLatest(of identifier: DeviceAlert.Identifier,
78+
with block: @escaping (StoredAlert) -> Void,
79+
completion: ((Result<Void, Error>) -> Void)?) {
80+
self.managedObjectContext.perform {
81+
self.lookupLatest(identifier: identifier) {
82+
switch $0 {
83+
case .success(let object):
84+
if let object = object {
85+
block(object)
86+
do {
87+
try self.managedObjectContext.save()
88+
self.log.default("Recorded alert: %{public}@", identifier.value)
89+
completion?(.success)
90+
} catch {
91+
self.log.error("Could not store alert: %{public}@, %{public}@", identifier.value, String(describing: error))
92+
completion?(.failure(error))
93+
}
94+
} else {
95+
self.log.default("Alert not found for update: %{public}@", identifier.value)
96+
completion?(.failure(AlertStoreError.notFound))
97+
}
98+
case .failure(let error):
99+
completion?(.failure(error))
100+
}
101+
}
102+
}
103+
}
104+
105+
private func lookupLatest(identifier: DeviceAlert.Identifier, completion: @escaping (Result<StoredAlert?, Error>) -> Void) {
106+
managedObjectContext.perform {
107+
do {
108+
let fetchRequest: NSFetchRequest<StoredAlert> = StoredAlert.fetchRequest()
109+
fetchRequest.predicate = identifier.equalsPredicate
110+
fetchRequest.sortDescriptors = [ NSSortDescriptor(key: "modificationCounter", ascending: false) ]
111+
fetchRequest.fetchLimit = 1
112+
let result = try self.managedObjectContext.fetch(fetchRequest)
113+
completion(.success(result.last))
114+
} catch {
115+
completion(.failure(error))
116+
}
117+
}
118+
}
119+
120+
}
121+
122+
// MARK: Query Support
123+
124+
public protocol QueryFilter: Equatable {
125+
var predicate: NSPredicate? { get }
126+
}
127+
128+
extension AlertStore {
129+
130+
public struct QueryAnchor<Filter: QueryFilter>: RawRepresentable, Equatable {
131+
public typealias RawValue = [String: Any]
132+
internal var modificationCounter: Int64
133+
internal var filter: Filter?
134+
public init() {
135+
self.modificationCounter = 0
136+
}
137+
init(modificationCounter: Int64? = nil, filter: Filter?) {
138+
self.modificationCounter = modificationCounter ?? 0
139+
self.filter = filter
140+
}
141+
public init?(rawValue: RawValue) {
142+
guard let modificationCounter = rawValue["modificationCounter"] as? Int64 else {
143+
return nil
144+
}
145+
self.modificationCounter = modificationCounter
146+
}
147+
public var rawValue: RawValue {
148+
var rawValue: RawValue = [:]
149+
rawValue["modificationCounter"] = modificationCounter
150+
return rawValue
151+
}
152+
}
153+
typealias QueryResult<Filter: QueryFilter> = Result<(QueryAnchor<Filter>, [StoredAlert]), Error>
154+
155+
struct NoFilter: QueryFilter {
156+
let predicate: NSPredicate?
157+
}
158+
struct SinceDateFilter: QueryFilter {
159+
let date: Date
160+
var predicate: NSPredicate? { NSPredicate(format: "issuedDate >= %@", date as NSDate) }
161+
}
162+
163+
func executeQuery(since date: Date, limit: Int, completion: @escaping (QueryResult<SinceDateFilter>) -> Void) {
164+
executeAlertQuery(from: QueryAnchor(filter: SinceDateFilter(date: date)), limit: limit, completion: completion)
165+
}
166+
167+
func continueQuery<Filter: QueryFilter>(from anchor: QueryAnchor<Filter>, limit: Int, completion: @escaping (QueryResult<Filter>) -> Void) {
168+
executeAlertQuery(from: anchor, limit: limit, completion: completion)
169+
}
170+
171+
private func executeAlertQuery<Filter: QueryFilter>(from anchor: QueryAnchor<Filter>, limit: Int, completion: @escaping (QueryResult<Filter>) -> Void) {
172+
self.managedObjectContext.perform {
173+
guard limit > 0 else {
174+
completion(.success((anchor, [])))
175+
return
176+
}
177+
let storedRequest: NSFetchRequest<StoredAlert> = StoredAlert.fetchRequest()
178+
let anchorPredicate = NSPredicate(format: "modificationCounter > %d", anchor.modificationCounter)
179+
if let filterPredicate = anchor.filter?.predicate {
180+
storedRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
181+
anchorPredicate,
182+
filterPredicate
183+
])
184+
} else {
185+
storedRequest.predicate = anchorPredicate
186+
}
187+
storedRequest.sortDescriptors = [NSSortDescriptor(key: "modificationCounter", ascending: true)]
188+
storedRequest.fetchLimit = limit
189+
190+
do {
191+
let stored = try self.managedObjectContext.fetch(storedRequest)
192+
let modificationCounter = stored.max(by: { $0.modificationCounter < $1.modificationCounter })?.modificationCounter
193+
let newAnchor = QueryAnchor<Filter>(modificationCounter: modificationCounter, filter: anchor.filter)
194+
completion(.success((newAnchor, stored)))
195+
} catch let error {
196+
completion(.failure(error))
197+
}
198+
}
199+
}
200+
201+
// At the moment, this is only used for unit testing
202+
internal func fetch(identifier: DeviceAlert.Identifier, completion: @escaping (Result<[StoredAlert], Error>) -> Void) {
203+
self.managedObjectContext.perform {
204+
let storedRequest: NSFetchRequest<StoredAlert> = StoredAlert.fetchRequest()
205+
storedRequest.predicate = identifier.equalsPredicate
206+
do {
207+
let stored = try self.managedObjectContext.fetch(storedRequest)
208+
completion(.success(stored))
209+
} catch {
210+
completion(.failure(error))
211+
}
212+
}
213+
}
214+
}
215+
216+
extension DeviceAlert.Identifier {
217+
var equalsPredicate: NSPredicate {
218+
return NSCompoundPredicate(andPredicateWithSubpredicates: [
219+
NSPredicate(format: "managerIdentifier == %@", managerIdentifier),
220+
NSPredicate(format: "alertIdentifier == %@", alertIdentifier)
221+
])
222+
}
223+
}
224+
225+
extension Result where Success == Void {
226+
static var success: Result {
227+
return Result.success(Void())
228+
}
229+
}

0 commit comments

Comments
 (0)