-
Notifications
You must be signed in to change notification settings - Fork 18
Improvements to critical alarm handling #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d7e3385
f2bc27f
5d4622b
a3e03d7
69aec75
dfe5ec8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |
|
|
||
| import SwiftUI | ||
| import HealthKit | ||
| import LibreTransmitter | ||
|
|
||
| private func systemImage(_ name:String) -> some View { | ||
| Image(systemName: name) | ||
|
|
@@ -24,6 +25,7 @@ class AlarmScheduleState: ObservableObject, Identifiable, Hashable { | |
| @Published var lowmgdl: Double = 72 | ||
| @Published var highmgdl: Double = 180 | ||
| @Published var enabled: Bool? = false | ||
| @Published var overrideDoNotDisturb: Bool? = false | ||
|
|
||
| @Published var alarmDateComponents: AlarmTimeCellExternalState = AlarmTimeCellExternalState() | ||
|
|
||
|
|
@@ -114,6 +116,7 @@ class AlarmSettingsState: ObservableObject { | |
| schedule.enabled = storedState.schedules[i].enabled | ||
| schedule.lowmgdl = storedState.schedules[i].lowAlarm ?? -1 | ||
| schedule.highmgdl = storedState.schedules[i].highAlarm ?? -1 | ||
| schedule.overrideDoNotDisturb = storedState.schedules[i].overrideDoNotDisturb | ||
|
|
||
| schedule.alarmDateComponents.startComponents = storedState.schedules[i].from | ||
| schedule.alarmDateComponents.endComponents = storedState.schedules[i].to | ||
|
|
@@ -138,7 +141,9 @@ class AlarmSettingsState: ObservableObject { | |
| glucoseSchedule.highAlarm = newStateSchedule.highmgdl | ||
| glucoseSchedule.from = newStateSchedule.alarmDateComponents.startComponents | ||
| glucoseSchedule.to = newStateSchedule.alarmDateComponents.endComponents | ||
|
|
||
| glucoseSchedule.overrideDoNotDisturb = newStateSchedule.overrideDoNotDisturb | ||
|
|
||
|
|
||
| legacyState.schedules.append(glucoseSchedule) | ||
|
|
||
| } | ||
|
|
@@ -285,6 +290,103 @@ struct AlarmHighRow: View { | |
| } | ||
| } | ||
|
|
||
| struct OverrideDoNotDisturbRow: View { | ||
| @ObservedObject var schedule: AlarmScheduleState | ||
|
|
||
| var body: some View { | ||
| VStack(alignment: .leading, spacing: 4) { | ||
| HStack(alignment: .center) { | ||
| systemImage("bell.badge.fill") | ||
| .frame(maxWidth: 50, alignment: .leading) | ||
| Text(LocalizedString("Override Do Not Disturb", comment: "Text describing that 'Do Not Disturb' will overriden by this alarm")) | ||
| .frame(maxWidth: .infinity, alignment: .leading) | ||
|
|
||
| Toggle("", isOn: Binding<Bool>( | ||
| get: { schedule.overrideDoNotDisturb == true }, | ||
| set: { schedule.overrideDoNotDisturb = $0 } | ||
| )) | ||
| .frame(maxWidth: 50, alignment: .trailing) | ||
| } | ||
|
|
||
| if schedule.overrideDoNotDisturb == true { | ||
| Text(LocalizedString("This alarm will sound even in Do Not Disturb mode", comment: "Text that describes that the alarm will sound even in Do Not Disturb mode")) | ||
| .font(.caption) | ||
|
ETolboom marked this conversation as resolved.
|
||
| .foregroundColor(.secondary) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| struct CriticalAlertsBannerSection: View { | ||
| @Binding var criticalAlertsEnabled: Bool | ||
| @State private var presentableStatus: StatusMessage? | ||
|
|
||
| var body: some View { | ||
| if NotificationHelperOverride.shouldOverrideRequestCriticalPermissions && !criticalAlertsEnabled { | ||
| Section { | ||
| VStack(alignment: .leading, spacing: 8) { | ||
| Label(LocalizedString("Critical Alerts", comment: "Title for the critical alerts banner"), systemImage: "bell.badge") | ||
| .font(.headline) | ||
| Text(LocalizedString("Enable critical alerts so glucose alarms can sound even when Do Not Disturb or silent mode is on.", comment: "Text describing the functionality of the 'Do Not Disturb' toggle")) | ||
| .font(.subheadline) | ||
| .foregroundColor(.secondary) | ||
| Button(action: requestCriticalAlerts) { | ||
| Text(LocalizedString("Enable Critical Alerts", comment: "Button text to request critical alert permissions")) | ||
| .frame(maxWidth: .infinity) | ||
| } | ||
| .buttonStyle(.borderedProminent) | ||
| .tint(.orange) | ||
| .padding(.top, 4) | ||
| } | ||
| .padding(.vertical, 4) | ||
| } | ||
| .alert(item: $presentableStatus) { status in | ||
| Alert(title: Text(status.title), message: Text(status.message), dismissButton: .default(Text(LocalizedString("Got it!", comment: "Dismiss button for critical alerts permission alert")))) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private func requestCriticalAlerts() { | ||
| NotificationHelper.requestCriticalAlertPermission { enabled in | ||
| criticalAlertsEnabled = enabled | ||
| if !enabled { | ||
| presentableStatus = StatusMessage( | ||
| title: LocalizedString("Could Not Enable Critical Alerts", comment: "Alert title when critical alert permission was not granted"), | ||
| message: LocalizedString("Critical alerts were not enabled. If you denied the permission prompt, you can enable them in iOS Settings > Notifications for this app. If this is a development build, also make sure the app has the critical alerts entitlement and that the provisioning profile supports it.", comment: "Alert message explaining how to enable critical alerts if permission was denied") | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| struct CriticalAlarmsVolumeSection: View { | ||
| private enum Key: String { | ||
| case mmCriticalAlarmsVolume = "com.loopkit.libreCriticalAlarmsVolume" | ||
| } | ||
|
|
||
| @AppStorage(Key.mmCriticalAlarmsVolume.rawValue) var mmCriticalAlarmsVolume: Double = 60 | ||
| @State private var isEditing = false | ||
|
|
||
| private var intVolume: Int { | ||
| Int(mmCriticalAlarmsVolume) | ||
| } | ||
|
|
||
| var body: some View { | ||
| Section(header: Text(LocalizedString("Critical alarm volume", comment: "Header describing the volume of the critical alerts")), footer: Text(LocalizedString("Critical alarms will always be sent with volume at minimum 60%", comment: "Text describing that critical alerts are sent at a minimum volume of 60%"))) { | ||
| Slider( | ||
| value: $mmCriticalAlarmsVolume, | ||
| in: 60...100, | ||
| step: 5, | ||
| onEditingChanged: { editing in | ||
| isEditing = editing | ||
| } | ||
| ) | ||
| Text("\(intVolume)%") | ||
| .foregroundColor(isEditing ? .red : .blue) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| struct AlarmSettingsView: View { | ||
|
|
||
| private(set) var glucoseUnit: HKUnit | ||
|
|
@@ -304,6 +406,8 @@ struct AlarmSettingsView: View { | |
| // for accessing the alarm section | ||
| @State private var requiresAuthentication = Features.alarmSettingsViewRequiresAuthentication | ||
|
|
||
| @State private var criticalAlertsEnabled = false | ||
|
|
||
| var body: some View { | ||
| erasedWithKeyboardDismissal(list) | ||
| .alert(item: $presentableStatus) { status in | ||
|
|
@@ -318,10 +422,18 @@ struct AlarmSettingsView: View { | |
| } | ||
| } | ||
|
|
||
| checkCriticalAlertStatus() | ||
|
|
||
| } | ||
| .disabled(requiresAuthentication ? !authSuccess : false) | ||
| } | ||
|
|
||
| private func checkCriticalAlertStatus() { | ||
| NotificationHelper.checkCriticalAlertStatus { enabled in | ||
| criticalAlertsEnabled = enabled | ||
| } | ||
| } | ||
|
|
||
| func erasedWithKeyboardDismissal(_ view: any View) -> AnyView { | ||
| if #available(iOS 16.0, *) { | ||
| return AnyView(view.scrollDismissesKeyboard(.immediately)) | ||
|
|
@@ -333,19 +445,27 @@ struct AlarmSettingsView: View { | |
| @StateObject var errorReporter = FormErrorState() | ||
|
|
||
| var list: some View { | ||
|
|
||
| List { | ||
| CriticalAlertsBannerSection(criticalAlertsEnabled: $criticalAlertsEnabled) | ||
|
|
||
| ForEach(Array(alarmState.schedules.enumerated()), id: \.1) { i, schedule in | ||
| Section(header: Text(LocalizedString("Schedule ", comment: "Text describing schedule in alarmsettingsview") + "\(i+1)")) { | ||
| AlarmDateRow(schedule: schedule, tag: i, subviewSelection: $subviewSelection) | ||
| AlarmLowRow(schedule: schedule, glucoseUnit: glucoseUnit, glucoseUnitDesc: glucoseUnitDesc, errorReporter: errorReporter) | ||
| AlarmHighRow(schedule: schedule, glucoseUnit: glucoseUnit, glucoseUnitDesc: glucoseUnitDesc, errorReporter: errorReporter) | ||
| if criticalAlertsEnabled { | ||
| OverrideDoNotDisturbRow(schedule: schedule) | ||
| } | ||
|
Comment on lines
455
to
+458
|
||
|
|
||
| }.onTapGesture { | ||
| self.hideKeyboardPreIos16() | ||
| } | ||
|
|
||
| } | ||
|
|
||
| if criticalAlertsEnabled { | ||
| CriticalAlarmsVolumeSection() | ||
| } | ||
|
|
||
| Section { | ||
| Button("Save") { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
requestCriticalAlertPermissiondiscards thegrantedanderrorvalues returned byrequestAuthorization. Using those values would allow the UI to show a more accurate message (e.g. denied vs. missing entitlement) and avoid an extra round-trip in some cases.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is meaningful.
requestCriticalAlertPermission is called from the CriticalAlertsBannerSection which already shows a proper message in case the result is not
.enabledIf the user denied it then it is clear what the reason is. Otherwise there is only possible reason left: the app was not built with the correct entitlements.