Przeglądaj źródła

test app badge ang glucose notification

Ivan Valkou 4 lat temu
rodzic
commit
78813d9183
22 zmienionych plików z 381 dodań i 59 usunięć
  1. 3 3
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/NotificationHelper.swift
  2. 1 4
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/LibreTransmitterManager+UI.swift
  3. 1 5
      Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/Views/Settings/SettingsView.swift
  4. 44 0
      FreeAPS.xcodeproj/project.pbxproj
  5. 3 1
      FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json
  6. 30 0
      FreeAPS/Sources/APS/Storage/GlucoseStorage.swift
  7. 1 26
      FreeAPS/Sources/Application/AppDelegate.swift
  8. 2 1
      FreeAPS/Sources/Application/FreeAPSApp.swift
  9. 2 1
      FreeAPS/Sources/Assemblies/ServiceAssembly.swift
  10. 10 0
      FreeAPS/Sources/Models/FreeAPSSettings.swift
  11. 0 2
      FreeAPS/Sources/Modules/Home/HomeStateModel.swift
  12. 0 5
      FreeAPS/Sources/Modules/LibreConfig/View/LibreConfigRootView.swift
  13. 0 8
      FreeAPS/Sources/Modules/Main/View/MainRootView.swift
  14. 5 0
      FreeAPS/Sources/Modules/NotificationsConfig/NotificationsConfigDataFlow.swift
  15. 3 0
      FreeAPS/Sources/Modules/NotificationsConfig/NotificationsConfigProvider.swift
  16. 22 0
      FreeAPS/Sources/Modules/NotificationsConfig/NotificationsConfigStateModel.swift
  17. 20 0
      FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift
  18. 1 0
      FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift
  19. 3 0
      FreeAPS/Sources/Router/Screen.swift
  20. 25 2
      FreeAPS/Sources/Services/Calendar/CalendarManager.swift
  21. 1 1
      FreeAPS/Sources/Services/SettingsManager/SettingsManager.swift
  22. 204 0
      FreeAPS/Sources/Services/UserNotifiactions/UserNotificationsManager.swift

+ 3 - 3
Dependencies/LibreTransmitter/Sources/LibreTransmitter/Common/NotificationHelper.swift

@@ -244,9 +244,9 @@ public enum NotificationHelper {
 
             content.title = titles.joined(separator: " ")
             content.body = body.joined(separator: ", ") + body2s
-            addRequest(identifier: .glucocoseNotifications,
-                       content: content,
-                       deleteOld: true)
+//            addRequest(identifier: .glucocoseNotifications,
+//                       content: content,
+//                       deleteOld: true)
         }
     }
 

+ 1 - 4
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/LibreTransmitterManager+UI.swift

