NotificationHelper.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. //
  2. // NotificationHelper.swift
  3. // MiaomiaoClient
  4. //
  5. // Created by Bjørn Inge Berg on 30/05/2019.
  6. // Copyright © 2019 Bjørn Inge Berg. All rights reserved.
  7. //
  8. import Foundation
  9. import HealthKit
  10. import UserNotifications
  11. import os.log
  12. import UIKit
  13. fileprivate var logger = Logger(forType: "NotificationHelper")
  14. public enum NotificationHelper {
  15. private enum Identifiers: String {
  16. case glucocoseNotifications = "no.bjorninge.miaomiao.glucose-notification"
  17. case noSensorDetected = "no.bjorninge.miaomiao.nosensordetected-notification"
  18. case tryAgainLater = "no.bjorninge.miaomiao.glucoseNotAvailableTryAgainLater-notification"
  19. case sensorChange = "no.bjorninge.miaomiao.sensorchange-notification"
  20. case invalidSensor = "no.bjorninge.miaomiao.invalidsensor-notification"
  21. case lowBattery = "no.bjorninge.miaomiao.lowbattery-notification"
  22. case sensorExpire = "no.bjorninge.miaomiao.SensorExpire-notification"
  23. case noBridgeSelected = "no.bjorninge.miaomiao.noBridgeSelected-notification"
  24. case bluetoothPoweredOff = "no.bjorninge.miaomiao.bluetoothPoweredOff-notification"
  25. case invalidChecksum = "no.bjorninge.miaomiao.invalidChecksum-notification"
  26. case calibrationOngoing = "no.bjorninge.miaomiao.calibration-notification"
  27. case restoredState = "no.bjorninge.miaomiao.state-notification"
  28. }
  29. public static func GlucoseUnitIsSupported(unit: HKUnit) -> Bool {
  30. [HKUnit.milligramsPerDeciliter, HKUnit.millimolesPerLiter].contains(unit)
  31. }
  32. public static func sendRestoredStateNotification(msg: String) {
  33. ensureCanSendNotification {
  34. logger.debug("dabear:: sending RestoredStateNotification")
  35. let content = UNMutableNotificationContent()
  36. content.title = NSLocalizedString("State was restored", comment: "State was restored")
  37. content.body = msg
  38. addRequest(identifier: .restoredState, content: content )
  39. }
  40. }
  41. public static func sendBluetoothPowerOffNotification() {
  42. ensureCanSendNotification {
  43. logger.debug("dabear:: sending BluetoothPowerOffNotification")
  44. let content = UNMutableNotificationContent()
  45. content.title = NSLocalizedString("Bluetooth Power Off", comment: "Bluetooth Power Off")
  46. content.body = NSLocalizedString("Please turn on Bluetooth", comment: "Please turn on Bluetooth")
  47. addRequest(identifier: .bluetoothPoweredOff, content: content)
  48. }
  49. }
  50. public static func sendNoTransmitterSelectedNotification() {
  51. ensureCanSendNotification {
  52. logger.debug("dabear:: sending NoTransmitterSelectedNotification")
  53. let content = UNMutableNotificationContent()
  54. content.title = NSLocalizedString("No Libre Transmitter Selected", comment: "No Libre Transmitter Selected")
  55. content.body = NSLocalizedString("Delete Transmitter and start anew.", comment: "Delete Transmitter and start anew.")
  56. addRequest(identifier: .noBridgeSelected, content: content)
  57. }
  58. }
  59. private static func ensureCanSendGlucoseNotification(_ completion: @escaping (_ unit: HKUnit) -> Void ) {
  60. ensureCanSendNotification {
  61. if let glucoseUnit = UserDefaults.standard.mmGlucoseUnit, GlucoseUnitIsSupported(unit: glucoseUnit) {
  62. completion(glucoseUnit)
  63. }
  64. }
  65. }
  66. public static func requestNotificationPermissionsIfNeeded(){
  67. UNUserNotificationCenter.current().getNotificationSettings { settings in
  68. logger.debug("settings.authorizationStatus: \(String(describing: settings.authorizationStatus.rawValue))")
  69. if ![.authorized,.provisional].contains(settings.authorizationStatus) {
  70. requestNotificationPermissions()
  71. }
  72. }
  73. }
  74. private static func requestNotificationPermissions() {
  75. logger.debug("requestNotificationPermissions called")
  76. let center = UNUserNotificationCenter.current()
  77. center.requestAuthorization(options: [.badge, .sound, .alert]) { (granted, error) in
  78. if granted {
  79. logger.debug("requestNotificationPermissions was granted")
  80. } else {
  81. logger.debug("requestNotificationPermissions failed because of error: \(String(describing: error))")
  82. }
  83. }
  84. }
  85. private static func ensureCanSendNotification(_ completion: @escaping () -> Void ) {
  86. UNUserNotificationCenter.current().getNotificationSettings { settings in
  87. guard settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional else {
  88. logger.debug("dabear:: ensureCanSendNotification failed, authorization denied")
  89. return
  90. }
  91. logger.debug("dabear:: sending notification was allowed")
  92. completion()
  93. }
  94. }
  95. public static func sendInvalidChecksumIfDeveloper(_ sensorData: SensorData) {
  96. if sensorData.hasValidCRCs {
  97. return
  98. }
  99. ensureCanSendNotification {
  100. let content = UNMutableNotificationContent()
  101. content.title = NSLocalizedString("Invalid libre checksum", comment: "Invalid libre checksum")
  102. content.body = NSLocalizedString("Libre sensor was incorrectly read, CRCs were not valid", comment: "Libre sensor was incorrectly read, CRCs were not valid")
  103. addRequest(identifier: .invalidChecksum, content: content)
  104. }
  105. }
  106. private static func addRequest(identifier: Identifiers, content: UNMutableNotificationContent, deleteOld: Bool = false) {
  107. let center = UNUserNotificationCenter.current()
  108. //content.sound = UNNotificationSound.
  109. let request = UNNotificationRequest(identifier: identifier.rawValue, content: content, trigger: nil)
  110. if deleteOld {
  111. // Required since ios12+ have started to cache/group notifications
  112. center.removeDeliveredNotifications(withIdentifiers: [identifier.rawValue])
  113. center.removePendingNotificationRequests(withIdentifiers: [identifier.rawValue])
  114. }
  115. center.add(request) { error in
  116. if let error = error {
  117. logger.debug("dabear:: unable to addNotificationRequest: \(error.localizedDescription)")
  118. return
  119. }
  120. logger.debug("dabear:: sending \(identifier.rawValue) notification")
  121. }
  122. }
  123. public enum CalibrationMessage: String {
  124. case starting = "Calibrating sensor, please stand by!"
  125. case noCalibration = "Could not calibrate sensor, check libreoopweb permissions and internet connection"
  126. case invalidCalibrationData = "Could not calibrate sensor, invalid calibrationdata"
  127. case success = "Success!"
  128. }
  129. public static func sendCalibrationNotification(_ calibrationMessage: CalibrationMessage) {
  130. ensureCanSendNotification {
  131. let content = UNMutableNotificationContent()
  132. content.sound = .default
  133. content.title = NSLocalizedString("Extracting calibrationdata from sensor", comment: "Extracting calibrationdata from sensor")
  134. content.body = NSLocalizedString(calibrationMessage.rawValue, comment: "calibrationMessage")
  135. addRequest(identifier: .calibrationOngoing,
  136. content: content,
  137. deleteOld: true)
  138. }
  139. }
  140. public static func sendSensorNotDetectedNotificationIfNeeded(noSensor: Bool) {
  141. guard UserDefaults.standard.mmAlertNoSensorDetected && noSensor else {
  142. logger.debug("Not sending noSensorDetected notification")
  143. return
  144. }
  145. sendSensorNotDetectedNotification()
  146. }
  147. private static func sendSensorNotDetectedNotification() {
  148. ensureCanSendNotification {
  149. let content = UNMutableNotificationContent()
  150. content.title = NSLocalizedString("No Sensor Detected", comment: "No Sensor Detected")
  151. content.body = NSLocalizedString("This might be an intermittent problem, but please check that your transmitter is tightly secured over your sensor", comment: "This might be an intermittent problem, but please check that your transmitter is tightly secured over your sensor")
  152. addRequest(identifier: .noSensorDetected, content: content)
  153. }
  154. }
  155. public static func sendSensorChangeNotificationIfNeeded() {
  156. guard UserDefaults.standard.mmAlertNewSensorDetected else {
  157. logger.debug("not sending sendSensorChange notification ")
  158. return
  159. }
  160. sendSensorChangeNotification()
  161. }
  162. private static func sendSensorChangeNotification() {
  163. ensureCanSendNotification {
  164. let content = UNMutableNotificationContent()
  165. content.title = NSLocalizedString("New Sensor Detected", comment: "New Sensor Detected")
  166. content.body = NSLocalizedString("Please wait up to 30 minutes before glucose readings are available!", comment: "Please wait up to 30 minutes before glucose readings are available!")
  167. addRequest(identifier: .sensorChange, content: content)
  168. //content.sound = UNNotificationSound.
  169. }
  170. }
  171. public static func sendSensorTryAgainLaterNotification() {
  172. ensureCanSendNotification {
  173. let content = UNMutableNotificationContent()
  174. content.title = NSLocalizedString("Invalid Glucose sample detected, try again later", comment: "Invalid Glucose sample detected, try again later")
  175. content.body = NSLocalizedString("Sensor might have temporarily stopped, fallen off or is too cold or too warm", comment: "Sensor might have temporarily stopped, fallen off or is too cold or too warm")
  176. addRequest(identifier: .tryAgainLater, content: content)
  177. //content.sound = UNNotificationSound.
  178. }
  179. }
  180. public static func sendInvalidSensorNotificationIfNeeded(sensorData: SensorData) {
  181. let isValid = sensorData.isLikelyLibre1FRAM && (sensorData.state == .starting || sensorData.state == .ready)
  182. guard UserDefaults.standard.mmAlertInvalidSensorDetected && !isValid else {
  183. logger.debug("not sending invalidSensorDetected notification")
  184. return
  185. }
  186. sendInvalidSensorNotification(sensorData: sensorData)
  187. }
  188. private static func sendInvalidSensorNotification(sensorData: SensorData) {
  189. ensureCanSendNotification {
  190. let content = UNMutableNotificationContent()
  191. content.title = NSLocalizedString("Invalid Sensor Detected", comment: "Invalid Sensor Detected")
  192. if !sensorData.isLikelyLibre1FRAM {
  193. content.body = NSLocalizedString("Detected sensor seems not to be a libre 1 sensor!", comment: "Detected sensor seems not to be a libre 1 sensor!")
  194. } else if !(sensorData.state == .starting || sensorData.state == .ready) {
  195. content.body = String(format: NSLocalizedString("Detected sensor is invalid: %@", comment: "Detected sensor is invalid: %@"), sensorData.state.description)
  196. }
  197. content.sound = .default
  198. addRequest(identifier: .invalidSensor, content: content)
  199. }
  200. }
  201. private static var lastBatteryWarning: Date?
  202. public static func sendLowBatteryNotificationIfNeeded(device: LibreTransmitterMetadata) {
  203. guard UserDefaults.standard.mmAlertLowBatteryWarning else {
  204. logger.debug("mmAlertLowBatteryWarning toggle was not enabled, not sending low notification")
  205. return
  206. }
  207. if let battery = device.battery, battery > 20 {
  208. logger.debug("device battery is \(battery), not sending low notification")
  209. return
  210. }
  211. let now = Date()
  212. //only once per mins minute
  213. let mins = 60.0 * 120
  214. if let earlierplus = lastBatteryWarning?.addingTimeInterval(mins) {
  215. if earlierplus < now {
  216. sendLowBatteryNotification(batteryPercentage: device.batteryString,
  217. deviceName: device.name)
  218. lastBatteryWarning = now
  219. } else {
  220. logger.debug("Device battery is running low, but lastBatteryWarning Notification was sent less than 45 minutes ago, aborting. earlierplus: \(earlierplus), now: \(now)")
  221. }
  222. } else {
  223. sendLowBatteryNotification(batteryPercentage: device.batteryString,
  224. deviceName: device.name)
  225. lastBatteryWarning = now
  226. }
  227. }
  228. private static func sendLowBatteryNotification(batteryPercentage: String, deviceName: String) {
  229. ensureCanSendNotification {
  230. let content = UNMutableNotificationContent()
  231. content.title = NSLocalizedString("Low Battery", comment: "Low Battery")
  232. content.body = String(format: NSLocalizedString("Battery is running low %@, consider charging your %@ device as soon as possible", comment: ""), batteryPercentage, deviceName)
  233. content.sound = .default
  234. addRequest(identifier: .lowBattery, content: content)
  235. }
  236. }
  237. private static var lastSensorExpireAlert: Date?
  238. public static func sendSensorExpireAlertIfNeeded(minutesLeft: Double) {
  239. guard UserDefaults.standard.mmAlertWillSoonExpire else {
  240. logger.debug("mmAlertWillSoonExpire toggle was not enabled, not sending expiresoon alarm")
  241. return
  242. }
  243. guard TimeInterval(minutes: minutesLeft) < TimeInterval(hours: 24) else {
  244. logger.debug("Sensor time left was more than 24 hours, not sending notification: \(minutesLeft.twoDecimals) minutes")
  245. return
  246. }
  247. let now = Date()
  248. //only once per 6 hours
  249. let min45 = 60.0 * 60 * 6
  250. if let earlier = lastSensorExpireAlert {
  251. if earlier.addingTimeInterval(min45) < now {
  252. sendSensorExpireAlert(minutesLeft: minutesLeft)
  253. lastSensorExpireAlert = now
  254. } else {
  255. logger.debug("Sensor is soon expiring, but lastSensorExpireAlert was sent less than 6 hours ago, so aborting")
  256. }
  257. } else {
  258. sendSensorExpireAlert(minutesLeft: minutesLeft)
  259. lastSensorExpireAlert = now
  260. }
  261. }
  262. public static func sendSensorExpireAlertIfNeeded(sensorData: SensorData) {
  263. sendSensorExpireAlertIfNeeded(minutesLeft: Double(sensorData.minutesLeft))
  264. }
  265. private static func sendSensorExpireAlert(minutesLeft: Double) {
  266. ensureCanSendNotification {
  267. let hours = minutesLeft == 0 ? 0 : round(minutesLeft/60)
  268. let dynamicText = hours <= 1 ? NSLocalizedString("minutes", comment: "minutes") + ": \(minutesLeft.twoDecimals)" : NSLocalizedString("hours", comment: "hours") + ": \(hours.twoDecimals)"
  269. let content = UNMutableNotificationContent()
  270. content.title = NSLocalizedString("Sensor Ending Soon", comment: "Sensor Ending Soon")
  271. content.body = String(format: NSLocalizedString("Current Sensor is Ending soon! Sensor Life left in %@", comment: ""), dynamicText)
  272. addRequest(identifier: .sensorExpire, content: content, deleteOld: true)
  273. }
  274. }
  275. }