UserNotificationsManager.swift 8.0 KB

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