@@ -62,14 +62,12 @@ public struct LibreTransmitterSettingsView: UIViewControllerRepresentable {
     private let glucoseUnit: HKUnit
     private let delete: (() -> Void)?
     private let completion: (() -> Void)?
-    private let openSnooze: Bool
 
-    public init(manager: LibreTransmitterManager, openSnooze: Bool = false, glucoseUnit: HKUnit, delete: (() -> Void)? = nil , completion: (() -> Void)? = nil) {
+    public init(manager: LibreTransmitterManager, glucoseUnit: HKUnit, delete: (() -> Void)? = nil , completion: (() -> Void)? = nil) {
         self.manager = manager
         self.glucoseUnit = glucoseUnit
         self.delete = delete
         self.completion = completion
-        self.openSnooze = openSnooze
     }
 
     public func makeUIViewController(context: Context) -> UIViewController {
@@ -78,7 +76,6 @@ public struct LibreTransmitterSettingsView: UIViewControllerRepresentable {
 
         let settings = SettingsView.asHostedViewController(
             glucoseUnit: glucoseUnit,
-            openSnooze: openSnooze,
             //displayGlucoseUnitObservable: displayGlucoseUnitObservable,
             notifyComplete: doneNotifier,
             notifyDelete: wantToTerminateNotifier,

+ 1 - 5
Dependencies/LibreTransmitter/Sources/LibreTransmitter/LibreTransmitterUI/Views/Settings/SettingsView.swift

@@ -94,8 +94,6 @@ class SettingsModel : ObservableObject {
 }
 
 struct SettingsView: View {
-    @State var openSnooze = false
-
     //@ObservedObject private var displayGlucoseUnitObservable: DisplayGlucoseUnitObservable
     @ObservedObject private var transmitterInfo: LibreTransmitter.TransmitterInfo
     @ObservedObject private var sensorInfo: LibreTransmitter.SensorInfo
@@ -118,7 +116,6 @@ struct SettingsView: View {
 
     static func asHostedViewController(
         glucoseUnit: HKUnit,
-        openSnooze: Bool,
         //displayGlucoseUnitObservable: DisplayGlucoseUnitObservable,
         notifyComplete: GenericObservableObject,
         notifyDelete: GenericObservableObject,
@@ -128,7 +125,6 @@ struct SettingsView: View {
         alarmStatus: LibreTransmitter.AlarmStatus) -> UIHostingController<SettingsView> {
         UIHostingController(rootView: self.init(
             //displayGlucoseUnitObservable: displayGlucoseUnitObservable,
-            openSnooze: openSnooze,
             transmitterInfo: transmitterInfoObservable, sensorInfo: sensorInfoObervable, glucoseMeasurement: glucoseInfoObservable, notifyComplete: notifyComplete, notifyDelete: notifyDelete, alarmStatus: alarmStatus, glucoseUnit: glucoseUnit
 
         ))
@@ -189,7 +185,7 @@ struct SettingsView: View {
 
     var snoozeSection: some View {
         Section {
-            NavigationLink(destination: SnoozeView(isAlarming: $alarmStatus.isAlarming, activeAlarms: $alarmStatus.glucoseScheduleAlarmResult), isActive: $openSnooze) {
+            NavigationLink(destination: SnoozeView(isAlarming: $alarmStatus.isAlarming, activeAlarms: $alarmStatus.glucoseScheduleAlarmResult)) {
                 if alarmStatus.isAlarming {
                     Text("Snooze Alerts").frame(alignment: .center)
                         .padding(.top, 30)

+ 44 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -10,6 +10,7 @@
 		041D1E995A6AE92E9289DC49 /* BolusDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8D1A7CA8C10C4403D4BBFA7 /* BolusDataFlow.swift */; };
 		0CEA2EA070AB041AF3E3745B /* BolusRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 10A0C32B0DAB52726EF9B6D9 /* BolusRootView.swift */; };
 		0D9A5E34A899219C5C4CDFAF /* DataTableStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9455FA2D92E77A6C4AFED8A3 /* DataTableStateModel.swift */; };
+		0F7A65FBD2CD8D6477ED4539 /* NotificationsConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = E625985B47742D498CB1681A /* NotificationsConfigProvider.swift */; };
 		17A9D0899046B45E87834820 /* CREditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C8D5F457B5AFF763F8CF3DF /* CREditorProvider.swift */; };
 		1927C8E62744606D00347C69 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1927C8E82744606D00347C69 /* InfoPlist.strings */; };
 		198377D2266BFFF6004DE65E /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 198377D4266BFFF6004DE65E /* Localizable.strings */; };
@@ -19,6 +20,7 @@
 		28089E07169488CF6DCC2A31 /* AddCarbsRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86FC1CFD647CF34508AF9A3B /* AddCarbsRootView.swift */; };
 		2BE9A6FA20875F6F4F9CD461 /* PumpSettingsEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97F14812C1AFED3621165A5 /* PumpSettingsEditorProvider.swift */; };
 		3083261C4B268E353F36CD0B /* AutotuneConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCCCCE633F5E98E41B0CD3C /* AutotuneConfigDataFlow.swift */; };
+		3171D2818C7C72CD1584BB5E /* NotificationsConfigStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC2C6489D29ECCCAD78E0721 /* NotificationsConfigStateModel.swift */; };
 		320D030F724170A637F06D50 /* CalibrationsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 212E8BFE6D66EE65AA26A114 /* CalibrationsProvider.swift */; };
 		33E198D3039045D98C3DC5D4 /* AddCarbsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39E7C997E56DAF8D28D09014 /* AddCarbsStateModel.swift */; };
 		3811DE0B25C9D32F00A708ED /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3811DE0725C9D32E00A708ED /* BaseView.swift */; };
@@ -176,6 +178,7 @@
 		38E4453D274E411700EC9A94 /* Disk+Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E44533274E411700EC9A94 /* Disk+Errors.swift */; };
 		38E87401274F77E400975559 /* CoreNFC.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 38E873FD274F761800975559 /* CoreNFC.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
 		38E87403274F78C000975559 /* libswiftCoreNFC.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 38E87402274F78C000975559 /* libswiftCoreNFC.tbd */; settings = {ATTRIBUTES = (Weak, ); }; };
+		38E87408274F9AD000975559 /* UserNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E87407274F9AD000975559 /* UserNotificationsManager.swift */; };
 		38E989DD25F5021400C0CED0 /* PumpStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E989DC25F5021400C0CED0 /* PumpStatus.swift */; };
 		38E98A2325F52C9300C0CED0 /* Signpost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A1B25F52C9300C0CED0 /* Signpost.swift */; };
 		38E98A2425F52C9300C0CED0 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38E98A1C25F52C9300C0CED0 /* Logger.swift */; };
@@ -242,6 +245,7 @@
 		BF1667ADE69E4B5B111CECAE /* ManualTempBasalProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680C4420C9A345D46D90D06C /* ManualTempBasalProvider.swift */; };
 		C967DACD3B1E638F8B43BE06 /* ManualTempBasalStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */; };
 		CA370FC152BC98B3D1832968 /* BasalProfileEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8BCB0C37DEB5EC377B9612 /* BasalProfileEditorRootView.swift */; };
