UserNotificationsManager.swift 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import AudioToolbox
  2. import Foundation
  3. import Swinject
  4. import UIKit
  5. import UserNotifications
  6. protocol UserNotificationsManager {}
  7. final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, Injectable {
  8. private enum Identifier: String {
  9. case glucocoseNotification = "FreeAPS.glucoseNotification"
  10. }
  11. @Injected() private var settingsManager: SettingsManager!
  12. @Injected() private var broadcaster: Broadcaster!
  13. @Injected() private var glucoseStorage: GlucoseStorage!
  14. private let center = UNUserNotificationCenter.current()
  15. init(resolver: Resolver) {
  16. super.init()
  17. center.delegate = self
  18. injectServices(resolver)
  19. broadcaster.register(GlucoseObserver.self, observer: self)
  20. requestNotificationPermissionsIfNeeded()
  21. sendGlucoseNotification()
  22. }
  23. private func addAppBadge(glucose: Int?) {
  24. guard let glucose = glucose, settingsManager.settings.glucoseBadge else {
  25. DispatchQueue.main.async {
  26. UIApplication.shared.applicationIconBadgeNumber = 0
  27. }
  28. return
  29. }
  30. let badge: Int
  31. if settingsManager.settings.units == .mmolL {
  32. badge = Int(round(Double((glucose * 10).asMmolL)))
  33. } else {
  34. badge = glucose
  35. }
  36. DispatchQueue.main.async {
  37. UIApplication.shared.applicationIconBadgeNumber = badge
  38. }
  39. }
  40. private func sendGlucoseNotification() {
  41. addAppBadge(glucose: nil)
  42. ensureCanSendNotification {
  43. let glucose = self.glucoseStorage.recent()
  44. guard let lastGlucose = glucose.last, let glucoseValue = lastGlucose.glucose else { return }
  45. let delta: Int?
  46. if glucose.count >= 2 {
  47. delta = glucoseValue - (glucose[glucose.count - 2].glucose ?? 0)
  48. } else {
  49. delta = nil
  50. }
  51. let content = UNMutableNotificationContent()
  52. var titles: [String] = []
  53. switch self.glucoseStorage.alarm {
  54. case .none:
  55. titles.append(NSLocalizedString("Glucose", comment: "Glucose"))
  56. case .low:
  57. titles.append(NSLocalizedString("LOWALERT!", comment: "LOWALERT!"))
  58. self.playSound()
  59. case .high:
  60. titles.append(NSLocalizedString("HIGHALERT!", comment: "HIGHALERT!"))
  61. self.playSound()
  62. }
  63. let units = self.settingsManager.settings.units
  64. let glucoseText = self.glucoseFormatter
  65. .string(from: Double(
  66. units == .mmolL ? glucoseValue
  67. .asMmolL : Decimal(glucoseValue)
  68. ) as NSNumber)! + " " + NSLocalizedString(units.rawValue, comment: "units")
  69. let directionText = lastGlucose.direction?.symbol ?? "↔︎"
  70. let deltaText = delta
  71. .map {
  72. self.deltaFormatter
  73. .string(from: Double(
  74. units == .mmolL ? $0
  75. .asMmolL : Decimal($0)
  76. ) as NSNumber)!
  77. } ?? "--"
  78. let body = glucoseText + " " + directionText + " " + deltaText
  79. titles.append(body)
  80. content.title = titles.joined(separator: " ")
  81. content.body = body
  82. self.addAppBadge(glucose: lastGlucose.glucose)
  83. self.addRequest(identifier: .glucocoseNotification, content: content, deleteOld: true)
  84. }
  85. }
  86. private func requestNotificationPermissionsIfNeeded() {
  87. center.getNotificationSettings { settings in
  88. debug(.service, "UNUserNotificationCenter.authorizationStatus: \(String(describing: settings.authorizationStatus))")
  89. if ![.authorized, .provisional].contains(settings.authorizationStatus) {
  90. self.requestNotificationPermissions()
  91. }
  92. }
  93. }
  94. private func requestNotificationPermissions() {
  95. debug(.service, "requestNotificationPermissions")
  96. center.requestAuthorization(options: [.badge, .sound, .alert]) { granted, error in
  97. if granted {
  98. debug(.service, "requestNotificationPermissions was granted")
  99. } else {
  100. warning(.service, "requestNotificationPermissions failed", error: error)
  101. }
  102. }
  103. }
  104. private func ensureCanSendNotification(_ completion: @escaping () -> Void) {
  105. center.getNotificationSettings { settings in
  106. guard settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional else {
  107. warning(.service, "ensureCanSendNotification failed, authorization denied")
  108. return
  109. }
  110. debug(.service, "Sending notification was allowed")
  111. completion()
  112. }
  113. }
  114. private func addRequest(identifier: Identifier, content: UNMutableNotificationContent, deleteOld: Bool = false) {
  115. let request = UNNotificationRequest(identifier: identifier.rawValue, content: content, trigger: nil)
  116. if deleteOld {
  117. center.removeDeliveredNotifications(withIdentifiers: [identifier.rawValue])
  118. center.removePendingNotificationRequests(withIdentifiers: [identifier.rawValue])
  119. }
  120. center.add(request) { error in
  121. if let error = error {
  122. warning(.service, "Unable to addNotificationRequest", error: error)
  123. return
  124. }
  125. debug(.service, "Sending \(identifier) notification")
  126. }
  127. }
  128. private func playSound(times: Int = 3) {
  129. guard times > 0 else {
  130. return
  131. }
  132. AudioServicesPlaySystemSoundWithCompletion(1336) {
  133. self.playSound(times: times - 1)
  134. }
  135. }
  136. private var glucoseFormatter: NumberFormatter {
  137. let formatter = NumberFormatter()
  138. formatter.numberStyle = .decimal
  139. formatter.maximumFractionDigits = 0
  140. if settingsManager.settings.units == .mmolL {
  141. formatter.minimumFractionDigits = 1
  142. formatter.maximumFractionDigits = 1
  143. }
  144. formatter.roundingMode = .halfUp
  145. return formatter
  146. }
  147. private var deltaFormatter: NumberFormatter {
  148. let formatter = NumberFormatter()
  149. formatter.numberStyle = .decimal
  150. formatter.maximumFractionDigits = 2
  151. formatter.positivePrefix = "+"
  152. return formatter
  153. }
  154. }
  155. extension BaseUserNotificationsManager: GlucoseObserver {
  156. func glucoseDidUpdate(_: [BloodGlucose]) {
  157. sendGlucoseNotification()
  158. }
  159. }
  160. extension BaseUserNotificationsManager: UNUserNotificationCenterDelegate {
  161. func userNotificationCenter(
  162. _: UNUserNotificationCenter,
  163. willPresent _: UNNotification,
  164. withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
  165. ) {
  166. completionHandler([.banner, .badge, .sound])
  167. }
  168. }