@@ -13,6 +13,19 @@ protocol DeviceAlertManagerResponder: class {
1313 func acknowledgeDeviceAlert( identifier: DeviceAlert . Identifier )
1414}
1515
16+ public protocol UserNotificationCenter {
17+ func add( _ request: UNNotificationRequest , withCompletionHandler: ( ( Error ? ) -> Void ) ? )
18+ func removePendingNotificationRequests( withIdentifiers: [ String ] )
19+ func removeDeliveredNotifications( withIdentifiers: [ String ] )
20+ func getDeliveredNotifications( completionHandler: @escaping ( [ UNNotification ] ) -> Void )
21+ func getPendingNotificationRequests( completionHandler: @escaping ( [ UNNotificationRequest ] ) -> Void )
22+ }
23+ extension UNUserNotificationCenter : UserNotificationCenter { }
24+
25+ public enum DeviceAlertUserNotificationUserInfoKey : String {
26+ case deviceAlert, deviceAlertTimestamp
27+ }
28+
1629/// Main (singleton-ish) class that is responsible for:
1730/// - managing the different targets (handlers) that will post alerts
1831/// - managing the different responders that might acknowledge the alert
@@ -21,17 +34,28 @@ protocol DeviceAlertManagerResponder: class {
2134public final class DeviceAlertManager {
2235 static let soundsDirectoryURL = FileManager . default. urls ( for: . libraryDirectory, in: . userDomainMask) . last!. appendingPathComponent ( " Sounds " )
2336
37+ static let timestampFormatter = ISO8601DateFormatter ( )
38+
2439 private let log = DiagnosticLog ( category: " DeviceAlertManager " )
2540
2641 var handlers : [ DeviceAlertPresenter ] = [ ]
2742 var responders : [ String : Weak < DeviceAlertResponder > ] = [ : ]
2843 var soundVendors : [ String : Weak < DeviceAlertSoundVendor > ] = [ : ]
44+
45+ let userNotificationCenter : UserNotificationCenter
46+ let fileManager : FileManager
2947
3048 public init ( rootViewController: UIViewController ,
31- handlers: [ DeviceAlertPresenter ] ? = nil ) {
49+ handlers: [ DeviceAlertPresenter ] ? = nil ,
50+ userNotificationCenter: UserNotificationCenter = UNUserNotificationCenter . current ( ) ,
51+ fileManager: FileManager = FileManager . default) {
52+ self . userNotificationCenter = userNotificationCenter
53+ self . fileManager = fileManager
3254 self . handlers = handlers ??
33- [ UserNotificationDeviceAlertPresenter ( ) ,
55+ [ UserNotificationDeviceAlertPresenter ( userNotificationCenter : userNotificationCenter ) ,
3456 InAppModalDeviceAlertPresenter ( rootViewController: rootViewController, deviceAlertManagerResponder: self ) ]
57+
58+ playbackPersistedAlerts ( )
3559 }
3660
3761 public func addAlertResponder( managerIdentifier: String , alertResponder: DeviceAlertResponder ) {
@@ -57,6 +81,10 @@ extension DeviceAlertManager: DeviceAlertManagerResponder {
5781 if let responder = responders [ identifier. managerIdentifier] ? . value {
5882 responder. acknowledgeAlert ( alertIdentifier: identifier. alertIdentifier)
5983 }
84+ // Also clear the alert from the NotificationCenter
85+ log. debug ( " Removing notification %@ from delivered & pending notifications " , identifier. value)
86+ userNotificationCenter. removeDeliveredNotifications ( withIdentifiers: [ identifier. value] )
87+ userNotificationCenter. removePendingNotificationRequests ( withIdentifiers: [ identifier. value] )
6088 }
6189}
6290
@@ -93,8 +121,7 @@ extension DeviceAlertManager {
93121 return
94122 }
95123 do {
96- let fileManager = FileManager . default
97- try fileManager. createDirectory ( atPath: DeviceAlertManager . soundsDirectoryURL. path, withIntermediateDirectories: true , attributes: nil )
124+ try fileManager. createDirectory ( at: DeviceAlertManager . soundsDirectoryURL, withIntermediateDirectories: true , attributes: nil )
98125 for sound in sounds {
99126 if let fromFilename = sound. filename,
100127 let toURL = DeviceAlertManager . soundURL ( managerIdentifier: managerIdentifier, sound: sound) {
@@ -112,8 +139,8 @@ extension FileManager {
112139 func copyIfNewer( from fromURL: URL , to toURL: URL ) throws {
113140 if fileExists ( atPath: toURL. path) {
114141 // If the source file is newer, remove the old one, otherwise skip it.
115- let toCreationDate = try toURL. fileCreationDate ( )
116- let fromCreationDate = try fromURL. fileCreationDate ( )
142+ let toCreationDate = try toURL. fileCreationDate ( self )
143+ let fromCreationDate = try fromURL. fileCreationDate ( self )
117144 if fromCreationDate > toCreationDate {
118145 try removeItem ( at: toURL)
119146 } else {
@@ -126,7 +153,159 @@ extension FileManager {
126153
127154extension URL {
128155
129- func fileCreationDate( ) throws -> Date {
130- return try FileManager . default. attributesOfItem ( atPath: self . path) [ . creationDate] as! Date
156+ func fileCreationDate( _ fileManager: FileManager ) throws -> Date {
157+ return try fileManager. attributesOfItem ( atPath: self . path) [ . creationDate] as! Date
158+ }
159+ }
160+
161+
162+ extension DeviceAlertManager {
163+
164+ private func playbackPersistedAlerts( ) {
165+
166+ userNotificationCenter. getDeliveredNotifications {
167+ $0. forEach { notification in
168+ self . log. debug ( " Delivered alert: %@ " , " \( notification) " )
169+ self . playbackDeliveredNotification ( notification)
170+ }
171+ }
172+
173+ userNotificationCenter. getPendingNotificationRequests {
174+ $0. forEach { request in
175+ self . log. debug ( " Pending alert: %@ " , " \( request) " )
176+ self . playbackPendingNotificationRequest ( request)
177+ }
178+ }
179+ }
180+
181+ private func playbackDeliveredNotification( _ notification: UNNotification ) {
182+ // Assume if it was delivered, the trigger should be .immediate.
183+ playbackAnyNotificationRequest ( notification. request, usingTrigger: . immediate)
184+ }
185+
186+ private func playbackPendingNotificationRequest( _ request: UNNotificationRequest ) {
187+ playbackAnyNotificationRequest ( request)
188+ }
189+
190+ private func playbackAnyNotificationRequest( _ request: UNNotificationRequest , usingTrigger trigger: DeviceAlert . Trigger ? = nil ) {
191+ guard let savedAlertString = request. content. userInfo [ DeviceAlertUserNotificationUserInfoKey . deviceAlert. rawValue] as? String ,
192+ let savedAlertTimestampString = request. content. userInfo [ DeviceAlertUserNotificationUserInfoKey . deviceAlertTimestamp. rawValue] as? String ,
193+ let savedAlertTimestamp = DeviceAlertManager . timestampFormatter. date ( from: savedAlertTimestampString) else {
194+ self . log. error ( " Could not find persistent alert in notification " )
195+ return
196+ }
197+ do {
198+ let savedAlert = try DeviceAlert . decode ( from: savedAlertString)
199+ let newTrigger = trigger ?? determineNewTrigger ( from: savedAlert, timestamp: savedAlertTimestamp)
200+ let newAlert = DeviceAlert ( identifier: savedAlert. identifier,
201+ foregroundContent: savedAlert. foregroundContent,
202+ backgroundContent: savedAlert. backgroundContent,
203+ trigger: newTrigger,
204+ sound: savedAlert. sound)
205+ self . log. debug ( " Replaying %@Alert: %@ with %@trigger %@ " ,
206+ trigger != nil ? " " : " Pending " ,
207+ trigger != nil ? " " : " new " ,
208+ savedAlertString, " \( newTrigger) " )
209+ self . issueAlert ( newAlert)
210+ } catch {
211+ self . log. error ( " Could not decode alert: error %@, from %@ " , error. localizedDescription, savedAlertString)
212+ }
213+ }
214+
215+ private func determineNewTrigger( from alert: DeviceAlert , timestamp: Date ) -> DeviceAlert . Trigger {
216+ switch alert. trigger {
217+ case . immediate:
218+ return alert. trigger
219+ case . delayed( let interval) :
220+ let triggerTime = timestamp. addingTimeInterval ( interval)
221+ let timeIntervalSinceNow = triggerTime. timeIntervalSinceNow
222+ if timeIntervalSinceNow < 0 {
223+ // Trigger time has passed...trigger immediately
224+ return . immediate
225+ } else {
226+ return . delayed( interval: timeIntervalSinceNow)
227+ }
228+ case . repeating:
229+ // Strange case here: if it is a repeating trigger, we can't really play back exactly
230+ // at the right "remaining time" and then repeat at the original period. So, I think
231+ // the best we can do is just use the original trigger
232+ return alert. trigger
233+ }
234+ }
235+
236+ }
237+
238+ public extension DeviceAlert {
239+
240+ enum Error : String , Swift . Error {
241+ case noBackgroundContent
242+ }
243+
244+ fileprivate func getUserNotificationContent( timestamp: Date ) throws -> UNNotificationContent {
245+ guard let content = backgroundContent else {
246+ throw Error . noBackgroundContent
247+ }
248+ let userNotificationContent = UNMutableNotificationContent ( )
249+ userNotificationContent. title = content. title
250+ userNotificationContent. body = content. body
251+ userNotificationContent. sound = getUserNotificationSound ( )
252+ // TODO: Once we have a final design and approval for custom UserNotification buttons, we'll need to set categoryIdentifier
253+ // userNotificationContent.categoryIdentifier = LoopNotificationCategory.alert.rawValue
254+ userNotificationContent. threadIdentifier = identifier. value // Used to match categoryIdentifier, but I /think/ we want multiple threads for multiple alert types, no?
255+ userNotificationContent. userInfo = [
256+ LoopNotificationUserInfoKey . managerIDForAlert. rawValue: identifier. managerIdentifier,
257+ LoopNotificationUserInfoKey . alertTypeID. rawValue: identifier. alertIdentifier,
258+ ]
259+ let encodedAlert = try encodeToString ( )
260+ userNotificationContent. userInfo [ DeviceAlertUserNotificationUserInfoKey . deviceAlert. rawValue] = encodedAlert
261+ userNotificationContent. userInfo [ DeviceAlertUserNotificationUserInfoKey . deviceAlertTimestamp. rawValue] =
262+ DeviceAlertManager . timestampFormatter. string ( from: timestamp)
263+ print ( " Alert: \( encodedAlert) " )
264+ return userNotificationContent
265+ }
266+
267+ private func getUserNotificationSound( ) -> UNNotificationSound ? {
268+ guard let content = backgroundContent else {
269+ return nil
270+ }
271+ if let sound = sound {
272+ switch sound {
273+ case . vibrate:
274+ // TODO: Not sure how to "force" UNNotificationSound to "vibrate only"...so for now we just do the default
275+ break
276+ case . silence:
277+ // TODO: Not sure how to "force" UNNotificationSound to "silence"...so for now we just do the default
278+ break
279+ default :
280+ if let actualFileName = DeviceAlertManager . soundURL ( for: self ) ? . lastPathComponent {
281+ let unname = UNNotificationSoundName ( rawValue: actualFileName)
282+ return content. isCritical ? UNNotificationSound . criticalSoundNamed ( unname) : UNNotificationSound ( named: unname)
283+ }
284+ }
285+ }
286+
287+ return content. isCritical ? . defaultCritical : . default
288+ }
289+ }
290+
291+ public extension UNNotificationRequest {
292+ convenience init ( from deviceAlert: DeviceAlert , timestamp: Date ) throws {
293+ let uncontent = try deviceAlert. getUserNotificationContent ( timestamp: timestamp)
294+ self . init ( identifier: deviceAlert. identifier. value,
295+ content: uncontent,
296+ trigger: UNTimeIntervalNotificationTrigger ( from: deviceAlert. trigger) )
297+ }
298+ }
299+
300+ fileprivate extension UNTimeIntervalNotificationTrigger {
301+ convenience init ? ( from deviceAlertTrigger: DeviceAlert . Trigger ) {
302+ switch deviceAlertTrigger {
303+ case . immediate:
304+ return nil
305+ case . delayed( let timeInterval) :
306+ self . init ( timeInterval: timeInterval, repeats: false )
307+ case . repeating( let repeatInterval) :
308+ self . init ( timeInterval: repeatInterval, repeats: true )
309+ }
131310 }
132311}
0 commit comments