+		CD78BB94E43B249D60CC1A1B /* NotificationsConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22963BD06A9C83959D4914E4 /* NotificationsConfigRootView.swift */; };
 		D2165E9D78EFF692C1DED1C6 /* AddTempTargetDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8A42073A2D03A278914448 /* AddTempTargetDataFlow.swift */; };
 		D6D02515BBFBE64FEBE89856 /* DataTableRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* DataTableRootView.swift */; };
 		D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */; };
@@ -258,6 +262,7 @@
 		E13B7DAB2A435F57066AF02E /* TargetsEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36F58DDD71F0E795464FA3F0 /* TargetsEditorStateModel.swift */; };
 		E25073BC86C11C3D6A42F5AC /* CalibrationsStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47DFCE895C930F784EF11843 /* CalibrationsStateModel.swift */; };
 		E39E418C56A5A46B61D960EE /* ConfigEditorStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D5B4F8B4194BB7E260EF251 /* ConfigEditorStateModel.swift */; };
+		E3A08AAE59538BC8A8ABE477 /* NotificationsConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3260468377DA9DB4DEE9AF6D /* NotificationsConfigDataFlow.swift */; };
 		E4984C5262A90469788754BB /* PreferencesEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8BA8533F56BC55748CA877 /* PreferencesEditorProvider.swift */; };
 		E97285ED9B814CD5253C6658 /* AddCarbsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F48C3AC770D4CCD0EA2B0C2 /* AddCarbsDataFlow.swift */; };
 		E974172296125A5AE99E634C /* PumpConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AD22C985B79A2F0D2EA3D9D /* PumpConfigRootView.swift */; };
@@ -350,8 +355,10 @@
 		199732B5271B9EE900129A3F /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
 		212E8BFE6D66EE65AA26A114 /* CalibrationsProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CalibrationsProvider.swift; sourceTree = "<group>"; };
 		223EC0494F55A91E3EA69EF4 /* BolusStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BolusStateModel.swift; sourceTree = "<group>"; };
+		22963BD06A9C83959D4914E4 /* NotificationsConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationsConfigRootView.swift; sourceTree = "<group>"; };
 		2AD22C985B79A2F0D2EA3D9D /* PumpConfigRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigRootView.swift; sourceTree = "<group>"; };
 		2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigDataFlow.swift; sourceTree = "<group>"; };
+		3260468377DA9DB4DEE9AF6D /* NotificationsConfigDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationsConfigDataFlow.swift; sourceTree = "<group>"; };
 		36F58DDD71F0E795464FA3F0 /* TargetsEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorStateModel.swift; sourceTree = "<group>"; };
 		3811DE0725C9D32E00A708ED /* BaseView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseView.swift; sourceTree = "<group>"; };
 		3811DE0825C9D32F00A708ED /* BaseProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseProvider.swift; sourceTree = "<group>"; };
@@ -495,6 +502,7 @@
 		38E44533274E411700EC9A94 /* Disk+Errors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Disk+Errors.swift"; sourceTree = "<group>"; };
 		38E873FD274F761800975559 /* CoreNFC.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreNFC.framework; path = System/Library/Frameworks/CoreNFC.framework; sourceTree = SDKROOT; };
 		38E87402274F78C000975559 /* libswiftCoreNFC.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libswiftCoreNFC.tbd; path = usr/lib/swift/libswiftCoreNFC.tbd; sourceTree = SDKROOT; };
+		38E87407274F9AD000975559 /* UserNotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationsManager.swift; sourceTree = "<group>"; };
 		38E989DC25F5021400C0CED0 /* PumpStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpStatus.swift; sourceTree = "<group>"; };
 		38E98A1B25F52C9300C0CED0 /* Signpost.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Signpost.swift; sourceTree = "<group>"; };
 		38E98A1C25F52C9300C0CED0 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = "<group>"; };
@@ -578,6 +586,7 @@
 		D295A3F870E826BE371C0BB5 /* AutotuneConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AutotuneConfigStateModel.swift; sourceTree = "<group>"; };
 		D97F14812C1AFED3621165A5 /* PumpSettingsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpSettingsEditorProvider.swift; sourceTree = "<group>"; };
 		DA241FB1663EC96FDBE64C8A /* CalibrationsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CalibrationsDataFlow.swift; sourceTree = "<group>"; };
+		DC2C6489D29ECCCAD78E0721 /* NotificationsConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationsConfigStateModel.swift; sourceTree = "<group>"; };
 		E00EEBFD27368630002FF094 /* ServiceAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServiceAssembly.swift; sourceTree = "<group>"; };
 		E00EEBFE27368630002FF094 /* SecurityAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecurityAssembly.swift; sourceTree = "<group>"; };
 		E00EEBFF27368630002FF094 /* StorageAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageAssembly.swift; sourceTree = "<group>"; };
@@ -586,6 +595,7 @@
 		E00EEC0227368630002FF094 /* NetworkAssembly.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkAssembly.swift; sourceTree = "<group>"; };
 		E013D871273AC6FE0014109C /* GlucoseSimulatorSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseSimulatorSource.swift; sourceTree = "<group>"; };
 		E2EBA7C03C26FCC67E16D798 /* LibreConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigProvider.swift; sourceTree = "<group>"; };
+		E625985B47742D498CB1681A /* NotificationsConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NotificationsConfigProvider.swift; sourceTree = "<group>"; };
 		E68CDC1E5C438D1BEAD4CF24 /* LibreConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LibreConfigStateModel.swift; sourceTree = "<group>"; };
 		E9AAB83FB6C3B41EFD1846A0 /* AddTempTargetRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AddTempTargetRootView.swift; sourceTree = "<group>"; };
 		FBB3BAE7494CB771ABAC7B8B /* ISFEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorRootView.swift; sourceTree = "<group>"; };
@@ -708,6 +718,7 @@
 				3811DE1A25C9D48300A708ED /* Main */,
 				5031FE61F63C2A8A8B7674DD /* ManualTempBasal */,
 				D533BF261CDC1C3F871E7BFD /* NightscoutConfig */,
