579 lines
35 KiB
Swift
579 lines
35 KiB
Swift
import Foundation
|
|
import os
|
|
import UserNotifications
|
|
import AudioToolbox
|
|
|
|
|
|
/// has a function to check if an alert needs to be raised, and also raised the alert notification if needed.
|
|
///
|
|
/// has all the logic but should be not or almost not aware of the kind of alerts that exists. The logic that is different per type of alert is defined in type AlertKind.
|
|
public class AlertManager:NSObject {
|
|
|
|
// MARK: - private properties
|
|
|
|
/// snoozeActionIdentifier for alert notification
|
|
private let snoozeActionIdentifier = "snoozeActionIdentifier"
|
|
|
|
/// snoozeCategoryIdentifier for alert notification
|
|
private let snoozeCategoryIdentifier = "snoozeCategoryIdentifier"
|
|
|
|
/// for logging
|
|
private var log = OSLog(subsystem: ConstantsLog.subSystem, category: ConstantsLog.categoryAlertManager)
|
|
|
|
/// BgReadings instance
|
|
private let bgReadingsAccessor:BgReadingsAccessor
|
|
|
|
/// Calibrations instance
|
|
private let calibrationsAccessor:CalibrationsAccessor
|
|
|
|
/// Sensors instance
|
|
private let sensorsAccessor:SensorsAccessor
|
|
|
|
/// for getting alertTypes from coredata
|
|
private var alertTypesAccessor:AlertTypesAccessor
|
|
|
|
/// for getting alertEntries from coredata
|
|
private var alertEntriesAccessor:AlertEntriesAccessor
|
|
|
|
/// playSound instance
|
|
private var soundPlayer:SoundPlayer?
|
|
|
|
/// snooze parameters
|
|
private var snoozeParameters = [Int: SnoozeParameters]()
|
|
|
|
/// helper array with all alert notification identifiers
|
|
private var alertNotificationIdentifers = [String]()
|
|
|
|
/// permanent reference to notificationcenter
|
|
private let uNUserNotificationCenter:UNUserNotificationCenter
|
|
|
|
/// snooze times in minutes
|
|
private let snoozeValueMinutes = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 75, 90, 120, 150, 180, 240, 300, 360, 420, 480, 540, 600, 1440, 10080]
|
|
|
|
/// snooze times as shown to the user, actual strings will be replaced during init
|
|
private var snoozeValueStrings = ["5 minutes", "10 minutes", "15 minutes", "20 minutes", "25 minutes", "30 minutes", "35 minutes",
|
|
"40 minutes", "45 minutes", "50 minutes", "55 minutes", "1 hour", "1 hour 15 minutes", "1,5 hours", "2 hours", "2,5 hours", "3 hours", "4 hours",
|
|
"5 hours", "6 hours", "7 hours", "8 hours", "9 hours", "10 hours", "1 day", "1 week"]
|
|
|
|
/// constant for key in ApplicationManager.shared.addClosureToRunWhenAppWillEnterForeground - for closure that will stop playing sound
|
|
private let applicationManagerKeyStopPlayingSound = "AlertManager-stopplayingsound"
|
|
|
|
// MARK: - initializer
|
|
|
|
init(coreDataManager:CoreDataManager, soundPlayer:SoundPlayer?) {
|
|
// initialize properties
|
|
self.bgReadingsAccessor = BgReadingsAccessor(coreDataManager: coreDataManager)
|
|
self.alertTypesAccessor = AlertTypesAccessor(coreDataManager: coreDataManager)
|
|
self.alertEntriesAccessor = AlertEntriesAccessor(coreDataManager: coreDataManager)
|
|
self.calibrationsAccessor = CalibrationsAccessor(coreDataManager: coreDataManager)
|
|
self.sensorsAccessor = SensorsAccessor(coreDataManager: coreDataManager)
|
|
self.soundPlayer = soundPlayer
|
|
self.uNUserNotificationCenter = UNUserNotificationCenter.current()
|
|
|
|
// call super.init
|
|
super.init()
|
|
|
|
// initialize snoozeparameters
|
|
for alertKind in AlertKind.allCases {
|
|
snoozeParameters[alertKind.rawValue] = SnoozeParameters()
|
|
}
|
|
|
|
// in snoozeValueStrings, replace all occurrences of minutes, minute, etc... by language dependent value
|
|
for (index, _) in snoozeValueStrings.enumerated() {
|
|
snoozeValueStrings[index] = snoozeValueStrings[index].replacingOccurrences(of: "minutes", with: Texts_Common.minutes).replacingOccurrences(of: "hour", with: Texts_Common.hour).replacingOccurrences(of: "hours", with: Texts_Common.hours).replacingOccurrences(of: "day", with: Texts_Common.day).replacingOccurrences(of: "week", with: Texts_Common.week)
|
|
}
|
|
|
|
// initialize array of alertNotifications
|
|
initAlertNotificationIdentiferArray()
|
|
|
|
// need to set the alert notification categories, get the existing categories first, call setAlertNotificationCategories in competionhandler
|
|
UNUserNotificationCenter.current().getNotificationCategories(completionHandler: setAlertNotificationCategories(_:))
|
|
|
|
// alertManager may have raised an alert with a sound played by soundplayer. If user brings the app to the foreground, the soundPlayer needs to stop playing
|
|
ApplicationManager.shared.addClosureToRunWhenAppWillEnterForeground(key: applicationManagerKeyStopPlayingSound, closure: {
|
|
if let soundPlayer = soundPlayer {
|
|
soundPlayer.stopPlaying()
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
// MARK: - public functions
|
|
|
|
/// check all alerts and fire if needed
|
|
/// - parameters:
|
|
/// - maxAgeOfLastBgReadingInSeconds : for master mode max 1 minute should be ok, but for follower mode it could be interesting to take a higher value
|
|
public func checkAlerts(maxAgeOfLastBgReadingInSeconds:Double) {
|
|
|
|
// first of all remove all existing notifications, there should be only one open alert on the home screen. The most relevant one will be reraised
|
|
uNUserNotificationCenter.removeDeliveredNotifications(withIdentifiers: alertNotificationIdentifers)
|
|
uNUserNotificationCenter.removeAllPendingNotificationRequests()
|
|
|
|
// get last bgreading, ignore sensor, because it must also work for follower mode
|
|
let latestBgReadings = bgReadingsAccessor.getLatestBgReadings(limit: 2, howOld: nil, forSensor: nil, ignoreRawData: true, ignoreCalculatedValue: false)
|
|
|
|
// get latest calibration
|
|
var lastCalibration:Calibration?
|
|
if let latestSensor = sensorsAccessor.fetchActiveSensor() {
|
|
lastCalibration = calibrationsAccessor.lastCalibrationForActiveSensor(withActivesensor: latestSensor)
|
|
}
|
|
|
|
// get transmitterBatteryInfo
|
|
let transmitterBatteryInfo = UserDefaults.standard.transmitterBatteryInfo
|
|
|
|
// all alerts will only be created if there's a reading, less than 60 seconds old
|
|
// except for transmitterBatteryInfo alert
|
|
if latestBgReadings.count > 0 {
|
|
|
|
let lastBgReading = latestBgReadings[0]
|
|
|
|
if abs(lastBgReading.timeStamp.timeIntervalSinceNow) < maxAgeOfLastBgReadingInSeconds {
|
|
// reading is for an active sensor and is less than 60 seconds old, let's check the alerts
|
|
// need to call checkAlert
|
|
|
|
// if latestBgReadings[1] exists then assign it to lastButOneBgREading
|
|
var lastButOneBgREading:BgReading?
|
|
if latestBgReadings.count > 1 {
|
|
lastButOneBgREading = latestBgReadings[1]
|
|
}
|
|
|
|
|
|
// alerts are checked in order of importance - there should be only one alert raised, except missed reading alert which will always be checked.
|
|
// first check very low alert
|
|
if (!checkAlertAndFire(alertKind: .verylow, lastBgReading: lastBgReading, lastButOneBgREading: lastButOneBgREading, lastCalibration: lastCalibration, transmitterBatteryInfo: transmitterBatteryInfo)) {
|
|
// very low not fired, check low alert - if very low alert snoozed, skip the check for low alert and continue to next step
|
|
if getSnoozeParameters(alertKind: AlertKind.verylow).getSnoozeValue().isSnoozed || (!checkAlertAndFire(alertKind: .low, lastBgReading: lastBgReading, lastButOneBgREading: lastButOneBgREading, lastCalibration: lastCalibration, transmitterBatteryInfo: transmitterBatteryInfo)) {
|
|
// low not fired, check very high alert
|
|
if (!checkAlertAndFire(alertKind: .veryhigh, lastBgReading: lastBgReading, lastButOneBgREading: lastButOneBgREading, lastCalibration: lastCalibration, transmitterBatteryInfo: transmitterBatteryInfo)) {
|
|
// very high not fired, check high alert - if very high alert snoozed, skip the check for high alert and continue to next step
|
|
if getSnoozeParameters(alertKind: AlertKind.veryhigh).getSnoozeValue().isSnoozed || (!checkAlertAndFire(alertKind: .high, lastBgReading: lastBgReading, lastButOneBgREading: lastButOneBgREading, lastCalibration: lastCalibration, transmitterBatteryInfo: transmitterBatteryInfo)) {
|
|
// very high not fired check calibration alert
|
|
if (!checkAlertAndFire(alertKind: .calibration, lastBgReading: lastBgReading, lastButOneBgREading: lastButOneBgREading, lastCalibration: lastCalibration, transmitterBatteryInfo: transmitterBatteryInfo)) {
|
|
// finally let's check the battery level alert
|
|
_ = checkAlertAndFire(alertKind: .batterylow, lastBgReading: lastBgReading, lastButOneBgREading: lastButOneBgREading, lastCalibration: lastCalibration, transmitterBatteryInfo: transmitterBatteryInfo)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// set missed reading alert, this will be a future planned alert
|
|
_ = checkAlertAndFire(alertKind: .missedreading, lastBgReading: lastBgReading, lastButOneBgREading: lastButOneBgREading, lastCalibration: lastCalibration, transmitterBatteryInfo: transmitterBatteryInfo)
|
|
} else {
|
|
trace("in checkAlerts, latestBgReadings is older than %{public}@ minutes", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, maxAgeOfLastBgReadingInSeconds.description)
|
|
}
|
|
} else {
|
|
trace("in checkAlerts, latestBgReadings.count == 0", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info)
|
|
}
|
|
}
|
|
|
|
/// Function to be called that receives the notification actions. Will handle the response. - called when user clicks a notification
|
|
///
|
|
/// this function looks very similar to the function with the same name defined in UNUserNotificationCenterDelegate, difference is that it returns an optional instance of PickerViewData. This will have the snooze data, ie title, actionHandler, cancelHandler, list of values, etc. Goal is not to have UI related stuff in AlertManager class. it's the caller that needs to decide how to present the data
|
|
/// - returns:
|
|
/// - PickerViewData : contains data that user needs to pick from, nil means nothing to pick from
|
|
public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) -> PickerViewData? {
|
|
|
|
// declare returnValue
|
|
var returnValue:PickerViewData?
|
|
|
|
// loop through alertKinds to find matching notificationIdentifier
|
|
loop: for alertKind in AlertKind.allCases {
|
|
|
|
if response.notification.request.identifier == alertKind.notificationIdentifier() {
|
|
|
|
// user clicked an alert notification, time to stop playing if play
|
|
if let soundPlayer = soundPlayer {
|
|
soundPlayer.stopPlaying()
|
|
}
|
|
|
|
switch response.actionIdentifier {
|
|
|
|
case snoozeActionIdentifier:
|
|
|
|
// get the appicable alertEntry so we can find the alertType and default snooze value
|
|
let (currentAlertEntry, _) = alertEntriesAccessor.getCurrentAndNextAlertEntry(forAlertKind: alertKind, forWhen: Date(), alertTypesAccessor: alertTypesAccessor)
|
|
|
|
trace("in userNotificationCenter, received actionIdentifier : snoozeActionIdentifier, snoozing alert %{public}@ for %{public}@ minutes (3)", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging(), Int(currentAlertEntry.alertType.snoozeperiod).description)
|
|
|
|
snooze(alertKind: alertKind, snoozePeriodInMinutes: Int(currentAlertEntry.alertType.snoozeperiod), response: response)
|
|
|
|
case UNNotificationDefaultActionIdentifier:
|
|
|
|
trace("in userNotificationCenter, received actionIdentifier : UNNotificationDefaultActionIdentifier (user clicked the notification which opens the app, but not the snooze action in this notification)", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info)
|
|
|
|
// create pickerViewData for the alertKind for which alert went off, and return it to the caller who in turn needs to allow the user to select a snoozeperiod
|
|
returnValue = createPickerViewData(forAlertKind: alertKind, content: response.notification.request.content)
|
|
|
|
case UNNotificationDismissActionIdentifier:
|
|
trace("in userNotificationCenter, received actionIdentifier : UNNotificationDismissActionIdentifier", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info)
|
|
|
|
// user is swiping away the notification without opening the app, and not choosing the snooze option even if there would be an option to snooze
|
|
// if it's a reading alert (low, high, ...) then it will go off again in 5 minutes
|
|
// if it's a missed reading alert, let's replan it in 5 minutes
|
|
if alertKind == .missedreading {
|
|
snooze(alertKind: .missedreading, snoozePeriodInMinutes: 5, response: response)
|
|
}
|
|
|
|
default:
|
|
|
|
trace("in userNotificationCenter, received actionIdentifier %{public}@", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, response.actionIdentifier)
|
|
|
|
}
|
|
|
|
break loop
|
|
}
|
|
}
|
|
|
|
return returnValue
|
|
}
|
|
|
|
/// Function to be called that receives the notification actions. Will handle the response. completionHandler will not necessarily be called. Only if the identifier (response.notification.request.identifier) is one of the alert notification identifers, then it will handle the response and also call completionhandler.
|
|
/// called when notification created while app is in foreground
|
|
///
|
|
/// this function looks very similar to the UNUserNotificationCenterDelegate function, difference is that it returns an optional instance of PickerViewData. This will have the snooze data, ie title, actionHandler, cancelHandler, list of values, etc. Goal is not to have UI related stuff in AlertManager class. it's the caller that needs to decide how to present the data
|
|
/// - returns:
|
|
/// - PickerViewData : contains data that user needs to pick from, nil means nothing to pick from
|
|
public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) -> PickerViewData? {
|
|
|
|
// declare returnValue
|
|
var returnValue:PickerViewData?
|
|
|
|
/// check if it's for one of the alert notification
|
|
loop: for alertKind in AlertKind.allCases {
|
|
if alertKind.notificationIdentifier() == notification.request.identifier {
|
|
|
|
// it is possible to play the sound, show the content and/or set the badge counter as explained here https://developer.apple.com/documentation/usernotifications/unnotificationpresentationoptions
|
|
// none of them seems useful here
|
|
completionHandler([])
|
|
|
|
// create pickerViewData for the alertKind for which alert went off, and return it to the caller who in turn needs to allow the user to select a snoozeperiod
|
|
returnValue = createPickerViewData(forAlertKind: alertKind, content: notification.request.content)
|
|
|
|
}
|
|
}
|
|
return returnValue
|
|
}
|
|
|
|
// MARK: - private helper functions
|
|
|
|
/// will
|
|
/// - remove any pending missed reading alert
|
|
/// - create a new one repeating, repeat time will be equal to delay of first alert (that's what iOS allows us to do)
|
|
private func scheduleMissedReadingAlert(snoozePeriodInMinutes: Int, content: UNNotificationContent) {
|
|
|
|
// remove any planned missed reading alerts
|
|
uNUserNotificationCenter.removePendingNotificationRequests(withIdentifiers: [AlertKind.missedreading.notificationIdentifier()])
|
|
|
|
// replan missed reading alert, repeating with delay of snoozePeriodInMinutes
|
|
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(snoozePeriodInMinutes * 60), repeats: true)
|
|
|
|
// create the notificationrequest
|
|
let notificationRequest = UNNotificationRequest(identifier: AlertKind.missedreading.notificationIdentifier(), content: content, trigger: trigger)
|
|
|
|
// Add Request to User Notification Center
|
|
uNUserNotificationCenter.add(notificationRequest) { (error) in
|
|
if let error = error {
|
|
trace("Unable to Add Notification Request %{public}@", log: self.log, category: ConstantsLog.categoryAlertManager, type: .error, error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
trace("Scheduled missed reading alert with delay (and repeat) %{public}@ minutes", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, snoozePeriodInMinutes.description)
|
|
|
|
}
|
|
|
|
private func createPickerViewData(forAlertKind alertKind:AlertKind, content: UNNotificationContent) -> PickerViewData {
|
|
|
|
// find the default snooze period, so we can set selectedRow in the pickerviewdata
|
|
let defaultSnoozePeriodInMinutes = Int(alertEntriesAccessor.getCurrentAndNextAlertEntry(forAlertKind: alertKind, forWhen: Date(), alertTypesAccessor: alertTypesAccessor).currentAlertEntry.alertType.snoozeperiod)
|
|
var defaultRow = 0
|
|
for (index, _) in snoozeValueMinutes.enumerated() {
|
|
if snoozeValueMinutes[index] > defaultSnoozePeriodInMinutes {
|
|
break
|
|
} else {
|
|
defaultRow = index
|
|
}
|
|
}
|
|
|
|
return PickerViewData(withMainTitle: alertKind.alertTitle(), withSubTitle: Texts_Alerts.selectSnoozeTime, withData: snoozeValueStrings, selectedRow: defaultRow, withPriority: .high, actionButtonText: Texts_Common.Ok, cancelButtonText: Texts_Common.Cancel,
|
|
onActionClick: {
|
|
(snoozeIndex:Int) -> Void in
|
|
|
|
// if sound is currently playing then stop it
|
|
if let soundPlayer = self.soundPlayer {
|
|
soundPlayer.stopPlaying()
|
|
}
|
|
|
|
// get snooze period
|
|
let snoozePeriod = self.snoozeValueMinutes[snoozeIndex]
|
|
|
|
// snooze
|
|
trace(" snoozing alert %{public}@ for %{public}@ minutes (1)", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging(), snoozePeriod.description)
|
|
self.getSnoozeParameters(alertKind: alertKind).snooze(snoozePeriodInMinutes: snoozePeriod)
|
|
|
|
// if it's a missed reading alert, then cancel any planned missed reading alerts and reschedule
|
|
if alertKind == .missedreading {
|
|
self.scheduleMissedReadingAlert(snoozePeriodInMinutes: snoozePeriod, content: content)
|
|
}
|
|
|
|
},
|
|
onCancelClick: {
|
|
() -> Void in
|
|
if let soundPlayer = self.soundPlayer {
|
|
soundPlayer.stopPlaying()
|
|
}
|
|
}, didSelectRowHandler: nil
|
|
)
|
|
|
|
}
|
|
|
|
/// will check if the alert of type alertKind needs to be fired and also fires it, plays the sound, and if yes return true, otherwise false
|
|
private func checkAlertAndFire(alertKind:AlertKind, lastBgReading:BgReading?, lastButOneBgREading:BgReading?, lastCalibration:Calibration?, transmitterBatteryInfo:TransmitterBatteryInfo?) -> Bool {
|
|
|
|
/// This is only for missed reading alert. How many minutes between now and the moment the snooze expires (meaning when is it not snoozed anymore)
|
|
///
|
|
/// will be initialized later
|
|
var minimumDelayInSecondsToUse:Int?
|
|
|
|
// check if snoozed
|
|
if getSnoozeParameters(alertKind: alertKind).getSnoozeValue().isSnoozed {
|
|
|
|
// depending on alertKind, check if the alert is snoozed. For missedreading, behaviour for snoozed alert is different than for the other alerts
|
|
switch alertKind {
|
|
|
|
case .missedreading:// any alert type that would be configured with a delay
|
|
if let snoozePeriodInMinutes = getSnoozeParameters(alertKind: alertKind).snoozePeriodInMinutes, let snoozeTimeStamp = getSnoozeParameters(alertKind: alertKind).snoozeTimeStamp {
|
|
|
|
minimumDelayInSecondsToUse = -Int(Date().timeIntervalSince(Date(timeInterval: TimeInterval(snoozePeriodInMinutes * 60), since: snoozeTimeStamp)).rawValue)
|
|
trace("in checkAlertAndFire, minimumDelayInSecondsToUse = %{public}@" , log: log, category: ConstantsLog.categoryAlertManager, type: .info, minimumDelayInSecondsToUse!.description)
|
|
|
|
} // if snoozePeriodInMinutes or snoozeTimeStamp is nil (which shouldn't be the case) continue without taking into account the snooze status
|
|
|
|
case .calibration, .batterylow, .low, .high, .verylow, .veryhigh:
|
|
trace("in checkAlertAndFire, alert %{public}@ is currently snoozed", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging())
|
|
return false
|
|
}
|
|
|
|
}
|
|
|
|
|
|
// get the applicable current and next alertType from core data
|
|
let (currentAlertEntry, nextAlertEntry) = alertEntriesAccessor.getCurrentAndNextAlertEntry(forAlertKind: alertKind, forWhen: Date(), alertTypesAccessor: alertTypesAccessor)
|
|
|
|
// check if alert is required
|
|
let (alertNeeded, alertBody, alertTitle, delayInSeconds) = alertKind.alertNeeded(currentAlertEntry: currentAlertEntry, nextAlertEntry: nextAlertEntry, lastBgReading: lastBgReading, lastButOneBgREading, lastCalibration: lastCalibration, transmitterBatteryInfo: transmitterBatteryInfo)
|
|
|
|
// create a new property for delayInSeconds, if it's nil then set to 0 - because returnvalue might either be nil or 0, to be treated in the same way
|
|
var delayInSecondsToUse = delayInSeconds == nil ? 0 : delayInSeconds!
|
|
|
|
// if it's a delayed alert and if the alert is snoozed, then delay is either the momoment the alert expires, or the calculated delay, the maximu of the to values
|
|
if let minimumDelayInSecondsToUse = minimumDelayInSecondsToUse {
|
|
|
|
if minimumDelayInSecondsToUse > delayInSecondsToUse {
|
|
trace(" increasing delayInSecondsToUse to %{public}@, because the alert is snoozed", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, minimumDelayInSecondsToUse.description)
|
|
delayInSecondsToUse = minimumDelayInSecondsToUse
|
|
}
|
|
|
|
}
|
|
|
|
if alertNeeded {
|
|
// alert needs to be raised
|
|
|
|
// the applicable alertentry
|
|
var applicableAlertType = currentAlertEntry.alertType
|
|
|
|
// if delayInSecondsToUse > 0, then possibly we need to use another alertType
|
|
if delayInSecondsToUse > 0, let nextAlertEntry = nextAlertEntry {
|
|
|
|
// if start of nextAlertEntry < start of currentAlertEntry, then ad 24 hours, because it means the nextAlertEntry is actually the one of the day after
|
|
var nextAlertEntryStartValueToUse = nextAlertEntry.start
|
|
if nextAlertEntry.start < currentAlertEntry.start {
|
|
nextAlertEntryStartValueToUse += nextAlertEntryStartValueToUse + 24 * 60
|
|
}
|
|
|
|
// check if current time + delayInSeconds falls within timezone of nextAlertEntry
|
|
if Date().minutesSinceMidNightLocalTime() + delayInSecondsToUse / 60 > nextAlertEntryStartValueToUse {
|
|
applicableAlertType = nextAlertEntry.alertType
|
|
}
|
|
|
|
}
|
|
|
|
// create the content for the alert notification, set body and text, category
|
|
let content = UNMutableNotificationContent()
|
|
|
|
// set body, text
|
|
if let alertBody = alertBody {content.body = alertBody}
|
|
if let alertTitle = alertTitle {content.title = alertTitle}
|
|
|
|
// if snooze from notification in homescreen is needed then set the categoryIdentifier
|
|
if applicableAlertType.snooze {
|
|
content.categoryIdentifier = snoozeCategoryIdentifier
|
|
}
|
|
|
|
// The sound
|
|
// depending on mute override off or on, the sound will either be added to the notification content, or will be played by code here respectively - except if delayInSecondsToUse > 0, in which case we must use the sound in the notification
|
|
//
|
|
// soundToSet is the sound that will be played,
|
|
// if soundToSet is nil ==> then default sound must be used,
|
|
// if soundToSet = "" , empty string ==> no sound needs to be played
|
|
// Start with default sound
|
|
var soundToSet:String?
|
|
|
|
// if applicableAlertType.soundname is nil, then keep soundToSet nil, otherwise find the sound file name
|
|
if let alertTypeSoundName = applicableAlertType.soundname {
|
|
if alertTypeSoundName == "" {
|
|
// no sound to play
|
|
soundToSet = ""
|
|
} else {
|
|
// a sound name has been found in the alertType different from empty string (ie a sound must be played and it's not the default iOS sound)
|
|
// need to find the corresponding sound file name in ConstantsSounds
|
|
// start by setting it to to xdripalert, because the soundname found in the alert type might not be found in the list of sounds stored in the resources (although that shouldn't happen)
|
|
soundToSet = "xdripalert.aif"
|
|
soundloop: for sound in ConstantsSounds.allCases {
|
|
// ConstantsSounds defines available sounds. Per case there a string which is the soundname as shown in the UI and the filename of the sound in the Resources folder, seperated by backslash
|
|
// get array of indexes, of location of "/"
|
|
let indexOfBackSlash = sound.rawValue.indexes(of: "/")
|
|
// define range to get the soundname (as shown in UI)
|
|
let soundNameRange = sound.rawValue.startIndex..<indexOfBackSlash[0]
|
|
// now get the soundName in a string
|
|
let soundName = String(sound.rawValue[soundNameRange])
|
|
// check if it matches the soundname in the alerttype
|
|
if soundName == alertTypeSoundName {
|
|
// get indexOfBackSlash[0] + 1 because we don't need to backslash
|
|
let indexOfBackSlashPlusOne = sound.rawValue.index(after: indexOfBackSlash[0])
|
|
// get the range of the filename where the sound is stored
|
|
let soundFileNameRange = indexOfBackSlashPlusOne..<sound.rawValue.endIndex
|
|
// now get the filename
|
|
soundToSet = String(sound.rawValue[soundFileNameRange])
|
|
break soundloop
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// now we have the name of the file that has the soundfilename, we'll use it later to assign it to the content
|
|
|
|
// if soundToSet == nil, it means user selected the default iOS sound in the alert type, however we don't have the mp3, so if override mute is on and delayInSeconds = nil, then we need to be able to play the sound here with the soundplayer, so we set soundToSet to xdrip sound
|
|
if soundToSet == nil && applicableAlertType.overridemute && delayInSecondsToUse == 0 {
|
|
soundToSet = "xdripalert.aif"
|
|
}
|
|
|
|
// assign the sound to the notification, or play it here, depending on value
|
|
if let soundToSet = soundToSet {
|
|
if soundToSet == "" {
|
|
// no sound to play
|
|
} else {
|
|
// if override mute is on, then play the sound via code here
|
|
// also delayInSeconds must be nil, if delayInSeconds is not nil then we can not play the sound here at now, it must be added to the notification
|
|
if applicableAlertType.overridemute && delayInSecondsToUse == 0 {
|
|
// play the sound
|
|
if let soundPlayer = self.soundPlayer {
|
|
soundPlayer.playSound(soundFileName: soundToSet)
|
|
}
|
|
} else {
|
|
// mute should not be overriden, by adding the sound to the notification, we let iOS decide if the sound will be played or not
|
|
content.sound = UNNotificationSound.init(named: UNNotificationSoundName.init(soundToSet))
|
|
}
|
|
}
|
|
} else {
|
|
// default sound to be played
|
|
content.sound = UNNotificationSound.init(named: UNNotificationSoundName.init(""))
|
|
}
|
|
|
|
// create the trigger, only for notifications with delay
|
|
var trigger:UNTimeIntervalNotificationTrigger?
|
|
if delayInSecondsToUse > 0 {
|
|
// set repeats to true
|
|
trigger = UNTimeIntervalNotificationTrigger(timeInterval: TimeInterval(delayInSecondsToUse), repeats: true)
|
|
}
|
|
|
|
// create the notificationrequest
|
|
let notificationRequest = UNNotificationRequest(identifier: alertKind.notificationIdentifier(), content: content, trigger: trigger)
|
|
|
|
// Add Request to User Notification Center
|
|
uNUserNotificationCenter.add(notificationRequest) { (error) in
|
|
if let error = error {
|
|
trace("Unable to Add Notification Request %{public}@", log: self.log, category: ConstantsLog.categoryAlertManager, type: .error, error.localizedDescription)
|
|
}
|
|
}
|
|
|
|
// if vibrate required , and if delay is nil, then vibrate
|
|
if delayInSecondsToUse == 0, currentAlertEntry.alertType.vibrate {
|
|
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
|
|
}
|
|
|
|
// log the result
|
|
trace("in checkAlert, raising alert %{public}@", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging())
|
|
if delayInSecondsToUse > 0 {
|
|
trace(" delay = %{public}@ seconds, = %{public}@ minutes", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, delayInSecondsToUse.description, ((round(Double(delayInSecondsToUse)/60*10))/10).description)
|
|
}
|
|
|
|
return true
|
|
|
|
} else {
|
|
trace("in checkAlert, there's no need to raise alert %{public}@", log: self.log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging())
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// - if it's a missed reading alert, then reschedule with a delay of snoozePeriodInMinutes, also with a repeat every snoozePeriodInMinutes
|
|
/// - other alerts are snoozed normal
|
|
/// - parameters:
|
|
/// - alertKind
|
|
/// - snoozePeriodInMinutes
|
|
/// - response the UNNotificationResponse received from iOS when user clicks the notification
|
|
private func snooze(alertKind: AlertKind, snoozePeriodInMinutes: Int, response: UNNotificationResponse) {
|
|
|
|
// if it's a missedreading alert, then reschedule the alert with a delay of snoozePeriodInMinutes, repeating, with same content
|
|
if alertKind == .missedreading {
|
|
|
|
scheduleMissedReadingAlert(snoozePeriodInMinutes: snoozePeriodInMinutes, content: response.notification.request.content)
|
|
|
|
} else {
|
|
|
|
// any other type of alert, set it to snoozed
|
|
getSnoozeParameters(alertKind: alertKind).snooze(snoozePeriodInMinutes: snoozePeriodInMinutes)
|
|
trace("Snoozing alert %{public}@ for %{public}@ minutes (2)", log: log, category: ConstantsLog.categoryAlertManager, type: .info, alertKind.descriptionForLogging(), snoozePeriodInMinutes.description)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
private func getSnoozeParameters(alertKind: AlertKind) -> SnoozeParameters {
|
|
if let snoozeParameters = snoozeParameters[alertKind.rawValue] {
|
|
return snoozeParameters
|
|
} else {
|
|
fatalError("in snoozeParameters(alertKind: AlertKind) -> SnoozeParameters, failed to get snoozeparameters for alertKind")
|
|
}
|
|
}
|
|
|
|
// helper method used during intialization of AlertManager
|
|
private func initAlertNotificationIdentiferArray() {
|
|
for alertKind in AlertKind.allCases {
|
|
alertNotificationIdentifers.append(alertKind.notificationIdentifier())
|
|
}
|
|
}
|
|
|
|
/// adds the alert notification categories to the existing categories
|
|
/// - parameters:
|
|
/// - existingCategories : notification categories that currently exist
|
|
func setAlertNotificationCategories(_ existingCategories: Set<UNNotificationCategory>) {
|
|
|
|
// create var equal to existingCategories so we can add new categories
|
|
var mutableExistingCategories = existingCategories
|
|
|
|
// create the snooze action
|
|
let action = UNNotificationAction(identifier: snoozeActionIdentifier, title: Texts_Alerts.snooze, options: [])
|
|
|
|
// create the category - add option customDismissAction, this to make sure userNotificationCenter with didReceive will be called, which in turn will stop the soundPlayer, otherwise the user would dismiss the notification but in case off override mute, the sound keeps on playing
|
|
let generalCategory = UNNotificationCategory(identifier: snoozeCategoryIdentifier, actions: [action], intentIdentifiers: [], options: [.customDismissAction])
|
|
|
|
// add the category to the UNUserNotificationCenter
|
|
mutableExistingCategories.insert(generalCategory)
|
|
|
|
// get UNUserNotificationCenter and set new list of categories
|
|
UNUserNotificationCenter.current().setNotificationCategories(mutableExistingCategories)
|
|
|
|
}
|
|
|
|
}
|