UserNotificationsManager.swift 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665
  1. import AudioToolbox
  2. import Combine
  3. import CoreData
  4. import Foundation
  5. import LoopKit
  6. import SwiftUI
  7. import Swinject
  8. import UIKit
  9. import UserNotifications
  10. protocol UserNotificationsManager {}
  11. enum GlucoseSourceKey: String {
  12. case transmitterBattery
  13. case nightscoutPing
  14. case description
  15. }
  16. enum NotificationAction: String {
  17. static let key = "action"
  18. case snooze
  19. case pumpConfig
  20. case none
  21. }
  22. protocol BolusFailureObserver {
  23. func bolusDidFail()
  24. }
  25. protocol alertMessageNotificationObserver {
  26. func alertMessageNotification(_ message: MessageContent)
  27. }
  28. protocol pumpNotificationObserver {
  29. func pumpNotification(alert: AlertEntry)
  30. func pumpRemoveNotification()
  31. }
  32. final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, Injectable {
  33. private enum Identifier: String {
  34. case glucoseNotification = "FreeAPS.glucoseNotification"
  35. case carbsRequiredNotification = "FreeAPS.carbsRequiredNotification"
  36. case noLoopFirstNotification = "FreeAPS.noLoopFirstNotification"
  37. case noLoopSecondNotification = "FreeAPS.noLoopSecondNotification"
  38. case bolusFailedNotification = "FreeAPS.bolusFailedNotification"
  39. case pumpNotification = "FreeAPS.pumpNotification"
  40. case alertMessageNotification = "FreeAPS.alertMessageNotification"
  41. }
  42. @Injected() var alertPermissionsChecker: AlertPermissionsChecker!
  43. @Injected() private var settingsManager: SettingsManager!
  44. @Injected() private var broadcaster: Broadcaster!
  45. @Injected() private var glucoseStorage: GlucoseStorage!
  46. @Injected() private var apsManager: APSManager!
  47. @Injected() private var router: Router!
  48. @Injected(as: FetchGlucoseManager.self) private var sourceInfoProvider: SourceInfoProvider!
  49. @Persisted(key: "UserNotificationsManager.snoozeUntilDate") private var snoozeUntilDate: Date = .distantPast
  50. private let center = UNUserNotificationCenter.current()
  51. private var lifetime = Lifetime()
  52. private let viewContext = CoreDataStack.shared.persistentContainer.viewContext
  53. private let backgroundContext = CoreDataStack.shared.newTaskContext()
  54. private var coreDataPublisher: AnyPublisher<Set<NSManagedObject>, Never>?
  55. private var subscriptions = Set<AnyCancellable>()
  56. let firstInterval = 20 // min
  57. let secondInterval = 40 // min
  58. init(resolver: Resolver) {
  59. super.init()
  60. center.delegate = self
  61. injectServices(resolver)
  62. coreDataPublisher =
  63. changedObjectsOnManagedObjectContextDidSavePublisher()
  64. .receive(on: DispatchQueue.global(qos: .background))
  65. .share()
  66. .eraseToAnyPublisher()
  67. broadcaster.register(DeterminationObserver.self, observer: self)
  68. broadcaster.register(BolusFailureObserver.self, observer: self)
  69. broadcaster.register(pumpNotificationObserver.self, observer: self)
  70. broadcaster.register(alertMessageNotificationObserver.self, observer: self)
  71. requestNotificationPermissionsIfNeeded()
  72. Task {
  73. await sendGlucoseNotification()
  74. }
  75. registerHandlers()
  76. registerSubscribers()
  77. subscribeOnLoop()
  78. }
  79. private func subscribeOnLoop() {
  80. apsManager.lastLoopDateSubject
  81. .sink { [weak self] date in
  82. self?.scheduleMissingLoopNotifiactions(date: date)
  83. }
  84. .store(in: &lifetime)
  85. }
  86. private func registerHandlers() {
  87. // Due to the Batch insert this only is used for observing Deletion of Glucose entries
  88. coreDataPublisher?.filterByEntityName("GlucoseStored").sink { [weak self] _ in
  89. guard let self = self else { return }
  90. Task {
  91. await self.sendGlucoseNotification()
  92. }
  93. }.store(in: &subscriptions)
  94. }
  95. private func registerSubscribers() {
  96. glucoseStorage.updatePublisher
  97. .receive(on: DispatchQueue.global(qos: .background))
  98. .sink { [weak self] _ in
  99. guard let self = self else { return }
  100. Task {
  101. await self.sendGlucoseNotification()
  102. }
  103. }
  104. .store(in: &subscriptions)
  105. }
  106. private func addAppBadge(glucose: Int?) {
  107. guard let glucose = glucose, settingsManager.settings.glucoseBadge else {
  108. DispatchQueue.main.async {
  109. self.center.setBadgeCount(-1) { error in
  110. guard let error else {
  111. return
  112. }
  113. print(error)
  114. }
  115. }
  116. return
  117. }
  118. let badge: Int
  119. if settingsManager.settings.units == .mmolL {
  120. badge = Int(round(Double((glucose * 10).asMmolL)))
  121. } else {
  122. badge = glucose
  123. }
  124. DispatchQueue.main.async {
  125. self.center.setBadgeCount(badge) { error in
  126. guard let error else {
  127. return
  128. }
  129. print(error)
  130. }
  131. }
  132. }
  133. private func notifyCarbsRequired(_ carbs: Int) {
  134. guard Decimal(carbs) >= settingsManager.settings.carbsRequiredThreshold,
  135. settingsManager.settings.showCarbsRequiredBadge, settingsManager.settings.notificationsCarb else { return }
  136. var titles: [String] = []
  137. let content = UNMutableNotificationContent()
  138. if snoozeUntilDate > Date() {
  139. titles.append(NSLocalizedString("(Snoozed)", comment: "(Snoozed)"))
  140. } else {
  141. content.sound = .default
  142. playSoundIfNeeded()
  143. }
  144. titles.append(String(format: NSLocalizedString("Carbs required: %d g", comment: "Carbs required"), carbs))
  145. content.title = titles.joined(separator: " ")
  146. content.body = String(
  147. format: NSLocalizedString(
  148. "To prevent LOW required %d g of carbs",
  149. comment: "To prevent LOW required %d g of carbs"
  150. ),
  151. carbs
  152. )
  153. addRequest(identifier: .carbsRequiredNotification, content: content, deleteOld: true, messageSubtype: .carb)
  154. }
  155. private func scheduleMissingLoopNotifiactions(date _: Date) {
  156. let title = NSLocalizedString("Trio Not Active", comment: "Trio Not Active")
  157. let body = NSLocalizedString("Last loop was more than %d min ago", comment: "Last loop was more than %d min ago")
  158. let firstContent = UNMutableNotificationContent()
  159. firstContent.title = title
  160. firstContent.body = String(format: body, firstInterval)
  161. firstContent.sound = .default
  162. let secondContent = UNMutableNotificationContent()
  163. secondContent.title = title
  164. secondContent.body = String(format: body, secondInterval)
  165. secondContent.sound = .default
  166. let firstTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 60 * TimeInterval(firstInterval), repeats: false)
  167. let secondTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 60 * TimeInterval(secondInterval), repeats: false)
  168. addRequest(
  169. identifier: .noLoopFirstNotification,
  170. content: firstContent,
  171. deleteOld: true,
  172. trigger: firstTrigger,
  173. messageType: .error,
  174. messageSubtype: .algorithm
  175. )
  176. addRequest(
  177. identifier: .noLoopSecondNotification,
  178. content: secondContent,
  179. deleteOld: true,
  180. trigger: secondTrigger,
  181. messageType: .error,
  182. messageSubtype: .algorithm
  183. )
  184. }
  185. private func notifyBolusFailure() {
  186. let title = NSLocalizedString("Bolus failed", comment: "Bolus failed")
  187. let body = NSLocalizedString(
  188. "Bolus failed or inaccurate. Check pump history before repeating.",
  189. comment: "Bolus failed or inaccurate. Check pump history before repeating."
  190. )
  191. let content = UNMutableNotificationContent()
  192. content.title = title
  193. content.body = body
  194. content.sound = .default
  195. addRequest(
  196. identifier: .noLoopFirstNotification,
  197. content: content,
  198. deleteOld: true,
  199. trigger: nil,
  200. messageType: .error,
  201. messageSubtype: .pump
  202. )
  203. }
  204. private func fetchGlucoseIDs() async -> [NSManagedObjectID] {
  205. let results = await CoreDataStack.shared.fetchEntitiesAsync(
  206. ofType: GlucoseStored.self,
  207. onContext: backgroundContext,
  208. predicate: NSPredicate.predicateFor20MinAgo,
  209. key: "date",
  210. ascending: false,
  211. fetchLimit: 3
  212. )
  213. return await backgroundContext.perform {
  214. guard let fetchedResults = results as? [GlucoseStored] else { return [] }
  215. return fetchedResults.map(\.objectID)
  216. }
  217. }
  218. @MainActor private func sendGlucoseNotification() async {
  219. do {
  220. addAppBadge(glucose: nil)
  221. let glucoseIDs = await fetchGlucoseIDs()
  222. let glucoseObjects = try glucoseIDs.compactMap { id in
  223. try viewContext.existingObject(with: id) as? GlucoseStored
  224. }
  225. guard let lastReading = glucoseObjects.first?.glucose,
  226. let secondLastReading = glucoseObjects.dropFirst().first?.glucose,
  227. let lastDirection = glucoseObjects.first?.directionEnum?.symbol else { return }
  228. addAppBadge(glucose: (glucoseObjects.first?.glucose).map { Int($0) })
  229. guard glucoseStorage.alarm != nil || settingsManager.settings.glucoseNotificationsAlways else { return }
  230. var titles: [String] = []
  231. var notificationAlarm = false
  232. var messageType = MessageType.info
  233. switch glucoseStorage.alarm {
  234. case .none:
  235. titles.append(NSLocalizedString("Glucose", comment: "Glucose"))
  236. case .low:
  237. titles.append(NSLocalizedString("LOWALERT!", comment: "LOWALERT!"))
  238. messageType = MessageType.warning
  239. notificationAlarm = true
  240. case .high:
  241. titles.append(NSLocalizedString("HIGHALERT!", comment: "HIGHALERT!"))
  242. messageType = MessageType.warning
  243. notificationAlarm = true
  244. }
  245. let delta = glucoseObjects.count >= 2 ? lastReading - secondLastReading : nil
  246. let body = glucoseText(
  247. glucoseValue: Int(lastReading),
  248. delta: Int(delta ?? 0),
  249. direction: lastDirection
  250. ) + infoBody()
  251. if snoozeUntilDate > Date() {
  252. titles.append(NSLocalizedString("(Snoozed)", comment: "(Snoozed)"))
  253. notificationAlarm = false
  254. } else {
  255. titles.append(body)
  256. let content = UNMutableNotificationContent()
  257. content.title = titles.joined(separator: " ")
  258. content.body = body
  259. if notificationAlarm {
  260. playSoundIfNeeded()
  261. content.sound = .default
  262. content.userInfo[NotificationAction.key] = NotificationAction.snooze.rawValue
  263. }
  264. addRequest(
  265. identifier: .glucoseNotification,
  266. content: content,
  267. deleteOld: true,
  268. messageType: messageType,
  269. messageSubtype: .glucose,
  270. action: NotificationAction.snooze
  271. )
  272. }
  273. } catch {
  274. debugPrint(
  275. "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to send glucose notification with error: \(error.localizedDescription)"
  276. )
  277. }
  278. }
  279. private func glucoseText(glucoseValue: Int, delta: Int?, direction: String?) -> String {
  280. let units = settingsManager.settings.units
  281. let glucoseText = glucoseFormatter
  282. .string(from: Double(
  283. units == .mmolL ? glucoseValue
  284. .asMmolL : Decimal(glucoseValue)
  285. ) as NSNumber)! + " " + NSLocalizedString(units.rawValue, comment: "units")
  286. let directionText = direction ?? "↔︎"
  287. let deltaText = delta
  288. .map {
  289. self.deltaFormatter
  290. .string(from: Double(
  291. units == .mmolL ? $0
  292. .asMmolL : Decimal($0)
  293. ) as NSNumber)!
  294. } ?? "--"
  295. return glucoseText + " " + directionText + " " + deltaText
  296. }
  297. private func infoBody() -> String {
  298. var body = ""
  299. if settingsManager.settings.addSourceInfoToGlucoseNotifications,
  300. let info = sourceInfoProvider.sourceInfo()
  301. {
  302. // Description
  303. if let description = info[GlucoseSourceKey.description.rawValue] as? String {
  304. body.append("\n" + description)
  305. }
  306. // NS ping
  307. if let ping = info[GlucoseSourceKey.nightscoutPing.rawValue] as? TimeInterval {
  308. body.append(
  309. "\n"
  310. + String(
  311. format: NSLocalizedString("Nightscout ping: %d ms", comment: "Nightscout ping"),
  312. Int(ping * 1000)
  313. )
  314. )
  315. }
  316. // Transmitter battery
  317. if let transmitterBattery = info[GlucoseSourceKey.transmitterBattery.rawValue] as? Int {
  318. body.append(
  319. "\n"
  320. + String(
  321. format: NSLocalizedString("Transmitter: %@%%", comment: "Transmitter: %@%%"),
  322. "\(transmitterBattery)"
  323. )
  324. )
  325. }
  326. }
  327. return body
  328. }
  329. private func requestNotificationPermissionsIfNeeded() {
  330. center.getNotificationSettings { settings in
  331. debug(.service, "UNUserNotificationCenter.authorizationStatus: \(String(describing: settings.authorizationStatus))")
  332. if ![.authorized, .provisional].contains(settings.authorizationStatus) {
  333. self.requestNotificationPermissions()
  334. }
  335. }
  336. }
  337. private func requestNotificationPermissions() {
  338. debug(.service, "requestNotificationPermissions")
  339. center.requestAuthorization(options: [.badge, .sound, .alert]) { granted, error in
  340. if granted {
  341. debug(.service, "requestNotificationPermissions was granted")
  342. } else {
  343. warning(.service, "requestNotificationPermissions failed", error: error)
  344. }
  345. }
  346. }
  347. private func addRequest(
  348. identifier: Identifier,
  349. content: UNMutableNotificationContent,
  350. deleteOld: Bool = false,
  351. trigger: UNNotificationTrigger? = nil,
  352. messageType: MessageType = MessageType.other,
  353. messageSubtype: MessageSubtype = MessageSubtype.misc,
  354. action: NotificationAction = NotificationAction.none
  355. ) {
  356. if alertPermissionsChecker.notificationsDisabled {
  357. let messageCont = MessageContent(
  358. content: content.body,
  359. type: messageType,
  360. subtype: messageSubtype,
  361. title: content.title,
  362. useAPN: false,
  363. trigger: trigger,
  364. action: action
  365. )
  366. router.alertMessage.send(messageCont)
  367. return
  368. }
  369. var alertIdentifier = identifier.rawValue
  370. alertIdentifier = identifier == .pumpNotification ? alertIdentifier + content
  371. .title : (identifier == .alertMessageNotification ? alertIdentifier + content.body : alertIdentifier)
  372. let request = UNNotificationRequest(identifier: alertIdentifier, content: content, trigger: trigger)
  373. if deleteOld {
  374. DispatchQueue.main.async {
  375. self.center.removeDeliveredNotifications(withIdentifiers: [identifier.rawValue])
  376. self.center.removePendingNotificationRequests(withIdentifiers: [identifier.rawValue])
  377. }
  378. }
  379. DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
  380. self.center.add(request) { error in
  381. if let error = error {
  382. warning(.service, "Unable to addNotificationRequest", error: error)
  383. return
  384. }
  385. debug(.service, "Sending \(identifier) notification for \(request.content.title)")
  386. }
  387. }
  388. }
  389. private func playSoundIfNeeded() {
  390. guard settingsManager.settings.useAlarmSound, snoozeUntilDate < Date() else { return }
  391. Self.stopPlaying = false
  392. playSound()
  393. }
  394. static let soundID: UInt32 = 1336
  395. private static var stopPlaying = false
  396. private func playSound(times: Int = 1) {
  397. guard times > 0, !Self.stopPlaying else {
  398. return
  399. }
  400. AudioServicesPlaySystemSoundWithCompletion(Self.soundID) {
  401. self.playSound(times: times - 1)
  402. }
  403. }
  404. static func stopSound() {
  405. stopPlaying = true
  406. AudioServicesDisposeSystemSoundID(soundID)
  407. }
  408. private var glucoseFormatter: NumberFormatter {
  409. let formatter = NumberFormatter()
  410. formatter.numberStyle = .decimal
  411. formatter.maximumFractionDigits = 0
  412. if settingsManager.settings.units == .mmolL {
  413. formatter.minimumFractionDigits = 1
  414. formatter.maximumFractionDigits = 1
  415. }
  416. formatter.roundingMode = .halfUp
  417. return formatter
  418. }
  419. private var deltaFormatter: NumberFormatter {
  420. let formatter = NumberFormatter()
  421. formatter.numberStyle = .decimal
  422. formatter.maximumFractionDigits = 1
  423. formatter.positivePrefix = "+"
  424. return formatter
  425. }
  426. }
  427. extension BaseUserNotificationsManager: alertMessageNotificationObserver {
  428. func alertMessageNotification(_ message: MessageContent) {
  429. let content = UNMutableNotificationContent()
  430. var identifier: Identifier = .alertMessageNotification
  431. if message.title == "" {
  432. switch message.type {
  433. case .info:
  434. content.title = NSLocalizedString("Info", comment: "Info title")
  435. case .warning:
  436. content.title = NSLocalizedString("Warning", comment: "Warning title")
  437. case .error:
  438. content.title = NSLocalizedString("Error", comment: "Error title")
  439. default:
  440. content.title = message.title
  441. }
  442. } else {
  443. content.title = message.title
  444. }
  445. switch message.subtype {
  446. case .pump:
  447. identifier = .pumpNotification
  448. case .carb:
  449. identifier = .carbsRequiredNotification
  450. case .glucose:
  451. identifier = .glucoseNotification
  452. case .algorithm:
  453. if message.trigger != nil {
  454. identifier = message.content.contains(String(firstInterval)) ? Identifier.noLoopFirstNotification : Identifier
  455. .noLoopSecondNotification
  456. } else {
  457. identifier = Identifier.alertMessageNotification
  458. }
  459. default:
  460. identifier = .alertMessageNotification
  461. }
  462. switch message.action {
  463. case .snooze:
  464. content.userInfo[NotificationAction.key] = NotificationAction.snooze.rawValue
  465. case .pumpConfig:
  466. content.userInfo[NotificationAction.key] = NotificationAction.pumpConfig.rawValue
  467. default: break
  468. }
  469. content.body = NSLocalizedString(message.content, comment: "Info message")
  470. content.sound = .default
  471. addRequest(
  472. identifier: identifier,
  473. content: content,
  474. deleteOld: true,
  475. trigger: message.trigger,
  476. messageType: message.type,
  477. messageSubtype: message.subtype,
  478. action: message.action
  479. )
  480. }
  481. }
  482. extension BaseUserNotificationsManager: pumpNotificationObserver {
  483. func pumpNotification(alert: AlertEntry) {
  484. let content = UNMutableNotificationContent()
  485. let alertUp = alert.alertIdentifier.uppercased()
  486. let typeMessage: MessageType
  487. if alertUp.contains("FAULT") || alertUp.contains("ERROR") {
  488. content.userInfo[NotificationAction.key] = NotificationAction.pumpConfig.rawValue
  489. typeMessage = .error
  490. } else {
  491. typeMessage = .warning
  492. guard settingsManager.settings.notificationsPump else { return }
  493. }
  494. content.title = alert.contentTitle ?? "Unknown"
  495. content.body = alert.contentBody ?? "Unknown"
  496. content.sound = .default
  497. addRequest(
  498. identifier: .pumpNotification,
  499. content: content,
  500. deleteOld: true,
  501. trigger: nil,
  502. messageType: typeMessage,
  503. messageSubtype: .pump,
  504. action: .pumpConfig
  505. )
  506. }
  507. func pumpRemoveNotification() {
  508. let identifier: Identifier = .pumpNotification
  509. DispatchQueue.main.async {
  510. self.center.removeDeliveredNotifications(withIdentifiers: [identifier.rawValue])
  511. self.center.removePendingNotificationRequests(withIdentifiers: [identifier.rawValue])
  512. }
  513. }
  514. }
  515. extension BaseUserNotificationsManager: DeterminationObserver {
  516. func determinationDidUpdate(_ determination: Determination) {
  517. guard let carndRequired = determination.carbsReq else { return }
  518. notifyCarbsRequired(Int(carndRequired))
  519. }
  520. }
  521. extension BaseUserNotificationsManager: BolusFailureObserver {
  522. func bolusDidFail() {
  523. notifyBolusFailure()
  524. }
  525. }
  526. extension BaseUserNotificationsManager: UNUserNotificationCenterDelegate {
  527. static let shared = BaseUserNotificationsManager(resolver: FreeAPSApp.resolver)
  528. /// This is called when a notification is received while the app is in the foreground.
  529. /// There is an iOS 18 bug where it is called twice (https://forums.developer.apple.com/forums/thread/762126).
  530. /// As a workaround we are checking for duplicate ids and ignoring the second notification
  531. func userNotificationCenter(
  532. _: UNUserNotificationCenter,
  533. willPresent notification: UNNotification,
  534. withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
  535. ) {
  536. let lastNotificationKey = "lastNotificationKey"
  537. var isDuplicateNotification = false
  538. if let lastNotification = UserDefaults.standard.value(forKey: lastNotificationKey) as? String {
  539. isDuplicateNotification = lastNotification == notification.request.identifier + notification.request.content.title
  540. }
  541. UserDefaults.standard.set(
  542. notification.request.identifier + notification.request.content.title,
  543. forKey: lastNotificationKey
  544. )
  545. if !isDuplicateNotification {
  546. debug(
  547. .service,
  548. "Process non-duplicate \(notification.request.identifier) notification for \(notification.request.content.title)"
  549. )
  550. completionHandler([.banner, .badge, .sound])
  551. } else {
  552. debug(
  553. .service,
  554. "Skip duplicate \(notification.request.identifier) notification for \(notification.request.content.title)"
  555. )
  556. completionHandler([])
  557. }
  558. }
  559. func userNotificationCenter(
  560. _: UNUserNotificationCenter,
  561. didReceive response: UNNotificationResponse,
  562. withCompletionHandler completionHandler: @escaping () -> Void
  563. ) {
  564. defer { completionHandler() }
  565. guard let actionRaw = response.notification.request.content.userInfo[NotificationAction.key] as? String,
  566. let action = NotificationAction(rawValue: actionRaw)
  567. else { return }
  568. switch action {
  569. case .snooze:
  570. router.mainModalScreen.send(.snooze)
  571. case .pumpConfig:
  572. let messageCont = MessageContent(
  573. content: response.notification.request.content.body,
  574. type: MessageType.other,
  575. subtype: .pump,
  576. useAPN: false,
  577. action: .pumpConfig
  578. )
  579. router.alertMessage.send(messageCont)
  580. default: break
  581. }
  582. }
  583. }