+				F66B236E00924A05D6A9F9DF /* NotificationsConfig */,
 				3E1C41D9301B7058AA7BF5EA /* PreferencesEditor */,
 				99C01B871ACAB3F32CE755C7 /* PumpConfig */,
 				E493126EA71765130F64CCE5 /* PumpSettingsEditor */,
@@ -817,6 +828,7 @@
 		3811DE9125C9D88200A708ED /* Services */ = {
 			isa = PBXGroup;
 			children = (
+				38E87406274F9AA500975559 /* UserNotifiactions */,
 				3862CC2C2743F9DC00BF832C /* Calendar */,
 				38AEE75025F021F10013F05B /* SettingsManager */,
 				38B4F3C425E5016800E76A18 /* Notifications */,
@@ -1193,6 +1205,14 @@
 			path = Disk;
 			sourceTree = "<group>";
 		};
+		38E87406274F9AA500975559 /* UserNotifiactions */ = {
+			isa = PBXGroup;
+			children = (
+				38E87407274F9AD000975559 /* UserNotificationsManager.swift */,
+			);
+			path = UserNotifiactions;
+			sourceTree = "<group>";
+		};
 		38E98A1A25F52C9300C0CED0 /* Logger */ = {
 			isa = PBXGroup;
 			children = (
@@ -1535,6 +1555,25 @@
 			path = View;
 			sourceTree = "<group>";
 		};
+		F5DE2E6D7B2133BBD3353DC7 /* View */ = {
+			isa = PBXGroup;
+			children = (
+				22963BD06A9C83959D4914E4 /* NotificationsConfigRootView.swift */,
+			);
+			path = View;
+			sourceTree = "<group>";
+		};
+		F66B236E00924A05D6A9F9DF /* NotificationsConfig */ = {
+			isa = PBXGroup;
+			children = (
+				3260468377DA9DB4DEE9AF6D /* NotificationsConfigDataFlow.swift */,
+				E625985B47742D498CB1681A /* NotificationsConfigProvider.swift */,
+				DC2C6489D29ECCCAD78E0721 /* NotificationsConfigStateModel.swift */,
+				F5DE2E6D7B2133BBD3353DC7 /* View */,
+			);
+			path = NotificationsConfig;
+			sourceTree = "<group>";
+		};
 		F75CB57ED6971B46F8756083 /* CGM */ = {
 			isa = PBXGroup;
 			children = (
@@ -1796,6 +1835,7 @@
 				3871F39C25ED892B0013ECB5 /* TempTarget.swift in Sources */,
 				3811DEAB25C9D88300A708ED /* HTTPResponseStatus.swift in Sources */,
 				3811DE5F25C9D4D500A708ED /* ProgressBar.swift in Sources */,
+				38E87408274F9AD000975559 /* UserNotificationsManager.swift in Sources */,
 				38BF021D25E7E3AF00579895 /* Reservoir.swift in Sources */,
 				38BF021B25E7D06400579895 /* PumpSettingsView.swift in Sources */,
 				3862CC05273D152B00BF832C /* CalibrationService.swift in Sources */,
@@ -1918,6 +1958,10 @@
 				320D030F724170A637F06D50 /* CalibrationsProvider.swift in Sources */,
 				E25073BC86C11C3D6A42F5AC /* CalibrationsStateModel.swift in Sources */,
 				BA90041DC8991147E5C8C3AA /* CalibrationsRootView.swift in Sources */,
+				E3A08AAE59538BC8A8ABE477 /* NotificationsConfigDataFlow.swift in Sources */,
+				0F7A65FBD2CD8D6477ED4539 /* NotificationsConfigProvider.swift in Sources */,
+				3171D2818C7C72CD1584BB5E /* NotificationsConfigStateModel.swift in Sources */,
+				CD78BB94E43B249D60CC1A1B /* NotificationsConfigRootView.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 3 - 1
FreeAPS/Resources/json/defaults/freeaps/freeaps_settings.json

@@ -10,5 +10,7 @@
     "insulinReqFraction": 0.7,
     "skipBolusScreenAfterCarbs": false,
     "cgm": "nightscout",
-    "uploadGlucose": false
+    "uploadGlucose": false,
+    "glucoseBadge": false,
+    "glucoseNotifications": false
 }

+ 30 - 0
FreeAPS/Sources/APS/Storage/GlucoseStorage.swift

@@ -11,6 +11,7 @@ protocol GlucoseStorage {
     func isGlucoseFresh() -> Bool
     func isGlucoseNotFlat() -> Bool
     func nightscoutGlucoseNotUploaded() -> [BloodGlucose]
+    var alarm: GlucoseAlarm? { get }
 }
 
 final class BaseGlucoseStorage: GlucoseStorage, Injectable {
@@ -100,8 +101,37 @@ final class BaseGlucoseStorage: GlucoseStorage, Injectable {
 
         return Array(Set(recentGlucose).subtracting(Set(uploaded)))
     }
+
+    var alarm: GlucoseAlarm? {
+        guard let glucose = recent().last, glucose.dateString.addingTimeInterval(20.minutes.timeInterval) > Date(),
+              let glucoseValue = glucose.glucose else { return nil }
+
+        if glucoseValue < 72 {
+            return .low
+        }
+
+        if glucoseValue > 270 {
+            return .high
+        }
+
+        return nil
+    }
 }
 
 protocol GlucoseObserver {
     func glucoseDidUpdate(_ glucose: [BloodGlucose])
 }
+
+enum GlucoseAlarm {
+    case high
+    case low
+
+    var displayName: String {
+        switch self {
+        case .high:
+            return NSLocalizedString("LOWALERT!", comment: "LOWALERT!")
+        case .low:
+            return NSLocalizedString("HIGHALERT!", comment: "HIGHALERT!")
+        }
+    }
+}

+ 1 - 26
FreeAPS/Sources/Application/AppDelegate.swift

@@ -1,29 +1,4 @@
 import SwiftUI
 import UIKit
-import UserNotifications
 
-class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
-    @Published var notificationAction: NotificationAction? = nil
-
-    func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
-        UNUserNotificationCenter.current().delegate = self
-        return true
-    }
-}
-
-extension AppDelegate: UNUserNotificationCenterDelegate {
-    func userNotificationCenter(
-        _: UNUserNotificationCenter,
-        didReceive response: UNNotificationResponse,
-        withCompletionHandler completionHandler: @escaping () -> Void
-    ) {
-        if let action = response.notification.request.content.userInfo["action"] as? String {
-            notificationAction = NotificationAction(rawValue: action)
-        }
-        completionHandler()
-    }
-}
-
-enum NotificationAction: String {
-    case snoozeAlert = "snooze"
-}
+class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject {}

+ 2 - 1
FreeAPS/Sources/Application/FreeAPSApp.swift

@@ -35,6 +35,8 @@ import Swinject
         _ = resolver.resolve(FetchGlucoseManager.self)!
         _ = resolver.resolve(FetchTreatmentsManager.self)!
         _ = resolver.resolve(FetchAnnouncementsManager.self)!
+        _ = resolver.resolve(CalendarManager.self)!
+        _ = resolver.resolve(UserNotificationsManager.self)!
     }
 
     init() {
@@ -44,7 +46,6 @@ import Swinject
     var body: some Scene {
         WindowGroup {
             Main.RootView(resolver: resolver)
-                .environmentObject(appDelegate)
         }
         .onChange(of: scenePhase) { newScenePhase in
             debug(.default, "APPLICATION PHASE: \(newScenePhase)")

+ 2 - 1
FreeAPS/Sources/Assemblies/ServiceAssembly.swift

@@ -13,6 +13,7 @@ final class ServiceAssembly: Assembly {
             reporter.setup()
             return reporter
         }
-        container.register(CalendarManager.self) { r in BaseCalendarManager(resilver: r) }
+        container.register(CalendarManager.self) { r in BaseCalendarManager(resolver: r) }
+        container.register(UserNotificationsManager.self) { r in BaseUserNotificationsManager(resolver: r) }
     }
 }

+ 10 - 0
FreeAPS/Sources/Models/FreeAPSSettings.swift

@@ -14,6 +14,8 @@ struct FreeAPSSettings: JSON, Equatable {
     var cgm: CGMType = .nightscout
     var uploadGlucose: Bool = false
     var useCalendar: Bool = false
+    var glucoseBadge: Bool = false
+    var glucoseNotifications: Bool = false
 }
 
 extension FreeAPSSettings: Decodable {
@@ -74,6 +76,14 @@ extension FreeAPSSettings: Decodable {
             settings.useCalendar = useCalendar
         }
 
+        if let glucoseBadge = try? container.decode(Bool.self, forKey: .glucoseBadge) {
+            settings.glucoseBadge = glucoseBadge
+        }
+
+        if let glucoseNotifications = try? container.decode(Bool.self, forKey: .glucoseNotifications) {
+            settings.glucoseNotifications = glucoseNotifications
+        }
+
         self = settings
     }
 }

+ 0 - 2
FreeAPS/Sources/Modules/Home/HomeStateModel.swift

@@ -8,7 +8,6 @@ extension Home {
         @Injected() var settingsManager: SettingsManager!
         @Injected() var apsManager: APSManager!
         @Injected() var nightscoutManager: NightscoutManager!
-        @Injected() var calendarManager: CalendarManager!
         private let timer = DispatchTimer(timeInterval: 5)
         private(set) var filteredHours = 24
 
@@ -166,7 +165,6 @@ extension Home {
                 } else {
                     self.glucoseDelta = nil
                 }
-                self.calendarManager.createEvent(for: self.recentGlucose, delta: self.glucoseDelta)
             }
         }
 

+ 0 - 5
FreeAPS/Sources/Modules/LibreConfig/View/LibreConfigRootView.swift

@@ -6,14 +6,12 @@ extension LibreConfig {
     struct RootView: BaseView {
         let resolver: Resolver
         @StateObject var state = StateModel()
-        @EnvironmentObject var appDelegate: AppDelegate
 
         var body: some View {
             Group {
                 if state.configured, let manager = state.source.manager {
                     LibreTransmitterSettingsView(
                         manager: manager,
-                        openSnooze: appDelegate.notificationAction == .snoozeAlert,
                         glucoseUnit: state.unit
                     ) {
                         self.state.source.manager = nil
@@ -21,9 +19,6 @@ extension LibreConfig {
                     } completion: {
                         state.hideModal()
                     }
-                    .onAppear {
-                        appDelegate.notificationAction = nil
-                    }
                 } else {
                     LibreTransmitterSetupView { manager in
                         self.state.source.manager = manager

+ 0 - 8
FreeAPS/Sources/Modules/Main/View/MainRootView.swift

@@ -5,7 +5,6 @@ extension Main {
     struct RootView: BaseView {
         let resolver: Resolver
         @StateObject var state = StateModel()
-        @EnvironmentObject var appDelegate: AppDelegate
 
         var body: some View {
             router.view(for: .home)
@@ -24,13 +23,6 @@ extension Main {
                     )
                 }
                 .onAppear(perform: configureView)
-                .onReceive(appDelegate.$notificationAction) { action in
-                    switch action {
-                    case .snoozeAlert:
-                        state.showModal(for: .libreConfig)
-                    default: break
-                    }
-                }
         }
     }
 }

+ 5 - 0
FreeAPS/Sources/Modules/NotificationsConfig/NotificationsConfigDataFlow.swift

@@ -0,0 +1,5 @@
+enum NotificationsConfig {
+    enum Config {}
+}
+
+protocol NotificationsConfigProvider {}

+ 3 - 0
FreeAPS/Sources/Modules/NotificationsConfig/NotificationsConfigProvider.swift

@@ -0,0 +1,3 @@
+extension NotificationsConfig {
+    final class Provider: BaseProvider, NotificationsConfigProvider {}
+}

+ 22 - 0
FreeAPS/Sources/Modules/NotificationsConfig/NotificationsConfigStateModel.swift

@@ -0,0 +1,22 @@
+import SwiftUI
+
+extension NotificationsConfig {
+    final class StateModel: BaseStateModel<Provider> {
+        @Injected() private var settingsManager: SettingsManager!
+
+        @Published var glucoseBadge = false
+
+        override func subscribe() {
+            glucoseBadge = settingsManager.settings.glucoseBadge
+
+            $glucoseBadge
+                .removeDuplicates()
+                .assign(to: \.settings.glucoseBadge, on: settingsManager)
+                .store(in: &lifetime)
+        }
+
+        deinit {
+            print("OK")
+        }
+    }
+}

+ 20 - 0
FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift

@@ -0,0 +1,20 @@
+import SwiftUI
+import Swinject
+
+extension NotificationsConfig {
+    struct RootView: BaseView {
+        let resolver: Resolver
+        @StateObject var state = StateModel()
+
+        var body: some View {
+            Form {
+                Section(header: Text("Glucose")) {
+                    Toggle("Show glucose on the app badge", isOn: $state.glucoseBadge)
+                }
+            }
+            .onAppear(perform: configureView)
+            .navigationBarTitle("Notifications")
+            .navigationBarTitleDisplayMode(.automatic)
+        }
+    }
+}

+ 1 - 0
FreeAPS/Sources/Modules/Settings/View/SettingsRootView.swift

@@ -20,6 +20,7 @@ extension Settings {
                 Section(header: Text("Services")) {
                     Text("Nightscout").navigationLink(to: .nighscoutConfig, from: self)
                     Text("CGM").navigationLink(to: .cgm, from: self)
+                    Text("Notifications").navigationLink(to: .notificationsConfig, from: self)
                 }
 
                 Section(header: Text("Configuration")) {

+ 3 - 0
FreeAPS/Sources/Router/Screen.swift

@@ -23,6 +23,7 @@ enum Screen: Identifiable, Hashable {
     case cgm
     case libreConfig
     case calibrations
+    case notificationsConfig
 
     var id: Int { String(reflecting: self).hashValue }
 }
@@ -72,6 +73,8 @@ extension Screen {
             LibreConfig.RootView(resolver: resolver)
         case .calibrations:
             Calibrations.RootView(resolver: resolver)
+        case .notificationsConfig:
+            NotificationsConfig.RootView(resolver: resolver)
         }
     }
 

+ 25 - 2
FreeAPS/Sources/Services/Calendar/CalendarManager.swift

@@ -14,9 +14,13 @@ final class BaseCalendarManager: CalendarManager, Injectable {
 
     @Persisted(key: "CalendarManager.currentCalendarID") var currentCalendarID: String? = nil
     @Injected() private var settingsManager: SettingsManager!
+    @Injected() private var broadcaster: Broadcaster!
+    @Injected() private var glucoseStorage: GlucoseStorage!
 
-    init(resilver: Resolver) {
-        injectServices(resilver)
+    init(resolver: Resolver) {
+        injectServices(resolver)
+        broadcaster.register(GlucoseObserver.self, observer: self)
+        setupGlucose()
     }
 
     func requestAccessIfNeeded() -> AnyPublisher<Bool, Never> {
@@ -117,6 +121,7 @@ final class BaseCalendarManager: CalendarManager, Injectable {
             formatter.minimumFractionDigits = 1
             formatter.maximumFractionDigits = 1
         }
+        formatter.roundingMode = .halfUp
         return formatter
     }
 
@@ -127,6 +132,24 @@ final class BaseCalendarManager: CalendarManager, Injectable {
         formatter.positivePrefix = "+"
         return formatter
     }
+
+    func setupGlucose() {
+        let glucose = glucoseStorage.recent()
+        let recentGlucose = glucose.last
+        let glucoseDelta: Int?
+        if glucose.count >= 2 {
+            glucoseDelta = (recentGlucose?.glucose ?? 0) - (glucose[glucose.count - 2].glucose ?? 0)
+        } else {
+            glucoseDelta = nil
+        }
+        createEvent(for: recentGlucose, delta: glucoseDelta)
+    }
+}
+
+extension BaseCalendarManager: GlucoseObserver {
+    func glucoseDidUpdate(_: [BloodGlucose]) {
+        setupGlucose()
+    }
 }
 
 extension BloodGlucose.Direction {

+ 1 - 1
FreeAPS/Sources/Services/SettingsManager/SettingsManager.swift

@@ -1,7 +1,7 @@
 import Foundation
 import Swinject
 
-protocol SettingsManager {
+protocol SettingsManager: AnyObject {
     var settings: FreeAPSSettings { get set }
     var preferences: Preferences { get }
 }

+ 204 - 0
FreeAPS/Sources/Services/UserNotifiactions/UserNotificationsManager.swift

@@ -0,0 +1,204 @@
+import AudioToolbox
+import Foundation
+import Swinject
+import UIKit
+import UserNotifications
+
+protocol UserNotificationsManager {}
+
+final class BaseUserNotificationsManager: NSObject, UserNotificationsManager, Injectable {
+    private enum Identifier: String {
+        case glucocoseNotification = "FreeAPS.glucoseNotification"
+    }
+
+    @Injected() private var settingsManager: SettingsManager!
+    @Injected() private var broadcaster: Broadcaster!
+    @Injected() private var glucoseStorage: GlucoseStorage!
+
+    private let center = UNUserNotificationCenter.current()
+
+    init(resolver: Resolver) {
+        super.init()
+        center.delegate = self
+        injectServices(resolver)
+        broadcaster.register(GlucoseObserver.self, observer: self)
+
+        requestNotificationPermissionsIfNeeded()
+        sendGlucoseNotification()
+    }
+
+    private func addAppBadge(glucose: Int?) {
+        guard let glucose = glucose, settingsManager.settings.glucoseBadge else {
+            DispatchQueue.main.async {
+                UIApplication.shared.applicationIconBadgeNumber = 0
+            }
+            return
+        }
+
+        let badge: Int
+        if settingsManager.settings.units == .mmolL {
+            badge = Int(round(Double((glucose * 10).asMmolL)))
+        } else {
+            badge = glucose
+        }
+
+        DispatchQueue.main.async {
+            UIApplication.shared.applicationIconBadgeNumber = badge
+        }
+    }
+
+    private func sendGlucoseNotification() {
+        addAppBadge(glucose: nil)
+
+        ensureCanSendNotification {
+            let glucose = self.glucoseStorage.recent()
+            guard let lastGlucose = glucose.last, let glucoseValue = lastGlucose.glucose else { return }
+
+            let delta: Int?
+            if glucose.count >= 2 {
+                delta = glucoseValue - (glucose[glucose.count - 2].glucose ?? 0)
+            } else {
+                delta = nil
+            }
+
+            let content = UNMutableNotificationContent()
+
+            var titles: [String] = []
+
+            switch self.glucoseStorage.alarm {
+            case .none:
+                titles.append(NSLocalizedString("Glucose", comment: "Glucose"))
+            case .low:
+                titles.append(NSLocalizedString("LOWALERT!", comment: "LOWALERT!"))
+                self.playSound()
+            case .high:
+                titles.append(NSLocalizedString("HIGHALERT!", comment: "HIGHALERT!"))
+                self.playSound()
+            }
+
+            let units = self.settingsManager.settings.units
+
+            let glucoseText = self.glucoseFormatter
+                .string(from: Double(
+                    units == .mmolL ? glucoseValue
+                        .asMmolL : Decimal(glucoseValue)
+                ) as NSNumber)! + " " + NSLocalizedString(units.rawValue, comment: "units")
+            let directionText = lastGlucose.direction?.symbol ?? "↔︎"
+            let deltaText = delta
+                .map {
+                    self.deltaFormatter
+                        .string(from: Double(
+                            units == .mmolL ? $0
+                                .asMmolL : Decimal($0)
+                        ) as NSNumber)!
+                } ?? "--"
+
+            let body = glucoseText + " " + directionText + " " + deltaText
+            titles.append(body)
+
+            content.title = titles.joined(separator: " ")
+            content.body = body
+
+            self.addAppBadge(glucose: lastGlucose.glucose)
+
+            self.addRequest(identifier: .glucocoseNotification, content: content, deleteOld: true)
+        }
+    }
+
+    private func requestNotificationPermissionsIfNeeded() {
+        center.getNotificationSettings { settings in
+            debug(.service, "UNUserNotificationCenter.authorizationStatus: \(String(describing: settings.authorizationStatus))")
+            if ![.authorized, .provisional].contains(settings.authorizationStatus) {
+                self.requestNotificationPermissions()
+            }
+        }
+    }
+
+    private func requestNotificationPermissions() {
+        debug(.service, "requestNotificationPermissions")
+        center.requestAuthorization(options: [.badge, .sound, .alert]) { granted, error in
+            if granted {
+                debug(.service, "requestNotificationPermissions was granted")
+            } else {
+                warning(.service, "requestNotificationPermissions failed", error: error)
+            }
+        }
+    }
+
+    private func ensureCanSendNotification(_ completion: @escaping () -> Void) {
+        center.getNotificationSettings { settings in
+            guard settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional else {
+                warning(.service, "ensureCanSendNotification failed, authorization denied")
+                return
+            }
+
+            debug(.service, "Sending notification was allowed")
+
+            completion()
+        }
+    }
+
+    private func addRequest(identifier: Identifier, content: UNMutableNotificationContent, deleteOld: Bool = false) {
+        let request = UNNotificationRequest(identifier: identifier.rawValue, content: content, trigger: nil)
+
+        if deleteOld {
+            center.removeDeliveredNotifications(withIdentifiers: [identifier.rawValue])
+            center.removePendingNotificationRequests(withIdentifiers: [identifier.rawValue])
+        }
+
+        center.add(request) { error in
+            if let error = error {
+                warning(.service, "Unable to addNotificationRequest", error: error)
+                return
+            }
+
+            debug(.service, "Sending \(identifier) notification")
+        }
+    }
+
+    private func playSound(times: Int = 3) {
+        guard times > 0 else {
+            return
+        }
+
+        AudioServicesPlaySystemSoundWithCompletion(1336) {
+            self.playSound(times: times - 1)
+        }
+    }
+
+    private var glucoseFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 0
+        if settingsManager.settings.units == .mmolL {
+            formatter.minimumFractionDigits = 1
+            formatter.maximumFractionDigits = 1
+        }
+        formatter.roundingMode = .halfUp
+        return formatter
+    }
+
+    private var deltaFormatter: NumberFormatter {
+        let formatter = NumberFormatter()
+        formatter.numberStyle = .decimal
+        formatter.maximumFractionDigits = 2
+        formatter.positivePrefix = "+"
+        return formatter
+    }
+}
+
+extension BaseUserNotificationsManager: GlucoseObserver {
+    func glucoseDidUpdate(_: [BloodGlucose]) {
+        sendGlucoseNotification()
+    }
+}
+
+extension BaseUserNotificationsManager: UNUserNotificationCenterDelegate {
+    func userNotificationCenter(
+        _: UNUserNotificationCenter,
+        willPresent _: UNNotification,
+        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
+    ) {
+        completionHandler([.banner, .badge, .sound])
+    }
+}