diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index b2a2f6d46c..d081cabeaf 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -34,10 +34,10 @@ 1D82E6A025377C6B009131FB /* TrustedTimeChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D82E69F25377C6B009131FB /* TrustedTimeChecker.swift */; }; 1D8D55BC252274650044DBB6 /* BolusEntryViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D8D55BB252274650044DBB6 /* BolusEntryViewModelTests.swift */; }; 1D9650C82523FBA100A1370B /* DeviceDataManager+BolusEntryViewModelDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D9650C72523FBA100A1370B /* DeviceDataManager+BolusEntryViewModelDelegate.swift */; }; - 1DA649A7244126CD00F61E75 /* UserNotificationAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA649A6244126CD00F61E75 /* UserNotificationAlertPresenter.swift */; }; - 1DA649A9244126DA00F61E75 /* InAppModalAlertPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA649A8244126DA00F61E75 /* InAppModalAlertPresenter.swift */; }; + 1DA649A7244126CD00F61E75 /* UserNotificationAlertIssuer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA649A6244126CD00F61E75 /* UserNotificationAlertIssuer.swift */; }; + 1DA649A9244126DA00F61E75 /* InAppModalAlertIssuer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA649A8244126DA00F61E75 /* InAppModalAlertIssuer.swift */; }; 1DA7A84224476EAD008257F0 /* AlertManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA7A84124476EAD008257F0 /* AlertManagerTests.swift */; }; - 1DA7A84424477698008257F0 /* InAppModalAlertPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA7A84324477698008257F0 /* InAppModalAlertPresenterTests.swift */; }; + 1DA7A84424477698008257F0 /* InAppModalAlertIssuerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA7A84324477698008257F0 /* InAppModalAlertIssuerTests.swift */; }; 1DB1065124467E18005542BD /* AlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB1065024467E18005542BD /* AlertManager.swift */; }; 1DB1CA4D24A55F0000B3B94C /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DB1CA4C24A55F0000B3B94C /* Image.swift */; }; 1DC63E7425351BDF004605DA /* TrueTime.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1DC63E7325351BDF004605DA /* TrueTime.framework */; }; @@ -46,7 +46,7 @@ 1DDE273E24AEA4B000796622 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DE09BA824A3E23F009EE9F9 /* SettingsView.swift */; }; 1DDE273F24AEA4F200796622 /* NotificationsCriticalAlertPermissionsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B42C951624A3CAF200857C73 /* NotificationsCriticalAlertPermissionsViewModel.swift */; }; 1DDE274024AEA4F200796622 /* NotificationsCriticalAlertPermissionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA46B5F2492E2E300D71A63 /* NotificationsCriticalAlertPermissionsView.swift */; }; - 1DFE9E172447B6270082C280 /* UserNotificationAlertPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE9E162447B6270082C280 /* UserNotificationAlertPresenterTests.swift */; }; + 1DFE9E172447B6270082C280 /* UserNotificationAlertIssuerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DFE9E162447B6270082C280 /* UserNotificationAlertIssuerTests.swift */; }; 43027F0F1DFE0EC900C51989 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F526D5E1DF2459000A04910 /* HKUnit.swift */; }; 4302F4E11D4E9C8900F0FCAF /* TextFieldTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */; }; 4302F4E31D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */; }; @@ -372,6 +372,8 @@ 89F9119424358E4500ECCAF3 /* CarbAbsorptionTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89F9119324358E4500ECCAF3 /* CarbAbsorptionTime.swift */; }; 89F9119624358E6900ECCAF3 /* BolusPickerValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89F9119524358E6900ECCAF3 /* BolusPickerValues.swift */; }; 89FE21AD24AC57E30033F501 /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89FE21AC24AC57E30033F501 /* Collection.swift */; }; + A90EF53C25DEF06200F32D61 /* PluginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16DA84122E8E112008624C2 /* PluginManager.swift */; }; + A90EF54425DEF0A000F32D61 /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4374B5EE209D84BE00D17AA8 /* OSLog.swift */; }; A91E4C2124F867A700BE9213 /* StoredAlertTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91E4C2024F867A700BE9213 /* StoredAlertTests.swift */; }; A91E4C2324F86F1000BE9213 /* CriticalEventLogExportManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91E4C2224F86F1000BE9213 /* CriticalEventLogExportManagerTests.swift */; }; A92E557E2464DFFD00DB93BB /* DosingDecisionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A92E557D2464DFFD00DB93BB /* DosingDecisionStore.swift */; }; @@ -386,6 +388,7 @@ A966152B23EA5A37005D8B29 /* DerivedAssets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A966152923EA5A37005D8B29 /* DerivedAssets.xcassets */; }; A967D94C24F99B9300CDDF8A /* OutputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = A967D94B24F99B9300CDDF8A /* OutputStream.swift */; }; A977A2F424ACFECF0059C207 /* CriticalEventLogExportManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A977A2F324ACFECF0059C207 /* CriticalEventLogExportManager.swift */; }; + A97F250825E056D500F0EE19 /* OnboardingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A97F250725E056D500F0EE19 /* OnboardingManager.swift */; }; A985464D251448300099C1A6 /* OutputStreamTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A985464C251448300099C1A6 /* OutputStreamTests.swift */; }; A98556852493F901000FD662 /* AlertStore+SimulatedCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A98556842493F901000FD662 /* AlertStore+SimulatedCoreData.swift */; }; A987CD4924A58A0100439ADC /* ZipArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = A987CD4824A58A0100439ADC /* ZipArchive.swift */; }; @@ -408,6 +411,7 @@ A9CBE45A248ACBE1008E7BA2 /* DosingDecisionStore+SimulatedCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CBE459248ACBE1008E7BA2 /* DosingDecisionStore+SimulatedCoreData.swift */; }; A9CBE45C248ACC03008E7BA2 /* SettingsStore+SimulatedCoreData.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9CBE45B248ACC03008E7BA2 /* SettingsStore+SimulatedCoreData.swift */; }; A9CE912224CA032E00302A40 /* NSUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 430B29892041F54A00BA9F93 /* NSUserDefaults.swift */; }; + A9D5C5B625DC6C6A00534873 /* LoopAppManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9D5C5B525DC6C6A00534873 /* LoopAppManager.swift */; }; A9DAE7D02332D77F006AE942 /* LoopTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DAE7CF2332D77F006AE942 /* LoopTests.swift */; }; A9DCF32A25B0FABF00C89088 /* LoopUIColorPalette+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DCF2D525B0F3C500C89088 /* LoopUIColorPalette+Default.swift */; }; A9DF02CB24F72B9E00B7C988 /* CriticalEventLogTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DF02CA24F72B9E00B7C988 /* CriticalEventLogTests.swift */; }; @@ -464,7 +468,6 @@ C1201E2D23ECDF3D002DA84A /* WatchContextRequestUserInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1201E2B23ECDBD0002DA84A /* WatchContextRequestUserInfo.swift */; }; C12F21A71DFA79CB00748193 /* recommend_temp_basal_very_low_end_in_range.json in Resources */ = {isa = PBXBuildFile; fileRef = C12F21A61DFA79CB00748193 /* recommend_temp_basal_very_low_end_in_range.json */; }; C13255D6223E7BE2008AF50C /* BolusProgressTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C1F8B1DB223862D500DD66CF /* BolusProgressTableViewCell.xib */; }; - C136AA2423109CC6008A320D /* PluginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C16DA84122E8E112008624C2 /* PluginManager.swift */; }; C13BAD941E8009B000050CB5 /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43BFF0B31E45C1BE00FF19A9 /* NumberFormatter.swift */; }; C13DA2B024F6C7690098BB29 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C13DA2AF24F6C7690098BB29 /* UIViewController.swift */; }; C148CEE724FD91BD00711B3B /* DeliveryUncertaintyAlertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C148CEE624FD91BD00711B3B /* DeliveryUncertaintyAlertManager.swift */; }; @@ -750,17 +753,17 @@ 1D8D55BB252274650044DBB6 /* BolusEntryViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusEntryViewModelTests.swift; sourceTree = ""; }; 1D9650C72523FBA100A1370B /* DeviceDataManager+BolusEntryViewModelDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeviceDataManager+BolusEntryViewModelDelegate.swift"; sourceTree = ""; }; 1DA46B5F2492E2E300D71A63 /* NotificationsCriticalAlertPermissionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsCriticalAlertPermissionsView.swift; sourceTree = ""; }; - 1DA649A6244126CD00F61E75 /* UserNotificationAlertPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserNotificationAlertPresenter.swift; sourceTree = ""; }; - 1DA649A8244126DA00F61E75 /* InAppModalAlertPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppModalAlertPresenter.swift; sourceTree = ""; }; + 1DA649A6244126CD00F61E75 /* UserNotificationAlertIssuer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserNotificationAlertIssuer.swift; sourceTree = ""; }; + 1DA649A8244126DA00F61E75 /* InAppModalAlertIssuer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InAppModalAlertIssuer.swift; sourceTree = ""; }; 1DA7A84124476EAD008257F0 /* AlertManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertManagerTests.swift; sourceTree = ""; }; - 1DA7A84324477698008257F0 /* InAppModalAlertPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppModalAlertPresenterTests.swift; sourceTree = ""; }; + 1DA7A84324477698008257F0 /* InAppModalAlertIssuerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppModalAlertIssuerTests.swift; sourceTree = ""; }; 1DB1065024467E18005542BD /* AlertManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertManager.swift; sourceTree = ""; }; 1DB1CA4C24A55F0000B3B94C /* Image.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Image.swift; sourceTree = ""; }; 1DB1CA4E24A56D7600B3B94C /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; 1DC63E7325351BDF004605DA /* TrueTime.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TrueTime.framework; path = Carthage/Build/iOS/TrueTime.framework; sourceTree = ""; }; 1DD0B76624EC77AC008A2DC3 /* SupportScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportScreenView.swift; sourceTree = ""; }; 1DE09BA824A3E23F009EE9F9 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; - 1DFE9E162447B6270082C280 /* UserNotificationAlertPresenterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationAlertPresenterTests.swift; sourceTree = ""; }; + 1DFE9E162447B6270082C280 /* UserNotificationAlertIssuerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationAlertIssuerTests.swift; sourceTree = ""; }; 4302F4E01D4E9C8900F0FCAF /* TextFieldTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFieldTableViewController.swift; sourceTree = ""; }; 4302F4E21D4EA54200F0FCAF /* InsulinDeliveryTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InsulinDeliveryTableViewController.swift; sourceTree = ""; }; 430B29892041F54A00BA9F93 /* NSUserDefaults.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSUserDefaults.swift; sourceTree = ""; }; @@ -1271,6 +1274,7 @@ A966152923EA5A37005D8B29 /* DerivedAssets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = DerivedAssets.xcassets; sourceTree = ""; }; A967D94B24F99B9300CDDF8A /* OutputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputStream.swift; sourceTree = ""; }; A977A2F324ACFECF0059C207 /* CriticalEventLogExportManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalEventLogExportManager.swift; sourceTree = ""; }; + A97F250725E056D500F0EE19 /* OnboardingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingManager.swift; sourceTree = ""; }; A985464C251448300099C1A6 /* OutputStreamTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutputStreamTests.swift; sourceTree = ""; }; A98556842493F901000FD662 /* AlertStore+SimulatedCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlertStore+SimulatedCoreData.swift"; sourceTree = ""; }; A987CD4824A58A0100439ADC /* ZipArchive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZipArchive.swift; sourceTree = ""; }; @@ -1291,6 +1295,7 @@ A9CBE457248AB564008E7BA2 /* DoseStore+SimulatedCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DoseStore+SimulatedCoreData.swift"; sourceTree = ""; }; A9CBE459248ACBE1008E7BA2 /* DosingDecisionStore+SimulatedCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DosingDecisionStore+SimulatedCoreData.swift"; sourceTree = ""; }; A9CBE45B248ACC03008E7BA2 /* SettingsStore+SimulatedCoreData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SettingsStore+SimulatedCoreData.swift"; sourceTree = ""; }; + A9D5C5B525DC6C6A00534873 /* LoopAppManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopAppManager.swift; sourceTree = ""; }; A9DAE7CF2332D77F006AE942 /* LoopTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoopTests.swift; sourceTree = ""; }; A9DCF2D525B0F3C500C89088 /* LoopUIColorPalette+Default.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LoopUIColorPalette+Default.swift"; sourceTree = ""; }; A9DF02CA24F72B9E00B7C988 /* CriticalEventLogTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CriticalEventLogTests.swift; sourceTree = ""; }; @@ -1558,12 +1563,12 @@ 1DB1065024467E18005542BD /* AlertManager.swift */, 1D05219C2469F1F5000EBBDE /* AlertStore.swift */, 1D080CBB2473214A00356610 /* AlertStore.xcdatamodeld */, - 1DA649A8244126DA00F61E75 /* InAppModalAlertPresenter.swift */, + 1DA649A8244126DA00F61E75 /* InAppModalAlertIssuer.swift */, B4CAD8502549D02D0057946B /* LoopSettingsAlerter.swift */, 1D05219A2469E9DF000EBBDE /* StoredAlert.swift */, 1D4A3E2B2478628500FD601B /* StoredAlert+CoreDataClass.swift */, 1D4A3E2C2478628500FD601B /* StoredAlert+CoreDataProperties.swift */, - 1DA649A6244126CD00F61E75 /* UserNotificationAlertPresenter.swift */, + 1DA649A6244126CD00F61E75 /* UserNotificationAlertIssuer.swift */, ); path = Alerts; sourceTree = ""; @@ -1585,8 +1590,8 @@ children = ( 1D80313C24746274002810DF /* AlertStoreTests.swift */, 1DA7A84124476EAD008257F0 /* AlertManagerTests.swift */, - 1DA7A84324477698008257F0 /* InAppModalAlertPresenterTests.swift */, - 1DFE9E162447B6270082C280 /* UserNotificationAlertPresenterTests.swift */, + 1DA7A84324477698008257F0 /* InAppModalAlertIssuerTests.swift */, + 1DFE9E162447B6270082C280 /* UserNotificationAlertIssuerTests.swift */, A91E4C2024F867A700BE9213 /* StoredAlertTests.swift */, ); path = Alerts; @@ -2006,30 +2011,32 @@ 43F5C2E41B93C5D4003EB13D /* Managers */ = { isa = PBXGroup; children = ( - 1DA6499D2441266400F61E75 /* Alerts */, - E95D37FF24EADE68005E2F50 /* Store Protocols */, 439897361CD2F80600223065 /* AnalyticsServicesManager.swift */, B4F3D25024AF890C0095CE44 /* BluetoothStateManager.swift */, 439BED291E76093C00B0AED5 /* CGMManager.swift */, + C16575702538A36B004AE16E /* CGMStalenessMonitor.swift */, A977A2F324ACFECF0059C207 /* CriticalEventLogExportManager.swift */, + C148CEE624FD91BD00711B3B /* DeliveryUncertaintyAlertManager.swift */, 43DBF0521C93EC8200B3C386 /* DeviceDataManager.swift */, 43F78D251C8FC000002152D1 /* DoseMath.swift */, 89CA2B3C226E6B13004D9350 /* LocalTestingScenariosManager.swift */, A9C62D862331703000535612 /* LoggingServicesManager.swift */, 1D2609AC248EEB9900A6F258 /* LoopAlertsManager.swift */, + A9D5C5B525DC6C6A00534873 /* LoopAppManager.swift */, 43A567681C94880B00334FAC /* LoopDataManager.swift */, 43C094491CACCC73001F6403 /* NotificationManager.swift */, + A97F250725E056D500F0EE19 /* OnboardingManager.swift */, 432E73CA1D24B3D6009AD15D /* RemoteDataServicesManager.swift */, A9C62D852331703000535612 /* Service.swift */, A9C62D872331703000535612 /* ServicesManager.swift */, + E9BB27AA23B85C3500FB4987 /* SleepStore.swift */, 43FCEEA8221A615B0013DD30 /* StatusChartsManager.swift */, 4F70C20F1DE8FAC5006380B7 /* StatusExtensionDataManager.swift */, 89ADE13A226BFA0F0067222B /* TestingScenariosManager.swift */, 1D82E69F25377C6B009131FB /* TrustedTimeChecker.swift */, 4328E0341CFC0AE100E199AA /* WatchDataManager.swift */, - E9BB27AA23B85C3500FB4987 /* SleepStore.swift */, - C148CEE624FD91BD00711B3B /* DeliveryUncertaintyAlertManager.swift */, - C16575702538A36B004AE16E /* CGMStalenessMonitor.swift */, + 1DA6499D2441266400F61E75 /* Alerts */, + E95D37FF24EADE68005E2F50 /* Store Protocols */, ); path = Managers; sourceTree = ""; @@ -3352,14 +3359,15 @@ C1F8B243223E73FD00DD66CF /* BolusProgressTableViewCell.swift in Sources */, 89D6953E23B6DF8A002B3066 /* PotentialCarbEntryTableViewCell.swift in Sources */, 89CA2B30226C0161004D9350 /* DirectoryObserver.swift in Sources */, - 1DA649A7244126CD00F61E75 /* UserNotificationAlertPresenter.swift in Sources */, + 1DA649A7244126CD00F61E75 /* UserNotificationAlertIssuer.swift in Sources */, 1DDE273F24AEA4F200796622 /* NotificationsCriticalAlertPermissionsViewModel.swift in Sources */, 439A7942211F631C0041B75F /* RootNavigationController.swift in Sources */, 4F11D3C020DCBEEC006E072C /* GlucoseBackfillRequestUserInfo.swift in Sources */, 89E267FC2292456700A3F2AF /* FeatureFlags.swift in Sources */, 43A567691C94880B00334FAC /* LoopDataManager.swift in Sources */, - 1DA649A9244126DA00F61E75 /* InAppModalAlertPresenter.swift in Sources */, + 1DA649A9244126DA00F61E75 /* InAppModalAlertIssuer.swift in Sources */, 43B260491ED248FB008CAA77 /* CarbEntryTableViewCell.swift in Sources */, + A9D5C5B625DC6C6A00534873 /* LoopAppManager.swift in Sources */, 4302F4E11D4E9C8900F0FCAF /* TextFieldTableViewController.swift in Sources */, 43F64DD91D9C92C900D24DC6 /* TitleSubtitleTableViewCell.swift in Sources */, 43FCEEA9221A615B0013DD30 /* StatusChartsManager.swift in Sources */, @@ -3457,6 +3465,7 @@ 43F78D261C8FC000002152D1 /* DoseMath.swift in Sources */, 43511CE221FD80E400566C63 /* RetrospectiveCorrection.swift in Sources */, 1D05219B2469E9DF000EBBDE /* StoredAlert.swift in Sources */, + A97F250825E056D500F0EE19 /* OnboardingManager.swift in Sources */, 438D42F91D7C88BC003244B0 /* PredictionInputEffect.swift in Sources */, A9C62D8223316FF600535612 /* UserDefaults+Services.swift in Sources */, 892A5D692230C41D008961AB /* RangeReplaceableCollection.swift in Sources */, @@ -3679,7 +3688,7 @@ C16575732538AFF6004AE16E /* CGMStalenessMonitorTests.swift in Sources */, A9E6DFEA246A0448005B1A1C /* PumpManagerErrorTests.swift in Sources */, A9DF02CD24F72BC800B7C988 /* PersistenceControllerTestCase.swift in Sources */, - 1DA7A84424477698008257F0 /* InAppModalAlertPresenterTests.swift in Sources */, + 1DA7A84424477698008257F0 /* InAppModalAlertIssuerTests.swift in Sources */, E93E86A824DDCC4400FF40C8 /* MockDoseStore.swift in Sources */, E98A55F124EDD85E0008715D /* MockDosingDecisionStore.swift in Sources */, 8968B114240C55F10074BB48 /* LoopSettingsTests.swift in Sources */, @@ -3696,7 +3705,7 @@ A9DAE7D02332D77F006AE942 /* LoopTests.swift in Sources */, B4FACBB12541FAB700199981 /* LoopSettingsAlerterTests.swift in Sources */, E93E86B024DDE1BD00FF40C8 /* MockGlucoseStore.swift in Sources */, - 1DFE9E172447B6270082C280 /* UserNotificationAlertPresenterTests.swift in Sources */, + 1DFE9E172447B6270082C280 /* UserNotificationAlertIssuerTests.swift in Sources */, B4BC56382518DEA900373647 /* CGMStatusHUDViewModelTests.swift in Sources */, A9E6DFE8246A043D005B1A1C /* DoseStoreTests.swift in Sources */, C1900900252271BB00721625 /* SimpleBolusCalculatorTests.swift in Sources */, @@ -3715,14 +3724,15 @@ 4F2C15831E0757E600E160D4 /* HKUnit.swift in Sources */, C1FB4290217922A100FAB378 /* PumpManagerUI.swift in Sources */, 1D4990E824A25931005CC357 /* FeatureFlags.swift in Sources */, + A90EF53C25DEF06200F32D61 /* PluginManager.swift in Sources */, C1FB428D21791D2500FAB378 /* PumpManager.swift in Sources */, 43E93FB51E4675E800EAB8DB /* NumberFormatter.swift in Sources */, 4345E3FB21F04911009E00E5 /* UIColor+HIG.swift in Sources */, 43BFF0CD1E466C8400FF19A9 /* StateColorPalette.swift in Sources */, 4FC8C8021DEB943800A1452E /* NSUserDefaults+StatusExtension.swift in Sources */, - C136AA2423109CC6008A320D /* PluginManager.swift in Sources */, 4F70C2121DE900EA006380B7 /* StatusExtensionContext.swift in Sources */, 4F70C1E11DE8DCA7006380B7 /* StatusViewController.swift in Sources */, + A90EF54425DEF0A000F32D61 /* OSLog.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Loop/AppDelegate.swift b/Loop/AppDelegate.swift index 55ebe073dd..15ee9037ed 100644 --- a/Loop/AppDelegate.swift +++ b/Loop/AppDelegate.swift @@ -6,111 +6,32 @@ // Copyright © 2015 Nathan Racklyeft. All rights reserved. // -import HealthKit -import Intents -import LoopCore -import LoopKit -import LoopKitUI import UIKit -import UserNotifications +import LoopKit @UIApplicationMain -final class AppDelegate: UIResponder, UIApplicationDelegate, DeviceOrientationController { - - private lazy var log = DiagnosticLog(category: "AppDelegate") - - private lazy var pluginManager = PluginManager() - - private var alertManager: AlertManager! - private var deviceDataManager: DeviceDataManager! - private var loopAlertsManager: LoopAlertsManager! - private var bluetoothStateManager: BluetoothStateManager! - private var trustedTimeChecker: TrustedTimeChecker! - +final class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - - var launchOptions: [UIApplication.LaunchOptionsKey: Any]? - - private var rootViewController: RootNavigationController! { - return window?.rootViewController as? RootNavigationController - } - - private func isAfterFirstUnlock() -> Bool { - let fileManager = FileManager.default - do { - let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) - let fileURL = documentDirectory.appendingPathComponent("protection.test") - guard fileManager.fileExists(atPath: fileURL.path) else { - let contents = Data("unimportant".utf8) - try? contents.write(to: fileURL, options: .completeFileProtectionUntilFirstUserAuthentication) - // If file doesn't exist, we're at first start, which will be user directed. - return true - } - let contents = try? Data(contentsOf: fileURL) - return contents != nil - } catch { - log.error("Could not create after first unlock test file: %@", String(describing: error)) - } - return false - } - - private func finishLaunch(application: UIApplication) { - log.default("Finishing launching") - UIDevice.current.isBatteryMonitoringEnabled = true - - bluetoothStateManager = BluetoothStateManager() - alertManager = AlertManager(rootViewController: rootViewController, expireAfter: Bundle.main.localCacheDuration) - deviceDataManager = DeviceDataManager(pluginManager: pluginManager, alertManager: alertManager, bluetoothStateManager: bluetoothStateManager, rootViewController: rootViewController) - - let statusTableViewController = UIStoryboard(name: "Main", bundle: Bundle(for: AppDelegate.self)).instantiateViewController(withIdentifier: "MainStatusViewController") as! StatusTableViewController - - statusTableViewController.deviceManager = deviceDataManager - - bluetoothStateManager.addBluetoothStateObserver(statusTableViewController) - - loopAlertsManager = LoopAlertsManager(alertManager: alertManager, bluetoothStateManager: bluetoothStateManager) - - SharedLogging.instance = deviceDataManager.loggingServicesManager - - deviceDataManager?.analyticsServicesManager.application(application, didFinishLaunchingWithOptions: launchOptions) - OrientationLock.deviceOrientationController = self + private let loopAppManager = LoopAppManager() + private let log = DiagnosticLog(category: "AppDelegate") - NotificationManager.authorize(delegate: self) - - rootViewController.pushViewController(statusTableViewController, animated: false) - - let notificationOption = launchOptions?[.remoteNotification] - - if let notification = notificationOption as? [String: AnyObject] { - deviceDataManager?.handleRemoteNotification(notification) - } - - scheduleBackgroundTasks() - - launchOptions = nil - - trustedTimeChecker = TrustedTimeChecker(alertManager) - } + // MARK: - UIApplicationDelegate - Initialization func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - self.launchOptions = launchOptions - - log.default("didFinishLaunchingWithOptions %{public}@", String(describing: launchOptions)) - - registerBackgroundTasks() + log.default("%{public}@ with launchOptions: %{public}@", #function, String(describing: launchOptions)) - guard isAfterFirstUnlock() else { - log.default("Launching before first unlock; pausing launch...") - return false - } + loopAppManager.initialize(with: launchOptions) + loopAppManager.launch(into: window) + return loopAppManager.isLaunchComplete + } - finishLaunch(application: application) + // MARK: - UIApplicationDelegate - Life Cycle - window?.tintColor = .loopAccent + func applicationDidBecomeActive(_ application: UIApplication) { + log.default(#function) - return true + loopAppManager.didBecomeActive() } func applicationWillResignActive(_ application: UIApplication) { @@ -125,139 +46,47 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, DeviceOrientationCo log.default(#function) } - func applicationDidBecomeActive(_ application: UIApplication) { - deviceDataManager?.updatePumpManagerBLEHeartbeatPreference() - } - func applicationWillTerminate(_ application: UIApplication) { log.default(#function) } - // MARK: - Continuity - - func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { - log.default(#function) - - if #available(iOS 12.0, *) { - if userActivity.activityType == NewCarbEntryIntent.className { - log.default("Restoring %{public}@ intent", userActivity.activityType) - rootViewController.restoreUserActivityState(.forNewCarbEntry()) - return true - } - } - - switch userActivity.activityType { - case NSUserActivity.newCarbEntryActivityType, - NSUserActivity.viewLoopStatusActivityType: - log.default("Restoring %{public}@ activity", userActivity.activityType) - restorationHandler([rootViewController]) - return true - default: - return false - } - } - - // MARK: - Remote notifications - func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { - let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) } - let token = tokenParts.joined() - log.default("RemoteNotifications device token: %{public}@", token) - deviceDataManager?.loopManager.settings.deviceToken = deviceToken - } + // MARK: - UIApplicationDelegate - Environment - func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - log.error("Failed to register: %{public}@", String(describing: error)) - } - - func application(_ application: UIApplication, - didReceiveRemoteNotification userInfo: [AnyHashable : Any], - fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - guard let notification = userInfo as? [String: AnyObject] else { - completionHandler(.failed) - return - } - - deviceDataManager?.handleRemoteNotification(notification) - completionHandler(.noData) - } - func applicationProtectedDataDidBecomeAvailable(_ application: UIApplication) { - log.default("applicationProtectedDataDidBecomeAvailable") - - if deviceDataManager == nil { - finishLaunch(application: application) + if !loopAppManager.isLaunchComplete { + loopAppManager.launch(into: window) } } - - // MARK: - DeviceOrientationController - var supportedInterfaceOrientations = UIInterfaceOrientationMask.allButUpsideDown + // MARK: - UIApplicationDelegate - Remote Notification - func setOriginallySupportedInferfaceOrientations() { - supportedInterfaceOrientations = UIInterfaceOrientationMask.allButUpsideDown - } + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + log.default(#function) - func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { - supportedInterfaceOrientations + loopAppManager.setRemoteNotificationsDeviceToken(deviceToken) } - // MARK: - Background Tasks - - private func registerBackgroundTasks() { - if DeviceDataManager.registerCriticalEventLogHistoricalExportBackgroundTask({ self.deviceDataManager.handleCriticalEventLogHistoricalExportBackgroundTask($0) }) { - log.debug("Critical event log export background task registered") - } else { - log.error("Critical event log export background task not registered") - } + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + log.error("%{public}@ with error: %{public}@", #function, String(describing: error)) } - private func scheduleBackgroundTasks() { - deviceDataManager.scheduleCriticalEventLogHistoricalExportBackgroundTask() - } -} + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + log.default(#function) -// MARK: UNUserNotificationCenterDelegate implementation + completionHandler(loopAppManager.handleRemoteNotification(userInfo as? [String: AnyObject]) ? .noData : .failed) + } -extension AppDelegate: UNUserNotificationCenterDelegate { - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - switch response.actionIdentifier { - case NotificationManager.Action.retryBolus.rawValue: - if let units = response.notification.request.content.userInfo[LoopNotificationUserInfoKey.bolusAmount.rawValue] as? Double, - let startDate = response.notification.request.content.userInfo[LoopNotificationUserInfoKey.bolusStartDate.rawValue] as? Date, - startDate.timeIntervalSinceNow >= TimeInterval(minutes: -5) - { - deviceDataManager?.analyticsServicesManager.didRetryBolus() + // MARK: - UIApplicationDelegate - Continuity - deviceDataManager?.enactBolus(units: units, at: startDate) { (_) in - completionHandler() - } - return - } - case NotificationManager.Action.acknowledgeAlert.rawValue: - let userInfo = response.notification.request.content.userInfo - if let alertIdentifier = userInfo[LoopNotificationUserInfoKey.alertTypeID.rawValue] as? Alert.AlertIdentifier, - let managerIdentifier = userInfo[LoopNotificationUserInfoKey.managerIDForAlert.rawValue] as? String { - alertManager.acknowledgeAlert(identifier: - Alert.Identifier(managerIdentifier: managerIdentifier, alertIdentifier: alertIdentifier)) - } - default: - break - } + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + log.default(#function) - completionHandler() + return loopAppManager.userActivity(userActivity, restorationHandler: restorationHandler) } - func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - switch notification.request.identifier { - // TODO: Until these notifications are converted to use the new alert system, they shall still show in the foreground - case LoopNotificationCategory.bolusFailure.rawValue, - LoopNotificationCategory.pumpBatteryLow.rawValue, - LoopNotificationCategory.pumpExpired.rawValue, - LoopNotificationCategory.pumpFault.rawValue: - completionHandler([.badge, .sound, .alert]) - default: - // All other userNotifications are not to be displayed while in the foreground - completionHandler([]) - } + // MARK: - UIApplicationDelegate - Interface + + func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { + return loopAppManager.supportedInterfaceOrientations } } diff --git a/Loop/Base.lproj/LaunchScreen.storyboard b/Loop/Base.lproj/LaunchScreen.storyboard index 57331f0bef..03664e6dbd 100644 --- a/Loop/Base.lproj/LaunchScreen.storyboard +++ b/Loop/Base.lproj/LaunchScreen.storyboard @@ -10,13 +10,14 @@ - + + + - diff --git a/Loop/Base.lproj/Main.storyboard b/Loop/Base.lproj/Main.storyboard index 8cfcddc5a9..cb76a7af25 100644 --- a/Loop/Base.lproj/Main.storyboard +++ b/Loop/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -673,15 +673,18 @@ - + + + - - + + + @@ -904,6 +907,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/Loop/Extensions/DeviceDataManager+DeviceStatus.swift b/Loop/Extensions/DeviceDataManager+DeviceStatus.swift index ec35887963..463c34a5a3 100644 --- a/Loop/Extensions/DeviceDataManager+DeviceStatus.swift +++ b/Loop/Extensions/DeviceDataManager+DeviceStatus.swift @@ -12,12 +12,11 @@ import LoopCore extension DeviceDataManager { var cgmStatusHighlight: DeviceStatusHighlight? { - if bluetoothState == .poweredOff { - return BluetoothStateManager.bluetoothOffHighlight - } else if bluetoothState == .denied || - bluetoothState == .unauthorized - { - return BluetoothStateManager.bluetoothUnavailableHighlight + let bluetoothState = bluetoothProvider.bluetoothState + if bluetoothState == .unsupported || bluetoothState == .unauthorized { + return BluetoothState.unavailableHighlight + } else if bluetoothState == .poweredOff { + return BluetoothState.offHighlight } else if cgmManager == nil { return DeviceDataManager.addCGMStatusHighlight } else { @@ -34,11 +33,9 @@ extension DeviceDataManager { } var pumpStatusHighlight: DeviceStatusHighlight? { - if bluetoothState == .denied || - bluetoothState == .unauthorized || - bluetoothState == .poweredOff - { - return BluetoothStateManager.bluetoothEnableHighlight + let bluetoothState = bluetoothProvider.bluetoothState + if bluetoothState == .unsupported || bluetoothState == .unauthorized || bluetoothState == .poweredOff { + return BluetoothState.enableHighlight } else if pumpManager == nil { return DeviceDataManager.addPumpStatusHighlight } else { @@ -67,21 +64,21 @@ extension DeviceDataManager { } func didTapOnCGMStatus(_ view: BaseHUDView? = nil) -> HUDTapAction? { - if let action = bluetoothState.action { + if let action = bluetoothProvider.bluetoothState.action { return action } else if let url = cgmManager?.appURL, UIApplication.shared.canOpenURL(url) { return .openAppURL(url) } else if let cgmManagerUI = (cgmManager as? CGMManagerUI) { - return .presentViewController(cgmManagerUI.settingsViewController(for: displayGlucoseUnitObservable, colorPalette: .default)) + return .presentViewController(cgmManagerUI.settingsViewController(for: displayGlucoseUnitObservable, bluetoothProvider: bluetoothProvider, colorPalette: .default)) } else { return .setupNewCGM } } func didTapOnPumpStatus(_ view: BaseHUDView? = nil) -> HUDTapAction? { - if let action = bluetoothState.action { + if let action = bluetoothProvider.bluetoothState.action { return action } else if let pumpManagerHUDProvider = pumpManagerHUDProvider, let view = view, @@ -89,7 +86,7 @@ extension DeviceDataManager { { return action } else if let pumpManager = pumpManager { - return .presentViewController(pumpManager.settingsViewController(colorPalette: .default)) + return .presentViewController(pumpManager.settingsViewController(bluetoothProvider: bluetoothProvider, colorPalette: .default)) } else { return .setupNewPump } @@ -102,3 +99,39 @@ extension DeviceDataManager { } } +// MARK: - BluetoothState + +fileprivate extension BluetoothState { + struct Highlight: DeviceStatusHighlight { + var localizedMessage: String + var imageName: String = "bluetooth.disabled" + var state: DeviceStatusHighlightState = .critical + + init(localizedMessage: String) { + self.localizedMessage = localizedMessage + } + } + + static var offHighlight: Highlight { + return Highlight(localizedMessage: NSLocalizedString("Bluetooth\nOff", comment: "Message to the user to that the bluetooth is off")) + } + + static var enableHighlight: Highlight { + return Highlight(localizedMessage: NSLocalizedString("Enable\nBluetooth", comment: "Message to the user to enable bluetooth")) + } + + static var unavailableHighlight: Highlight { + return Highlight(localizedMessage: NSLocalizedString("Bluetooth\nUnavailable", comment: "Message to the user that bluetooth is unavailable to the app")) + } + + var action: HUDTapAction? { + switch self { + case .unauthorized: + return .openAppURL(URL(string: UIApplication.openSettingsURLString)!) + case .poweredOff: + return .takeNoAction + default: + return nil + } + } +} diff --git a/Loop/Managers/Alerts/AlertManager.swift b/Loop/Managers/Alerts/AlertManager.swift index 04a8f10205..3efd7fa590 100644 --- a/Loop/Managers/Alerts/AlertManager.swift +++ b/Loop/Managers/Alerts/AlertManager.swift @@ -36,7 +36,7 @@ public final class AlertManager { private let log = DiagnosticLog(category: "AlertManager") - private var handlers: [AlertPresenter] = [] + private var handlers: [AlertIssuer] = [] private var responders: [String: Weak] = [:] private var soundVendors: [String: Weak] = [:] @@ -45,8 +45,8 @@ public final class AlertManager { let alertStore: AlertStore - public init(rootViewController: UIViewController, - handlers: [AlertPresenter]? = nil, + public init(alertPresenter: AlertPresenter, + handlers: [AlertIssuer]? = nil, userNotificationCenter: UserNotificationCenter = UNUserNotificationCenter.current(), fileManager: FileManager = FileManager.default, alertStore: AlertStore? = nil, @@ -65,8 +65,8 @@ public final class AlertManager { } self.alertStore = alertStore ?? AlertStore(storageDirectoryURL: alertStoreDirectory, expireAfter: expireAfter) self.handlers = handlers ?? - [UserNotificationAlertPresenter(userNotificationCenter: userNotificationCenter), - InAppModalAlertPresenter(rootViewController: rootViewController, alertManagerResponder: self)] + [UserNotificationAlertIssuer(userNotificationCenter: userNotificationCenter), + InAppModalAlertIssuer(alertPresenter: alertPresenter, alertManagerResponder: self)] playbackAlertsFromPersistence() } @@ -101,9 +101,9 @@ extension AlertManager: AlertManagerResponder { } } -// MARK: AlertPresenter implementation +// MARK: AlertIssuer implementation -extension AlertManager: AlertPresenter { +extension AlertManager: AlertIssuer { public func issueAlert(_ alert: Alert) { handlers.forEach { $0.issueAlert(alert) } diff --git a/Loop/Managers/Alerts/InAppModalAlertPresenter.swift b/Loop/Managers/Alerts/InAppModalAlertIssuer.swift similarity index 94% rename from Loop/Managers/Alerts/InAppModalAlertPresenter.swift rename to Loop/Managers/Alerts/InAppModalAlertIssuer.swift index e7cc694c46..ebc7d8aa84 100644 --- a/Loop/Managers/Alerts/InAppModalAlertPresenter.swift +++ b/Loop/Managers/Alerts/InAppModalAlertIssuer.swift @@ -1,5 +1,5 @@ // -// InAppModalAlertPresenter.swift +// InAppModalAlertIssuer.swift // LoopKit // // Created by Rick Pasetto on 4/9/20. @@ -9,9 +9,9 @@ import Foundation import LoopKit -public class InAppModalAlertPresenter: AlertPresenter { +public class InAppModalAlertIssuer: AlertIssuer { - private weak var rootViewController: UIViewController? + private weak var alertPresenter: AlertPresenter? private weak var alertManagerResponder: AlertManagerResponder? private var alertsShowing: [Alert.Identifier: (UIAlertController, Alert)] = [:] @@ -25,12 +25,12 @@ public class InAppModalAlertPresenter: AlertPresenter { private let soundPlayer: AlertSoundPlayer - init(rootViewController: UIViewController, + init(alertPresenter: AlertPresenter?, alertManagerResponder: AlertManagerResponder, soundPlayer: AlertSoundPlayer = DeviceAVSoundPlayer(), newActionFunc: @escaping ActionFactoryFunction = UIAlertAction.init, newTimerFunc: TimerFactoryFunction? = nil) { - self.rootViewController = rootViewController + self.alertPresenter = alertPresenter self.alertManagerResponder = alertManagerResponder self.soundPlayer = soundPlayer self.newActionFunc = newActionFunc @@ -65,7 +65,7 @@ public class InAppModalAlertPresenter: AlertPresenter { } /// Private functions -extension InAppModalAlertPresenter { +extension InAppModalAlertIssuer { private func schedule(alert: Alert, interval: TimeInterval, repeats: Bool) { guard alert.foregroundContent != nil else { @@ -140,7 +140,7 @@ extension InAppModalAlertPresenter { // For now, this is a simple alert with an "OK" button let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) alertController.addAction(newActionFunc(action, .default, { _ in completion() })) - rootViewController?.topmostViewController.present(alertController, animated: true) + alertPresenter?.present(alertController, animated: true) return alertController } diff --git a/Loop/Managers/Alerts/LoopSettingsAlerter.swift b/Loop/Managers/Alerts/LoopSettingsAlerter.swift index 122903e03e..c83d7cbef8 100644 --- a/Loop/Managers/Alerts/LoopSettingsAlerter.swift +++ b/Loop/Managers/Alerts/LoopSettingsAlerter.swift @@ -17,14 +17,14 @@ class LoopSettingsAlerter { weak var delegate: LoopSettingsAlerterDelegate? - private let alertPresenter: AlertPresenter? + private let alertIssuer: AlertIssuer? let workoutOverrideReminderInterval: TimeInterval - init(alertPresenter: AlertPresenter? = nil, + init(alertIssuer: AlertIssuer? = nil, workoutOverrideReminderInterval: TimeInterval = .days(1)) { - self.alertPresenter = alertPresenter + self.alertIssuer = alertIssuer self.workoutOverrideReminderInterval = workoutOverrideReminderInterval NotificationCenter.default.addObserver(forName: .LoopRunning, object: nil, queue: nil) { @@ -47,7 +47,7 @@ class LoopSettingsAlerter { } private func issueWorkoutOverrideReminder() { - alertPresenter?.issueAlert(workoutOverrideReminderAlert) + alertIssuer?.issueAlert(workoutOverrideReminderAlert) } } diff --git a/Loop/Managers/Alerts/UserNotificationAlertPresenter.swift b/Loop/Managers/Alerts/UserNotificationAlertIssuer.swift similarity index 95% rename from Loop/Managers/Alerts/UserNotificationAlertPresenter.swift rename to Loop/Managers/Alerts/UserNotificationAlertIssuer.swift index 13fd6a5c3e..4c3d97065d 100644 --- a/Loop/Managers/Alerts/UserNotificationAlertPresenter.swift +++ b/Loop/Managers/Alerts/UserNotificationAlertIssuer.swift @@ -1,5 +1,5 @@ // -// UserNotificationAlertPresenter.swift +// UserNotificationAlertIssuer.swift // LoopKit // // Created by Rick Pasetto on 4/9/20. @@ -8,10 +8,10 @@ import LoopKit -class UserNotificationAlertPresenter: AlertPresenter { +class UserNotificationAlertIssuer: AlertIssuer { let userNotificationCenter: UserNotificationCenter - let log = DiagnosticLog(category: "UserNotificationAlertPresenter") + let log = DiagnosticLog(category: "UserNotificationAlertIssuer") init(userNotificationCenter: UserNotificationCenter) { self.userNotificationCenter = userNotificationCenter @@ -45,7 +45,7 @@ class UserNotificationAlertPresenter: AlertPresenter { } } -extension UserNotificationAlertPresenter: AlertManagerResponder { +extension UserNotificationAlertIssuer: AlertManagerResponder { func acknowledgeAlert(identifier: Alert.Identifier) { DispatchQueue.main.async { self.log.debug("Removing notification %@ from delivered notifications", identifier.value) diff --git a/Loop/Managers/AnalyticsServicesManager.swift b/Loop/Managers/AnalyticsServicesManager.swift index 1a0f271c34..4794f7a6fe 100644 --- a/Loop/Managers/AnalyticsServicesManager.swift +++ b/Loop/Managers/AnalyticsServicesManager.swift @@ -37,7 +37,7 @@ final class AnalyticsServicesManager { // MARK: - UIApplicationDelegate - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [AnyHashable: Any]?) { + func application(didFinishLaunchingWithOptions launchOptions: [AnyHashable: Any]?) { logEvent("App Launch") } diff --git a/Loop/Managers/BluetoothStateManager.swift b/Loop/Managers/BluetoothStateManager.swift index 452acfe40d..4b4282b1bd 100644 --- a/Loop/Managers/BluetoothStateManager.swift +++ b/Loop/Managers/BluetoothStateManager.swift @@ -10,101 +10,105 @@ import CoreBluetooth import LoopKit import LoopKitUI -public protocol BluetoothStateManagerObserver: class { - func bluetoothStateManager(_ bluetoothStateManager: BluetoothStateManager, bluetoothStateDidUpdate bluetoothState: BluetoothStateManager.BluetoothState) -} +public class BluetoothStateManager: NSObject, BluetoothProvider { + private var completion: ((BluetoothAuthorization) -> Void)? + private var centralManager: CBCentralManager? + private var bluetoothObservers = WeakSynchronizedSet() -public class BluetoothStateManager: NSObject { + override init() { + super.init() - public enum BluetoothState { - case poweredOn - case poweredOff - case unauthorized - case denied - case other - - var action: HUDTapAction? { - switch self { - case .unauthorized, .denied: - return .openAppURL(URL(string: UIApplication.openSettingsURLString)!) - case .poweredOff: - return .takeNoAction - default: - return nil - } + if bluetoothAuthorization != .notDetermined { + self.centralManager = CBCentralManager(delegate: self, queue: nil) } } - - private var bluetoothCentralManager: CBCentralManager! - - private var bluetoothState: BluetoothState = .other - - private var bluetoothStateObservers = WeakSynchronizedSet() - - override init() { - super.init() - bluetoothCentralManager = CBCentralManager(delegate: self, queue: nil) + + public var bluetoothAuthorization: BluetoothAuthorization { + if #available(iOS 13.1, *) { // TODO: Remove once iOS 14 required + return BluetoothAuthorization(CBCentralManager.authorization) + } + return .notDetermined } - - public func addBluetoothStateObserver(_ observer: BluetoothStateManagerObserver, - queue: DispatchQueue = .main) - { - bluetoothStateObservers.insert(observer, queue: queue) + + public var bluetoothState: BluetoothState { + guard let centralManager = centralManager else { + return .unknown + } + return BluetoothState(centralManager.state) } - - public func removeBluetoothStateObserver(_ observer: BluetoothStateManagerObserver) { - bluetoothStateObservers.removeElement(observer) + + public func authorizeBluetooth(_ completion: @escaping (BluetoothAuthorization) -> Void) { + guard centralManager == nil else { + completion(bluetoothAuthorization) + return + } + self.completion = completion + self.centralManager = CBCentralManager(delegate: self, queue: nil) + } + + public func addBluetoothObserver(_ observer: BluetoothObserver, queue: DispatchQueue = .main) { + bluetoothObservers.insert(observer, queue: queue) + } + + public func removeBluetoothObserver(_ observer: BluetoothObserver) { + bluetoothObservers.removeElement(observer) } } -// MARK: CBCentralManagerDelegate implementation +// MARK: - CBCentralManagerDelegate extension BluetoothStateManager: CBCentralManagerDelegate { - public func centralManagerDidUpdateState(_ central: CBCentralManager) { - switch central.state { - case .unauthorized: - bluetoothState = .unauthorized - switch central.authorization { - case .denied: - bluetoothState = .denied - default: - break - } - case .poweredOn: - bluetoothState = .poweredOn - case .poweredOff: - bluetoothState = .poweredOff - default: - bluetoothState = .other - break + if let completion = completion { + completion(bluetoothAuthorization) + self.completion = nil } - bluetoothStateObservers.forEach { $0.bluetoothStateManager(self, bluetoothStateDidUpdate: self.bluetoothState) } + bluetoothObservers.forEach { $0.bluetoothDidUpdateState(BluetoothState(central.state)) } } } -// MARK: - Bluetooth Status Highlight +// MARK: - BluetoothAuthorization -extension BluetoothStateManager { - struct BluetoothStateHighlight: DeviceStatusHighlight { - var localizedMessage: String - var imageName: String = "bluetooth.disabled" - var state: DeviceStatusHighlightState = .critical - - init(localizedMessage: String) { - self.localizedMessage = localizedMessage +extension BluetoothAuthorization { + fileprivate init(_ authorization: CBManagerAuthorization) { + switch authorization { + case .notDetermined: + self = .notDetermined + case .restricted: + self = .restricted + case .denied: + self = .denied + case .allowedAlways: + self = .authorized + @unknown default: + self = .notDetermined } } - - public static var bluetoothOffHighlight: DeviceStatusHighlight { - return BluetoothStateHighlight(localizedMessage: NSLocalizedString("Bluetooth\nOff", comment: "Message to the user to that the bluetooth is off")) - } - - public static var bluetoothEnableHighlight: DeviceStatusHighlight { - return BluetoothStateHighlight(localizedMessage: NSLocalizedString("Enable\nBluetooth", comment: "Message to the user to enable bluetooth")) - } - - public static var bluetoothUnavailableHighlight: DeviceStatusHighlight { - return BluetoothStateHighlight(localizedMessage: NSLocalizedString("Bluetooth\nUnavailable", comment: "Message to the user that bluetooth is unavailable to the app")) +} + +// MARK: - BluetoothState + +extension BluetoothState { + fileprivate init(_ state: CBManagerState) { + switch state { + case .unknown: + self = .unknown + case .resetting: + self = .resetting + case .unsupported: + #if IOS_SIMULATOR + self = .poweredOn // Simulator reports unsupported, but pretend it is powered on + #else + self = .unsupported + #endif + case .unauthorized: + self = .unauthorized + case .poweredOff: + self = .poweredOff + case .poweredOn: + self = .poweredOn + @unknown default: + self = .unknown + } } } diff --git a/Loop/Managers/DeliveryUncertaintyAlertManager.swift b/Loop/Managers/DeliveryUncertaintyAlertManager.swift index c396ee7316..87f77fcbd1 100644 --- a/Loop/Managers/DeliveryUncertaintyAlertManager.swift +++ b/Loop/Managers/DeliveryUncertaintyAlertManager.swift @@ -12,18 +12,18 @@ import LoopKitUI class DeliveryUncertaintyAlertManager { private let pumpManager: PumpManagerUI - private let rootViewController: UIViewController + private let alertPresenter: AlertPresenter private var uncertainDeliveryAlert: UIAlertController? - init(pumpManager: PumpManagerUI, rootViewController: UIViewController) { + init(pumpManager: PumpManagerUI, alertPresenter: AlertPresenter) { self.pumpManager = pumpManager - self.rootViewController = rootViewController + self.alertPresenter = alertPresenter } private func showUncertainDeliveryRecoveryView() { var controller = pumpManager.deliveryUncertaintyRecoveryViewController(colorPalette: .default) controller.completionDelegate = self - self.rootViewController.present(controller, animated: true) + self.alertPresenter.present(controller, animated: true) } func showAlert(animated: Bool = true) { @@ -39,8 +39,8 @@ class DeliveryUncertaintyAlertManager { self.showUncertainDeliveryRecoveryView() } alert.addAction(action) - self.rootViewController.dismiss(animated: false) { - self.rootViewController.present(alert, animated: animated) + self.alertPresenter.dismiss(animated: false) { + self.alertPresenter.present(alert, animated: animated) } self.uncertainDeliveryAlert = alert } diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 20a53f9c0c..d93aafeada 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -23,6 +23,7 @@ final class DeviceDataManager { let pluginManager: PluginManager weak var alertManager: AlertManager! + let bluetoothProvider: BluetoothProvider /// Remember the launch date of the app for diagnostic reporting private let launchDate = Date() @@ -38,11 +39,9 @@ final class DeviceDataManager { private var deviceLog: PersistentDeviceLog - var bluetoothState: BluetoothStateManager.BluetoothState = .other - // MARK: - App-level responsibilities - private var rootViewController: UIViewController + private var alertPresenter: AlertPresenter private var deliveryUncertaintyAlertManager: DeliveryUncertaintyAlertManager? @@ -174,7 +173,7 @@ final class DeviceDataManager { private(set) var loopManager: LoopDataManager! - init(pluginManager: PluginManager, alertManager: AlertManager, bluetoothStateManager: BluetoothStateManager, rootViewController: UIViewController) { + init(pluginManager: PluginManager, alertManager: AlertManager, bluetoothProvider: BluetoothProvider, alertPresenter: AlertPresenter) { let localCacheDuration = Bundle.main.localCacheDuration let fileManager = FileManager.default @@ -194,7 +193,8 @@ final class DeviceDataManager { self.pluginManager = pluginManager self.alertManager = alertManager - self.rootViewController = rootViewController + self.bluetoothProvider = bluetoothProvider + self.alertPresenter = alertPresenter self.healthStore = HKHealthStore() self.cacheStore = PersistenceController.controllerInAppGroupDirectory() @@ -251,8 +251,6 @@ final class DeviceDataManager { // HealthStorePreferredGlucoseUnitDidChange will be notified once the user completes the health access form. Set to .milligramsPerDeciliter until then displayGlucoseUnitObservable = DisplayGlucoseUnitObservable(displayGlucoseUnit: glucoseStore.preferredUnit ?? .milligramsPerDeciliter) - - bluetoothStateManager.addBluetoothStateObserver(self) if let pumpManagerRawValue = UserDefaults.appGroup?.pumpManagerRawValue { pumpManager = pumpManagerFromRawValue(pumpManagerRawValue) @@ -280,7 +278,7 @@ final class DeviceDataManager { carbStore: carbStore, dosingDecisionStore: dosingDecisionStore, settingsStore: settingsStore, - alertPresenter: alertManager + alertIssuer: alertManager ) cacheStore.delegate = loopManager @@ -366,6 +364,38 @@ final class DeviceDataManager { return pluginManager.availablePumpManagers + availableStaticPumpManagers } + func setupPumpManager(withIdentifier identifier: String, initialSettings settings: PumpManagerSetupSettings) -> Swift.Result, Error> { + switch setupPumpManagerUI(withIdentifier: identifier, initialSettings: settings) { + case .failure(let error): + return .failure(error) + case .success(let success): + switch success { + case .userInteractionRequired(let viewController): + return .success(.userInteractionRequired(viewController)) + case .createdAndOnboarded(let pumpManagerUI): + return .success(.createdAndOnboarded(pumpManagerUI)) + } + } + } + + struct UnknownPumpManagerIdentifierError: Error {} + + func setupPumpManagerUI(withIdentifier identifier: String, initialSettings settings: PumpManagerSetupSettings) -> Swift.Result, Error> { + guard let pumpManagerUIType = pumpManagerTypeByIdentifier(identifier) else { + return .failure(UnknownPumpManagerIdentifierError()) + } + + let result = pumpManagerUIType.setupViewController(initialSettings: settings, bluetoothProvider: bluetoothProvider, colorPalette: .default) + if case .createdAndOnboarded(let pumpManagerUI) = result { + if let basalRateSchedule = loopManager.basalRateSchedule { + pumpManagerUI.syncBasalRateSchedule(items: basalRateSchedule.items, completion: { _ in }) + } + self.pumpManager = pumpManagerUI + } + + return .success(result) + } + public func pumpManagerTypeByIdentifier(_ identifier: String) -> PumpManagerUI.Type? { return pluginManager.getPumpManagerTypeByIdentifier(identifier) ?? staticPumpManagersByIdentifier[identifier] } @@ -425,6 +455,39 @@ final class DeviceDataManager { return availableCGMManagers } + func setupCGMManager(withIdentifier identifier: String) -> Swift.Result, Error> { + if let cgmManager = setupCGMManagerFromPumpManager(withIdentifier: identifier) { + return .success(.createdAndOnboarded(cgmManager)) + } + + switch setupCGMManagerUI(withIdentifier: identifier) { + case .failure(let error): + return .failure(error) + case .success(let success): + switch success { + case .userInteractionRequired(let viewController): + return .success(.userInteractionRequired(viewController)) + case .createdAndOnboarded(let cgmManagerUI): + return .success(.createdAndOnboarded(cgmManagerUI)) + } + } + } + + struct UnknownCGMManagerIdentifierError: Error {} + + fileprivate func setupCGMManagerUI(withIdentifier identifier: String) -> Swift.Result, Error> { + guard let cgmManagerUIType = cgmManagerTypeByIdentifier(identifier) else { + return .failure(UnknownCGMManagerIdentifierError()) + } + + let result = cgmManagerUIType.setupViewController(bluetoothProvider: bluetoothProvider, colorPalette: .default) + if case .createdAndOnboarded(let cgmManagerUI) = result { + self.cgmManager = cgmManagerUI + } + + return .success(result) + } + public func cgmManagerTypeByIdentifier(_ identifier: String) -> CGMManagerUI.Type? { return pluginManager.getCGMManagerTypeByIdentifier(identifier) ?? staticCGMManagersByIdentifier[identifier] as? CGMManagerUI.Type } @@ -464,9 +527,15 @@ final class DeviceDataManager { } } } + + func getHealthStoreAuthorization(_ completion: @escaping (HKAuthorizationRequestStatus) -> Void) { + healthStore.getRequestStatusForAuthorization(toShare: shareTypes, read: readTypes) { (authorizationRequestStatus, _) in + completion(authorizationRequestStatus) + } + } // Get HealthKit authorization for all of the stores - func authorizeHealthStore(_ completion: @escaping (Bool) -> Void) { + func authorizeHealthStore(_ completion: @escaping (HKAuthorizationRequestStatus) -> Void) { // Authorize all types at once for simplicity healthStore.requestAuthorization(toShare: shareTypes, read: readTypes) { (success, error) in if success { @@ -476,7 +545,7 @@ final class DeviceDataManager { self.glucoseStore.authorize(toShare: true, { _ in }) } - completion(success) + self.getHealthStoreAuthorization(completion) } } @@ -565,7 +634,7 @@ private extension DeviceDataManager { pumpManager?.delegateQueue = queue doseStore.device = pumpManager?.status.device - pumpManagerHUDProvider = pumpManager?.hudProvider(colorPalette: .default) + pumpManagerHUDProvider = pumpManager?.hudProvider(bluetoothProvider: bluetoothProvider, colorPalette: .default) // Proliferate PumpModel preferences to DoseStore if let pumpRecordsBasalProfileStartEvents = pumpManager?.pumpRecordsBasalProfileStartEvents { @@ -577,7 +646,7 @@ private extension DeviceDataManager { alertManager?.addAlertSoundVendor(managerIdentifier: pumpManager.managerIdentifier, soundVendor: pumpManager) - deliveryUncertaintyAlertManager = DeliveryUncertaintyAlertManager(pumpManager: pumpManager, rootViewController: rootViewController) + deliveryUncertaintyAlertManager = DeliveryUncertaintyAlertManager(pumpManager: pumpManager, alertPresenter: alertPresenter) } } @@ -699,7 +768,7 @@ extension DeviceDataManager: DeviceManagerDelegate { } // MARK: - UserAlertHandler -extension DeviceDataManager: AlertPresenter { +extension DeviceDataManager: AlertIssuer { static let managerIdentifier = "DeviceDataManager" func issueAlert(_ alert: Alert) { @@ -769,14 +838,22 @@ extension DeviceDataManager: CGMManagerDelegate { } } +// MARK: - CGMManagerCreateDelegate + extension DeviceDataManager: CGMManagerCreateDelegate { func cgmManagerCreateNotifying(didCreateCGMManager cgmManager: CGMManagerUI) { + log.default("CGM manager with identifier '%{public}@' created", cgmManager.managerIdentifier) self.cgmManager = cgmManager } } +// MARK: - CGMManagerOnboardDelegate + extension DeviceDataManager: CGMManagerOnboardDelegate { - func cgmManagerOnboardNotifying(didOnboardCGMManager cgmManager: CGMManagerUI) {} + func cgmManagerOnboardNotifying(didOnboardCGMManager cgmManager: CGMManagerUI) { + precondition(cgmManager.isOnboarded) + log.default("CGM manager with identifier '%{public}@' onboarded", cgmManager.managerIdentifier) + } } // MARK: - PumpManagerDelegate @@ -972,6 +1049,34 @@ extension DeviceDataManager: PumpManagerDelegate { } } +// MARK: - PumpManagerCreateDelegate + +extension DeviceDataManager: PumpManagerCreateDelegate { + func pumpManagerCreateNotifying(didCreatePumpManager pumpManager: PumpManagerUI) { + log.default("Pump manager with identifier '%{public}@' created", pumpManager.managerIdentifier) + self.pumpManager = pumpManager + } +} + +// MARK: - PumpManagerOnboardDelegate + +extension DeviceDataManager: PumpManagerOnboardDelegate { + func pumpManagerOnboardNotifying(didOnboardPumpManager pumpManager: PumpManagerUI, withFinalSettings settings: PumpManagerSetupSettings) { + precondition(pumpManager.isOnboarded) + log.default("Pump manager with identifier '%{public}@' onboarded", pumpManager.managerIdentifier) + + if let basalRateSchedule = settings.basalSchedule { + loopManager.basalRateSchedule = basalRateSchedule + } + if let maxBasalRateUnitsPerHour = settings.maxBasalRateUnitsPerHour { + loopManager.settings.maximumBasalRatePerHour = maxBasalRateUnitsPerHour + } + if let maxBolusUnits = settings.maxBolusUnits { + loopManager.settings.maximumBolus = maxBolusUnits + } + } +} + // MARK: - CarbStoreDelegate extension DeviceDataManager: CarbStoreDelegate { @@ -1267,16 +1372,6 @@ extension DeviceDataManager { } } -//MARK: - Bluetooth State Manager Observation - -extension DeviceDataManager: BluetoothStateManagerObserver { - func bluetoothStateManager(_ bluetoothStateManager: BluetoothStateManager, - bluetoothStateDidUpdate bluetoothState: BluetoothStateManager.BluetoothState) - { - self.bluetoothState = bluetoothState - } -} - fileprivate extension FileManager { var exportsDirectoryURL: URL { let applicationSupportDirectory = try! url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) diff --git a/Loop/Managers/LoopAlertsManager.swift b/Loop/Managers/LoopAlertsManager.swift index e953e6b687..565e017843 100644 --- a/Loop/Managers/LoopAlertsManager.swift +++ b/Loop/Managers/LoopAlertsManager.swift @@ -11,12 +11,6 @@ import LoopKit /// Class responsible for monitoring "system level" operations and alerting the user to any anomalous situations (e.g. bluetooth off) public class LoopAlertsManager { - public enum BluetoothState { - case on - case off - case unauthorized - } - static let managerIdentifier = "Loop" private lazy var log = DiagnosticLog(category: String(describing: LoopAlertsManager.self)) @@ -25,9 +19,9 @@ public class LoopAlertsManager { private let bluetoothPoweredOffIdentifier = Alert.Identifier(managerIdentifier: managerIdentifier, alertIdentifier: "bluetoothPoweredOff") - init(alertManager: AlertManager, bluetoothStateManager: BluetoothStateManager) { + init(alertManager: AlertManager, bluetoothProvider: BluetoothProvider) { self.alertManager = alertManager - bluetoothStateManager.addBluetoothStateObserver(self) + bluetoothProvider.addBluetoothObserver(self, queue: .main) } private func onBluetoothPermissionDenied() { @@ -61,18 +55,16 @@ public class LoopAlertsManager { } -// MARK: - Bluetooth State Observer +// MARK: - BluetoothObserver -extension LoopAlertsManager: BluetoothStateManagerObserver { - public func bluetoothStateManager(_ bluetoothStateManager: BluetoothStateManager, - bluetoothStateDidUpdate bluetoothState: BluetoothStateManager.BluetoothState) - { - switch bluetoothState { +extension LoopAlertsManager: BluetoothObserver { + public func bluetoothDidUpdateState(_ state: BluetoothState) { + switch state { case .poweredOn: onBluetoothPoweredOn() case .poweredOff: onBluetoothPoweredOff() - case .denied: + case .unauthorized: onBluetoothPermissionDenied() default: return diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift new file mode 100644 index 0000000000..3eae58c921 --- /dev/null +++ b/Loop/Managers/LoopAppManager.swift @@ -0,0 +1,330 @@ +// +// LoopAppManager.swift +// Loop +// +// Created by Darin Krauss on 2/16/21. +// Copyright © 2021 LoopKit Authors. All rights reserved. +// + +import UIKit +import LoopKit +import LoopKitUI + +public protocol AlertPresenter: AnyObject { + /// Present the alert view controller, with or without animation. + /// - Parameters: + /// - viewControllerToPresent: The alert view controller to present. + /// - animated: Animate the alert view controller presentation or not. + /// - completion: Completion to call once view controller is presented. + func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)?) + + /// Retract any alerts with the given identifier. This includes both pending and delivered alerts. + + /// Dismiss the topmost view controller, presumably the alert view controller. + /// - Parameters: + /// - animated: Animate the alert view controller dismissal or not. + /// - completion: Completion to call once view controller is dismissed. + func dismiss(animated: Bool, completion: (() -> Void)?) +} + +public extension AlertPresenter { + func present(_ viewController: UIViewController, animated: Bool) { present(viewController, animated: animated, completion: nil) } + func dismiss(animated: Bool) { dismiss(animated: animated, completion: nil) } +} + +class LoopAppManager: NSObject { + private enum State: Int { + case initialize + case launchManagers + case launchOnboarding + case launchHomeScreen + case launchComplete + + var next: State { State(rawValue: rawValue + 1) ?? .launchComplete } + } + + private var launchOptions: [UIApplication.LaunchOptionsKey: Any]? + private var window: UIWindow? + + private var pluginManager: PluginManager! + private var bluetoothStateManager: BluetoothStateManager! + private var alertManager: AlertManager! + private var loopAlertsManager: LoopAlertsManager! + private var trustedTimeChecker: TrustedTimeChecker! + private var deviceDataManager: DeviceDataManager! + private var onboardingManager: OnboardingManager! + + private var state: State = .initialize + + private let log = DiagnosticLog(category: "LoopAppManager") + + // MARK: - Initialization + + func initialize(with launchOptions: [UIApplication.LaunchOptionsKey: Any]?) { + dispatchPrecondition(condition: .onQueue(.main)) + precondition(state == .initialize) + + self.launchOptions = launchOptions + + registerBackgroundTasks() + + self.state = state.next + } + + func launch(into window: UIWindow?) { + dispatchPrecondition(condition: .onQueue(.main)) + precondition(!isLaunchComplete) + precondition(state != .initialize) + + guard isProtectedDataAvailable() else { + log.default("Protected data not available; deferring launch...") + return + } + + self.window = window + + window?.tintColor = .loopAccent + OrientationLock.deviceOrientationController = self + UNUserNotificationCenter.current().delegate = self + + resumeLaunch() + } + + var isLaunchComplete: Bool { state == .launchComplete } + + private func resumeLaunch() { + if state == .launchManagers { + launchManagers() + } + if state == .launchOnboarding { + launchOnboarding() + } + if state == .launchHomeScreen { + launchHomeScreen() + } + } + + private func launchManagers() { + dispatchPrecondition(condition: .onQueue(.main)) + precondition(state == .launchManagers) + + self.pluginManager = PluginManager() + self.bluetoothStateManager = BluetoothStateManager() + self.alertManager = AlertManager(alertPresenter: self, + expireAfter: Bundle.main.localCacheDuration) + self.loopAlertsManager = LoopAlertsManager(alertManager: alertManager, + bluetoothProvider: bluetoothStateManager) + self.trustedTimeChecker = TrustedTimeChecker(alertManager: alertManager) + self.deviceDataManager = DeviceDataManager(pluginManager: pluginManager, + alertManager: alertManager, + bluetoothProvider: bluetoothStateManager, + alertPresenter: self) + SharedLogging.instance = deviceDataManager.loggingServicesManager + + scheduleBackgroundTasks() + + self.onboardingManager = OnboardingManager(pluginManager: pluginManager, + bluetoothProvider: bluetoothStateManager, + deviceDataManager: deviceDataManager, + servicesManager: deviceDataManager.servicesManager, + loopDataManager: deviceDataManager.loopManager, + window: window, + userDefaults: UserDefaults.appGroup!) + + deviceDataManager.analyticsServicesManager.application(didFinishLaunchingWithOptions: launchOptions) + + self.state = state.next + } + + private func launchOnboarding() { + dispatchPrecondition(condition: .onQueue(.main)) + precondition(state == .launchOnboarding) + + onboardingManager.onboard { + DispatchQueue.main.async { + self.state = self.state.next + self.resumeLaunch() + } + } + } + + private func launchHomeScreen() { + dispatchPrecondition(condition: .onQueue(.main)) + precondition(state == .launchHomeScreen) + + let storyboard = UIStoryboard(name: "Main", bundle: Bundle(for: Self.self)) + let statusTableViewController = storyboard.instantiateViewController(withIdentifier: "MainStatusViewController") as! StatusTableViewController + statusTableViewController.deviceManager = deviceDataManager + bluetoothStateManager.addBluetoothObserver(statusTableViewController) + + let rootNavigationController = RootNavigationController() + rootViewController = rootNavigationController + rootNavigationController.setViewControllers([statusTableViewController], animated: true) + + handleRemoteNotificationFromLaunchOptions() + + self.launchOptions = nil + + self.state = state.next + } + + // MARK: - Life Cycle + + func didBecomeActive() { + deviceDataManager?.updatePumpManagerBLEHeartbeatPreference() + } + + // MARK: - Remote Notification + + func setRemoteNotificationsDeviceToken(_ remoteNotificationsDeviceToken: Data) { + deviceDataManager?.loopManager.settings.deviceToken = remoteNotificationsDeviceToken + } + + private func handleRemoteNotificationFromLaunchOptions() { + handleRemoteNotification(launchOptions?[.remoteNotification] as? [String: AnyObject]) + } + + @discardableResult + func handleRemoteNotification(_ notification: [String: AnyObject]?) -> Bool { + guard let notification = notification else { + return false + } + deviceDataManager?.handleRemoteNotification(notification) + return true + } + + // MARK: - Continuity + + func userActivity(_ userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + if #available(iOS 12.0, *) { + if userActivity.activityType == NewCarbEntryIntent.className { + log.default("Restoring %{public}@ intent", userActivity.activityType) + rootViewController?.restoreUserActivityState(.forNewCarbEntry()) + return true + } + } + + switch userActivity.activityType { + case NSUserActivity.newCarbEntryActivityType, + NSUserActivity.viewLoopStatusActivityType: + log.default("Restoring %{public}@ activity", userActivity.activityType) + if let rootViewController = rootViewController { + restorationHandler([rootViewController]) + } + return true + default: + return false + } + } + + // MARK: - Interface + + private static let defaultSupportedInterfaceOrientations = UIInterfaceOrientationMask.allButUpsideDown + + var supportedInterfaceOrientations = defaultSupportedInterfaceOrientations + + // MARK: - Background Tasks + + private func registerBackgroundTasks() { + if DeviceDataManager.registerCriticalEventLogHistoricalExportBackgroundTask({ self.deviceDataManager?.handleCriticalEventLogHistoricalExportBackgroundTask($0) }) { + log.debug("Critical event log export background task registered") + } else { + log.error("Critical event log export background task not registered") + } + } + + private func scheduleBackgroundTasks() { + deviceDataManager?.scheduleCriticalEventLogHistoricalExportBackgroundTask() + } + + // MARK: - Private + + private func isProtectedDataAvailable() -> Bool { + let fileManager = FileManager.default + do { + let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false) + let fileURL = documentDirectory.appendingPathComponent("protection.test") + guard fileManager.fileExists(atPath: fileURL.path) else { + let contents = Data("unimportant".utf8) + try? contents.write(to: fileURL, options: .completeFileProtectionUntilFirstUserAuthentication) + // If file doesn't exist, we're at first start, which will be user directed. + return true + } + let contents = try? Data(contentsOf: fileURL) + return contents != nil + } catch { + log.error("Could not create after first unlock test file: %@", String(describing: error)) + } + return false + } + + private var rootViewController: UIViewController? { + get { window?.rootViewController } + set { window?.rootViewController = newValue } + } +} + +// MARK: - AlertPresenter + +extension LoopAppManager: AlertPresenter { + func present(_ viewControllerToPresent: UIViewController, animated: Bool, completion: (() -> Void)?) { + rootViewController?.present(viewControllerToPresent, animated: animated, completion: completion) + } + + func dismiss(animated: Bool, completion: (() -> Void)?) { + rootViewController?.dismiss(animated: animated, completion: completion) + } +} + +// MARK: - DeviceOrientationController + +extension LoopAppManager: DeviceOrientationController { + func setDefaultSupportedInferfaceOrientations() { + supportedInterfaceOrientations = Self.defaultSupportedInterfaceOrientations + } +} + +// MARK: - UNUserNotificationCenterDelegate + +extension LoopAppManager: UNUserNotificationCenterDelegate { + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + switch notification.request.identifier { + // TODO: Until these notifications are converted to use the new alert system, they shall still show in the foreground + case LoopNotificationCategory.bolusFailure.rawValue, + LoopNotificationCategory.pumpBatteryLow.rawValue, + LoopNotificationCategory.pumpExpired.rawValue, + LoopNotificationCategory.pumpFault.rawValue: + completionHandler([.badge, .sound, .alert]) + default: + // All other userNotifications are not to be displayed while in the foreground + completionHandler([]) + } + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { + switch response.actionIdentifier { + case NotificationManager.Action.retryBolus.rawValue: + if let units = response.notification.request.content.userInfo[LoopNotificationUserInfoKey.bolusAmount.rawValue] as? Double, + let startDate = response.notification.request.content.userInfo[LoopNotificationUserInfoKey.bolusStartDate.rawValue] as? Date, + startDate.timeIntervalSinceNow >= TimeInterval(minutes: -5) + { + deviceDataManager?.analyticsServicesManager.didRetryBolus() + + deviceDataManager?.enactBolus(units: units, at: startDate) { (_) in + completionHandler() + } + return + } + case NotificationManager.Action.acknowledgeAlert.rawValue: + let userInfo = response.notification.request.content.userInfo + if let alertIdentifier = userInfo[LoopNotificationUserInfoKey.alertTypeID.rawValue] as? Alert.AlertIdentifier, + let managerIdentifier = userInfo[LoopNotificationUserInfoKey.managerIDForAlert.rawValue] as? String { + alertManager?.acknowledgeAlert(identifier: Alert.Identifier(managerIdentifier: managerIdentifier, alertIdentifier: alertIdentifier)) + } + default: + break + } + + completionHandler() + } +} diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 883ea952a3..67754bf237 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -70,7 +70,7 @@ final class LoopDataManager: LoopSettingsAlerterDelegate { dosingDecisionStore: DosingDecisionStoreProtocol, settingsStore: SettingsStoreProtocol, now: @escaping () -> Date = { Date() }, - alertPresenter: AlertPresenter? = nil + alertIssuer: AlertIssuer? = nil ) { self.analyticsServicesManager = analyticsServicesManager self.lockedLastLoopCompleted = Locked(lastLoopCompleted) @@ -94,11 +94,14 @@ final class LoopDataManager: LoopSettingsAlerterDelegate { retrospectiveCorrection = settings.enabledRetrospectiveCorrectionAlgorithm - loopSettingsAlerter = LoopSettingsAlerter(alertPresenter: alertPresenter) + loopSettingsAlerter = LoopSettingsAlerter(alertIssuer: alertIssuer) loopSettingsAlerter.delegate = self overrideHistory.delegate = self + // Required for device settings in stored dosing decisions + UIDevice.current.isBatteryMonitoringEnabled = true + // Observe changes notificationObservers = [ NotificationCenter.default.addObserver( @@ -2019,15 +2022,30 @@ extension LoopDataManager { extension LoopDataManager { public var therapySettings: TherapySettings { - TherapySettings(glucoseTargetRangeSchedule: settings.glucoseTargetRangeSchedule, - preMealTargetRange: settings.preMealTargetRange, - workoutTargetRange: settings.legacyWorkoutTargetRange, - maximumBasalRatePerHour: settings.maximumBasalRatePerHour, - maximumBolus: settings.maximumBolus, - suspendThreshold: settings.suspendThreshold, - insulinSensitivitySchedule: insulinSensitivitySchedule, - carbRatioSchedule: carbRatioSchedule, - basalRateSchedule: basalRateSchedule, - insulinModelSettings: insulinModelSettings) + get { + TherapySettings(glucoseTargetRangeSchedule: settings.glucoseTargetRangeSchedule, + preMealTargetRange: settings.preMealTargetRange, + workoutTargetRange: settings.legacyWorkoutTargetRange, + maximumBasalRatePerHour: settings.maximumBasalRatePerHour, + maximumBolus: settings.maximumBolus, + suspendThreshold: settings.suspendThreshold, + insulinSensitivitySchedule: insulinSensitivitySchedule, + carbRatioSchedule: carbRatioSchedule, + basalRateSchedule: basalRateSchedule, + insulinModelSettings: insulinModelSettings) + } + + set { + settings.glucoseTargetRangeSchedule = newValue.glucoseTargetRangeSchedule + settings.preMealTargetRange = newValue.preMealTargetRange + settings.legacyWorkoutTargetRange = newValue.workoutTargetRange + settings.suspendThreshold = newValue.suspendThreshold + settings.maximumBolus = newValue.maximumBolus + settings.maximumBasalRatePerHour = newValue.maximumBasalRatePerHour + insulinSensitivitySchedule = newValue.insulinSensitivitySchedule + carbRatioSchedule = newValue.carbRatioSchedule + basalRateSchedule = newValue.basalRateSchedule + insulinModelSettings = newValue.insulinModelSettings + } } } diff --git a/Loop/Managers/NotificationManager.swift b/Loop/Managers/NotificationManager.swift index 9d780fac3f..d1e482a09a 100644 --- a/Loop/Managers/NotificationManager.swift +++ b/Loop/Managers/NotificationManager.swift @@ -51,19 +51,22 @@ extension NotificationManager { return Set(categories) } - static func authorize(delegate: UNUserNotificationCenterDelegate) { + static func getAuthorization(_ completion: @escaping (UNAuthorizationStatus) -> Void) { + UNUserNotificationCenter.current().getNotificationSettings { settings in + completion(settings.authorizationStatus) + } + } + + static func authorize(_ completion: @escaping (UNAuthorizationStatus) -> Void) { var authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] if FeatureFlags.criticalAlertsEnabled, #available(iOS 12.0, *) { authOptions.insert(.criticalAlert) } let center = UNUserNotificationCenter.current() - center.delegate = delegate center.requestAuthorization(options: authOptions) { (granted, error) in - guard granted else { - return - } UNUserNotificationCenter.current().getNotificationSettings { settings in + completion(settings.authorizationStatus) guard settings.authorizationStatus == .authorized else { return } diff --git a/Loop/Managers/OnboardingManager.swift b/Loop/Managers/OnboardingManager.swift new file mode 100644 index 0000000000..11d3f6a139 --- /dev/null +++ b/Loop/Managers/OnboardingManager.swift @@ -0,0 +1,372 @@ +// +// OnboardingManager.swift +// Loop +// +// Created by Darin Krauss on 2/19/21. +// Copyright © 2021 LoopKit Authors. All rights reserved. +// + +import HealthKit +import LoopKit +import LoopKitUI + +class OnboardingManager { + private let pluginManager: PluginManager + private let bluetoothProvider: BluetoothProvider + private let deviceDataManager: DeviceDataManager + private let servicesManager: ServicesManager + private let loopDataManager: LoopDataManager + private let window: UIWindow? + private let userDefaults: UserDefaults + + private var isOnboarded: Bool { + didSet { userDefaults.onboardingManagerIsOnboarded = isOnboarded } + } + private var completedOnboardingIdentifiers: [String] = [] { + didSet { userDefaults.onboardingManagerCompletedOnboardingIdentifiers = completedOnboardingIdentifiers } + } + private var activeOnboarding: OnboardingUI? = nil { + didSet { userDefaults.onboardingManagerActiveOnboardingRawValue = activeOnboarding?.rawValue } + } + + private var completion: (() -> Void)? + + init(pluginManager: PluginManager, bluetoothProvider: BluetoothProvider, deviceDataManager: DeviceDataManager, servicesManager: ServicesManager, loopDataManager: LoopDataManager, window: UIWindow?, userDefaults: UserDefaults = .standard) { + self.pluginManager = pluginManager + self.bluetoothProvider = bluetoothProvider + self.deviceDataManager = deviceDataManager + self.servicesManager = servicesManager + self.loopDataManager = loopDataManager + self.window = window + self.userDefaults = userDefaults + + self.isOnboarded = userDefaults.onboardingManagerIsOnboarded + if !isOnboarded { + self.completedOnboardingIdentifiers = userDefaults.onboardingManagerCompletedOnboardingIdentifiers + if let activeOnboardingRawValue = userDefaults.onboardingManagerActiveOnboardingRawValue { + self.activeOnboarding = onboardingFromRawValue(activeOnboardingRawValue) + self.activeOnboarding?.onboardingDelegate = self + } + } + } + + func onboard(_ completion: @escaping () -> Void) { + self.completion = completion + resumeOnboarding() + } + + private func resumeOnboarding() { + dispatchPrecondition(condition: .onQueue(.main)) + + guard !isOnboarded else { + complete() + return + } + + if let onboarding = nextActiveOnboarding { + displayOnboarding(onboarding) + return + } + + ensureAuthorization { + DispatchQueue.main.async { + self.isOnboarded = true + self.complete() + } + } + } + + private var nextActiveOnboarding: OnboardingUI? { + if activeOnboarding == nil { + self.activeOnboarding = nextOnboarding + self.activeOnboarding?.onboardingDelegate = self + } + return activeOnboarding + } + + private var nextOnboarding: OnboardingUI? { + let onboardingIdentifiers = pluginManager.availableOnboardingIdentifiers.filter { !completedOnboardingIdentifiers.contains($0) } + for onboardingIdentifier in onboardingIdentifiers { + guard let onboardingType = onboardingTypeByIdentifier(onboardingIdentifier) else { + continue + } + + let onboarding = onboardingType.createOnboarding() + guard !onboarding.isOnboarded else { + completedOnboardingIdentifiers.append(onboarding.onboardingIdentifier) + continue + } + + return onboarding + } + return nil + } + + private func displayOnboarding(_ onboarding: OnboardingUI) { + var onboardingViewController = onboarding.onboardingViewController(onboardingProvider: self, displayGlucoseUnitObservable: deviceDataManager.displayGlucoseUnitObservable, colorPalette: .default) + onboardingViewController.cgmManagerCreateDelegate = deviceDataManager + onboardingViewController.cgmManagerOnboardDelegate = deviceDataManager + onboardingViewController.pumpManagerCreateDelegate = deviceDataManager + onboardingViewController.pumpManagerOnboardDelegate = deviceDataManager + onboardingViewController.serviceCreateDelegate = servicesManager + onboardingViewController.serviceOnboardDelegate = servicesManager + onboardingViewController.completionDelegate = self + + window?.rootViewController = onboardingViewController + } + + private func completeActiveOnboarding() { + dispatchPrecondition(condition: .onQueue(.main)) + + if let activeOnboarding = self.activeOnboarding { + completedOnboardingIdentifiers.append(activeOnboarding.onboardingIdentifier) + self.activeOnboarding = nil + } + resumeOnboarding() + } + + private func ensureAuthorization(_ completion: @escaping () -> Void) { + ensureNotificationAuthorization { + self.ensureHealthStoreAuthorization { + self.ensureBluetoothAuthorization(completion) + } + } + } + + private func ensureNotificationAuthorization(_ completion: @escaping () -> Void) { + getNotificationAuthorization { authorization in + guard authorization == .notDetermined else { + completion() + return + } + self.authorizeNotification { _ in completion() } + } + } + + private func ensureHealthStoreAuthorization(_ completion: @escaping () -> Void) { + getHealthStoreAuthorization { authorization in + guard authorization == .notDetermined else { + completion() + return + } + self.authorizeHealthStore { _ in completion() } + } + } + + private func ensureBluetoothAuthorization(_ completion: @escaping () -> Void) { + guard bluetoothAuthorization == .notDetermined else { + completion() + return + } + authorizeBluetooth { _ in completion() } + } + + private func complete() { + if let completion = completion { + self.completion = nil + completion() + } + } + + // MARK: - State + + private func onboardingFromRawValue(_ rawValue: OnboardingUI.RawValue) -> OnboardingUI? { + guard let onboardingType = onboardingTypeFromRawValue(rawValue), + let rawState = rawValue["state"] as? OnboardingUI.RawState + else { + return nil + } + + return onboardingType.init(rawState: rawState) + } + + private func onboardingTypeFromRawValue(_ rawValue: OnboardingUI.RawValue) -> OnboardingUI.Type? { + guard let identifier = rawValue["onboardingIdentifier"] as? String else { + return nil + } + + return onboardingTypeByIdentifier(identifier) + } + + private func onboardingTypeByIdentifier(_ identifier: String) -> OnboardingUI.Type? { + return pluginManager.getOnboardingTypeByIdentifier(identifier) + } +} + +// MARK: - OnboardingDelegate + +extension OnboardingManager: OnboardingDelegate { + func onboardingDidUpdateState(_ onboarding: OnboardingUI) { + precondition(onboarding === activeOnboarding) + userDefaults.onboardingManagerActiveOnboardingRawValue = onboarding.rawValue + } + + func onboarding(_ onboarding: OnboardingUI, hasNewTherapySettings therapySettings: TherapySettings) { + precondition(onboarding === activeOnboarding) + loopDataManager.therapySettings = therapySettings + } +} + +// MARK: - CompletionDelegate + +extension OnboardingManager: CompletionDelegate { + func completionNotifyingDidComplete(_ object: CompletionNotifying) { + DispatchQueue.main.async { + self.completeActiveOnboarding() + } + } +} + +// MARK: - NotificationAuthorizationProvider + +extension OnboardingManager: NotificationAuthorizationProvider { + func getNotificationAuthorization(_ completion: @escaping (NotificationAuthorization) -> Void) { + NotificationManager.getAuthorization { completion(NotificationAuthorization($0)) } + } + + func authorizeNotification(_ completion: @escaping (NotificationAuthorization) -> Void) { + NotificationManager.authorize{ completion(NotificationAuthorization($0)) } + } +} + +// MARK: - HealthStoreAuthorizationProvider + +extension OnboardingManager: HealthStoreAuthorizationProvider { + func getHealthStoreAuthorization(_ completion: @escaping (HealthStoreAuthorization) -> Void) { + deviceDataManager.getHealthStoreAuthorization { completion(HealthStoreAuthorization($0)) } + } + + func authorizeHealthStore(_ completion: @escaping (HealthStoreAuthorization) -> Void) { + deviceDataManager.authorizeHealthStore { completion(HealthStoreAuthorization($0)) } + } +} + +// MARK: - BluetoothProvider + +extension OnboardingManager: BluetoothProvider { + var bluetoothAuthorization: BluetoothAuthorization { bluetoothProvider.bluetoothAuthorization } + + var bluetoothState: BluetoothState { bluetoothProvider.bluetoothState } + + func authorizeBluetooth(_ completion: @escaping (BluetoothAuthorization) -> Void) { bluetoothProvider.authorizeBluetooth(completion) } + + func addBluetoothObserver(_ observer: BluetoothObserver, queue: DispatchQueue) { bluetoothProvider.addBluetoothObserver(observer, queue: queue) } + + func removeBluetoothObserver(_ observer: BluetoothObserver) { bluetoothProvider.removeBluetoothObserver(observer) } +} + +// MARK: - CGMManagerProvider + +extension OnboardingManager: CGMManagerProvider { + var activeCGMManager: CGMManager? { deviceDataManager.cgmManager } + + var availableCGMManagers: [CGMManagerDescriptor] { deviceDataManager.availableCGMManagers } + + func setupCGMManager(withIdentifier identifier: String) -> Swift.Result, Error> { + return deviceDataManager.setupCGMManager(withIdentifier: identifier) + } +} + +// MARK: - PumpManagerProvider + +extension OnboardingManager: PumpManagerProvider { + var activePumpManager: PumpManager? { deviceDataManager.pumpManager } + + var availablePumpManagers: [PumpManagerDescriptor] { deviceDataManager.availablePumpManagers } + + func setupPumpManager(withIdentifier identifier: String, initialSettings settings: PumpManagerSetupSettings) -> Swift.Result, Error> { + return deviceDataManager.setupPumpManager(withIdentifier: identifier, initialSettings: settings) + } +} + +// MARK: - ServiceProvider + +extension OnboardingManager: ServiceProvider { + var activeServices: [Service] { servicesManager.activeServices } + + var availableServices: [ServiceDescriptor] { servicesManager.availableServices } + + func setupService(withIdentifier identifier: String) -> Swift.Result, Error> { + return servicesManager.setupService(withIdentifier: identifier) + } +} + +// MARK: - OnboardingProvider + +extension OnboardingManager: OnboardingProvider {} + +// MARK: - OnboardingUI + +fileprivate extension OnboardingUI { + typealias RawValue = [String: Any] + + var rawValue: RawValue { + return [ + "onboardingIdentifier": onboardingIdentifier, + "state": rawState + ] + } +} + +// MARK: - UserDefaults + +fileprivate extension UserDefaults { + private enum Key: String { + case onboardingManagerIsOnboarded = "com.loopkit.Loop.OnboardingManager.IsOnboarded" + case onboardingManagerCompletedOnboardingIdentifiers = "com.loopkit.Loop.OnboardingManager.CompletedOnboardingIdentifiers" + case onboardingManagerActiveOnboardingRawValue = "com.loopkit.Loop.OnboardingManager.ActiveOnboardingRawValue" + } + + var onboardingManagerIsOnboarded: Bool { + get { bool(forKey: Key.onboardingManagerIsOnboarded.rawValue) } + set { set(newValue, forKey: Key.onboardingManagerIsOnboarded.rawValue) } + } + + var onboardingManagerCompletedOnboardingIdentifiers: [String] { + get { array(forKey: Key.onboardingManagerCompletedOnboardingIdentifiers.rawValue) as? [String] ?? [] } + set { set(newValue, forKey: Key.onboardingManagerCompletedOnboardingIdentifiers.rawValue) } + } + + var onboardingManagerActiveOnboardingRawValue: OnboardingUI.RawValue? { + get { object(forKey: Key.onboardingManagerActiveOnboardingRawValue.rawValue) as? OnboardingUI.RawValue } + set { set(newValue, forKey: Key.onboardingManagerActiveOnboardingRawValue.rawValue) } + } +} + +// MARK: - NotificationAuthorization + +extension NotificationAuthorization { + init(_ authorization: UNAuthorizationStatus) { + switch authorization { + case .notDetermined: + self = .notDetermined + case .denied: + self = .denied + case .authorized: + self = .authorized + case .provisional: + self = .provisional + case .ephemeral: + self = .denied + @unknown default: + self = .notDetermined + } + } +} + +// MARK: - HealthStoreAuthorization + +extension HealthStoreAuthorization { + init(_ authorization: HKAuthorizationRequestStatus) { + switch authorization { + case .unknown: + self = .notDetermined + case .shouldRequest: + self = .notDetermined + case .unnecessary: + self = .determined + @unknown default: + self = .notDetermined + } + } +} diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index f3c20dd614..26a55e099a 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -44,6 +44,35 @@ class ServicesManager { return pluginManager.availableServices + availableStaticServices } + func setupService(withIdentifier identifier: String) -> Swift.Result, Error> { + switch setupServiceUI(withIdentifier: identifier) { + case .failure(let error): + return .failure(error) + case .success(let success): + switch success { + case .userInteractionRequired(let viewController): + return .success(.userInteractionRequired(viewController)) + case .createdAndOnboarded(let serviceUI): + return .success(.createdAndOnboarded(serviceUI)) + } + } + } + + struct UnknownServiceIdentifierError: Error {} + + fileprivate func setupServiceUI(withIdentifier identifier: String) -> Swift.Result, Error> { + guard let serviceUIType = serviceUITypeByIdentifier(identifier) else { + return .failure(UnknownServiceIdentifierError()) + } + + let result = serviceUIType.setupViewController(colorPalette: .default) + if case .createdAndOnboarded(let serviceUI) = result { + addActiveService(serviceUI) + } + + return .success(result) + } + func serviceUITypeByIdentifier(_ identifier: String) -> ServiceUI.Type? { return pluginManager.getServiceTypeByIdentifier(identifier) ?? staticServicesByIdentifier[identifier] as? ServiceUI.Type } @@ -135,6 +164,8 @@ class ServicesManager { } } +// MARK: - ServiceDelegate + extension ServicesManager: ServiceDelegate { func serviceDidUpdateState(_ service: Service) { saveState() @@ -146,6 +177,24 @@ extension ServicesManager: ServiceDelegate { } } +// MARK: - ServiceCreateDelegate + +extension ServicesManager: ServiceCreateDelegate { + func serviceCreateNotifying(didCreateService service: Service) { + log.default("Service with identifier '%{public}@' created", service.serviceIdentifier) + addActiveService(service) + } +} + +// MARK: - ServiceCreateDelegate + +extension ServicesManager: ServiceOnboardDelegate { + func serviceOnboardNotifying(didOnboardService service: Service) { + precondition(service.isOnboarded) + log.default("Service with identifier '%{public}@' onboarded", service.serviceIdentifier) + } +} + extension ServicesManager { var availableSupports: [SupportUI] { activeServices.compactMap { $0 as? SupportUI } } } diff --git a/Loop/Managers/TrustedTimeChecker.swift b/Loop/Managers/TrustedTimeChecker.swift index fe7817fa24..e8d867ce13 100644 --- a/Loop/Managers/TrustedTimeChecker.swift +++ b/Loop/Managers/TrustedTimeChecker.swift @@ -34,7 +34,7 @@ class TrustedTimeChecker { private weak var alertManager: AlertManager? private lazy var log = DiagnosticLog(category: "TrustedTimeChecker") - init(_ alertManager: AlertManager) { + init(alertManager: AlertManager) { ntpClient = TrueTimeClient.sharedInstance #if DEBUG if ntpClient.responds(to: #selector(setter: TrueTimeClient.logCallback)) { diff --git a/Loop/Plugins/PluginManager.swift b/Loop/Plugins/PluginManager.swift index f799b195e4..850615d2e7 100644 --- a/Loop/Plugins/PluginManager.swift +++ b/Loop/Plugins/PluginManager.swift @@ -6,6 +6,7 @@ // Copyright © 2019 LoopKit Authors. All rights reserved. // +import os.log import Foundation import LoopKit import LoopKitUI @@ -15,6 +16,8 @@ class PluginManager { public let availableSupports: [SupportUI] + private let log = OSLog(category: "PluginManager") + public init(pluginsURL: URL? = Bundle.main.privateFrameworksURL) { var bundles = [Bundle]() var availableSupports = [SupportUI]() @@ -24,7 +27,7 @@ class PluginManager { for pluginURL in try FileManager.default.contentsOfDirectory(at: pluginsURL, includingPropertiesForKeys: nil).filter({$0.path.hasSuffix(".framework")}) { if let bundle = Bundle(url: pluginURL) { if bundle.isLoopPlugin { - print("Found loop plugin at \(pluginURL)") + log.debug("Found loop plugin: %{public}@", pluginURL.absoluteString) bundles.append(bundle) if bundle.isSupportPlugin { if let support = try bundle.loadAndInstantiateSupport() { @@ -33,7 +36,7 @@ class PluginManager { } } if bundle.isLoopExtension { - print("Found Loop extension at \(pluginURL), loading...") + log.debug("Found loop extension: %{public}@", pluginURL.absoluteString) if let support = try bundle.loadAndInstantiateSupport() { availableSupports.append(support) } @@ -41,7 +44,7 @@ class PluginManager { } } } catch let error { - print("Error loading plugins: \(String(describing: error))") + log.error("Error loading plugins: %{public}@", String(describing: error)) } } self.pluginBundles = bundles @@ -66,7 +69,7 @@ class PluginManager { fatalError("PrincipalClass not found") } } catch let error { - print(error) + log.error("Error loading plugin: %{public}@", String(describing: error)) } } } @@ -102,7 +105,7 @@ class PluginManager { fatalError("PrincipalClass not found") } } catch let error { - print(error) + log.error("Error loading plugin: %{public}@", String(describing: error)) } } } @@ -138,7 +141,7 @@ class PluginManager { fatalError("PrincipalClass not found") } } catch let error { - print(error) + log.error("Error loading plugin: %{public}@", String(describing: error)) } } } @@ -174,7 +177,7 @@ class PluginManager { fatalError("PrincipalClass not found") } } catch let error { - print(error) + log.error("Error loading plugin: %{public}@", String(describing: error)) } } } diff --git a/Loop/View Controllers/StatusTableViewController.swift b/Loop/View Controllers/StatusTableViewController.swift index f5b0a80b11..990b93fafe 100644 --- a/Loop/View Controllers/StatusTableViewController.swift +++ b/Loop/View Controllers/StatusTableViewController.swift @@ -131,22 +131,13 @@ final class StatusTableViewController: LoopChartsTableViewController { } } - private var isOnboardingComplete: Bool { deviceManager.loopManager.therapySettings.isComplete } - - private var hasOnboarding: Bool { !deviceManager.pluginManager.availableOnboardingIdentifiers.isEmpty } - - private func navigateToOnboarding() { - if let identifier = deviceManager.pluginManager.availableOnboardingIdentifiers.first { - setupOnboarding(withIdentifier: identifier) - } - } - private var appearedOnce = false override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.setNavigationBarHidden(true, animated: animated) + navigationController?.setToolbarHidden(false, animated: animated) updateBolusProgress() } @@ -155,17 +146,10 @@ final class StatusTableViewController: LoopChartsTableViewController { super.viewDidAppear(animated) if !appearedOnce { - authorizeHealthStore { success in - self.appearedOnce = success - if success { - DispatchQueue.main.async { - self.log.debug("[reloadData] after HealthKit authorization") - self.reloadData() - if !self.isOnboardingComplete && self.hasOnboarding { - self.navigateToOnboarding() - } - } - } + self.appearedOnce = true + DispatchQueue.main.async { + self.log.debug("[reloadData] after HealthKit authorization") + self.reloadData() } } @@ -175,18 +159,6 @@ final class StatusTableViewController: LoopChartsTableViewController { deviceManager.checkDeliveryUncertaintyState() } - - private func authorizeHealthStore(completion: @escaping (Bool) -> Void) { - deviceManager.authorizeHealthStore { accessFormWasCompleted in - // returned Bool only indicates if the user completed the health access form. - if accessFormWasCompleted { - completion(accessFormWasCompleted) - } else { - // if the user did not complete the health access form, present the health access form again so the user can allow or deny access - self.authorizeHealthStore(completion: completion) - } - } - } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) @@ -320,7 +292,7 @@ final class StatusTableViewController: LoopChartsTableViewController { // This should be kept up to date immediately hudView?.loopCompletionHUD.lastLoopCompleted = deviceManager.loopManager.lastLoopCompleted - guard !reloading && !deviceManager.authorizationRequired && isOnboardingComplete else { + guard !reloading && !deviceManager.authorizationRequired else { return } @@ -1423,11 +1395,11 @@ final class StatusTableViewController: LoopChartsTableViewController { } private func onPumpTapped() { - guard var settingsViewController = deviceManager.pumpManager?.settingsViewController(colorPalette: .default) else { + guard var settingsViewController = deviceManager.pumpManager?.settingsViewController(bluetoothProvider: deviceManager.bluetoothProvider, colorPalette: .default) else { // assert? return } - settingsViewController.pumpManagerOnboardDelegate = self + settingsViewController.pumpManagerOnboardDelegate = deviceManager settingsViewController.completionDelegate = self show(settingsViewController, sender: self) } @@ -1438,8 +1410,8 @@ final class StatusTableViewController: LoopChartsTableViewController { return } - var settings = cgmManager.settingsViewController(for: deviceManager.displayGlucoseUnitObservable, colorPalette: .default) - settings.cgmManagerOnboardDelegate = self + var settings = cgmManager.settingsViewController(for: deviceManager.displayGlucoseUnitObservable, bluetoothProvider: deviceManager.bluetoothProvider, colorPalette: .default) + settings.cgmManagerOnboardDelegate = deviceManager settings.completionDelegate = self show(settings, sender: self) } @@ -1676,11 +1648,6 @@ final class StatusTableViewController: LoopChartsTableViewController { self.presentError(error) } }) - if hasOnboarding { - actionSheet.addAction(UIAlertAction(title: "Present Onboarding", style: .default) { _ in - self.navigateToOnboarding() - }) - } if FeatureFlags.mockTherapySettingsEnabled { actionSheet.addAction(UIAlertAction(title: "Mock Therapy Settings", style: .default) { _ in let settings = TherapySettings.mockTherapySettings @@ -1915,14 +1882,14 @@ extension StatusTableViewController: AddEditOverrideTableViewControllerDelegate extension StatusTableViewController { fileprivate func addCGMManager(withIdentifier identifier: String) { - switch setupCGMManager(withIdentifier: identifier) { + switch deviceManager.setupCGMManager(withIdentifier: identifier) { case .failure(let error): log.default("Failure to setup CGM manager with identifier '%{public}@': %{public}@", identifier, String(describing: error)) case .success(let success): switch success { case .userInteractionRequired(var setupViewController): - setupViewController.cgmManagerCreateDelegate = self - setupViewController.cgmManagerOnboardDelegate = self + setupViewController.cgmManagerCreateDelegate = deviceManager + setupViewController.cgmManagerOnboardDelegate = deviceManager setupViewController.completionDelegate = self show(setupViewController, sender: self) case .createdAndOnboarded: @@ -1932,33 +1899,19 @@ extension StatusTableViewController { } } -extension StatusTableViewController: CGMManagerCreateDelegate { - func cgmManagerCreateNotifying(didCreateCGMManager cgmManager: CGMManagerUI) { - log.default("CGM manager with identifier '%{public}@' created", cgmManager.managerIdentifier) - deviceManager.cgmManager = cgmManager - } -} - -extension StatusTableViewController: CGMManagerOnboardDelegate { - func cgmManagerOnboardNotifying(didOnboardCGMManager cgmManager: CGMManagerUI) { - precondition(cgmManager.isOnboarded) - log.default("CGM manager with identifier '%{public}@' onboarded", cgmManager.managerIdentifier) - } -} - extension StatusTableViewController { fileprivate func addPumpManager(withIdentifier identifier: String) { let settings = PumpManagerSetupSettings(maxBasalRateUnitsPerHour: deviceManager.loopManager.settings.maximumBasalRatePerHour, maxBolusUnits: deviceManager.loopManager.settings.maximumBolus, basalSchedule: deviceManager.loopManager.basalRateSchedule) - switch setupPumpManagerUI(withIdentifier: identifier, initialSettings: settings) { + switch deviceManager.setupPumpManagerUI(withIdentifier: identifier, initialSettings: settings) { case .failure(let error): log.default("Failure to setup pump manager with identifier '%{public}@': %{public}@", identifier, String(describing: error)) case .success(let success): switch success { case .userInteractionRequired(var setupViewController): - setupViewController.pumpManagerCreateDelegate = self - setupViewController.pumpManagerOnboardDelegate = self + setupViewController.pumpManagerCreateDelegate = deviceManager + setupViewController.pumpManagerOnboardDelegate = deviceManager setupViewController.completionDelegate = self show(setupViewController, sender: self) case .createdAndOnboarded: @@ -1968,34 +1921,8 @@ extension StatusTableViewController { } } -extension StatusTableViewController: PumpManagerCreateDelegate { - func pumpManagerCreateNotifying(didCreatePumpManager pumpManager: PumpManagerUI) { - log.default("Pump manager with identifier '%{public}@' created", pumpManager.managerIdentifier) - deviceManager.pumpManager = pumpManager - } -} - -extension StatusTableViewController: PumpManagerOnboardDelegate { - func pumpManagerOnboardNotifying(didOnboardPumpManager pumpManager: PumpManagerUI, withFinalSettings settings: PumpManagerSetupSettings) { - precondition(pumpManager.isOnboarded) - log.default("Pump manager with identifier '%{public}@' onboarded", pumpManager.managerIdentifier) - - if let basalRateSchedule = settings.basalSchedule { - deviceManager.loopManager.basalRateSchedule = basalRateSchedule - } - if let maxBasalRateUnitsPerHour = settings.maxBasalRateUnitsPerHour { - deviceManager.loopManager.settings.maximumBasalRatePerHour = maxBasalRateUnitsPerHour - } - if let maxBolusUnits = settings.maxBolusUnits { - deviceManager.loopManager.settings.maximumBolus = maxBolusUnits - } - } -} - -extension StatusTableViewController: BluetoothStateManagerObserver { - func bluetoothStateManager(_ bluetoothStateManager: BluetoothStateManager, - bluetoothStateDidUpdate bluetoothState: BluetoothStateManager.BluetoothState) - { +extension StatusTableViewController: BluetoothObserver { + func bluetoothDidUpdateState(_ state: BluetoothState) { refreshContext.update(with: .status) reloadData(animated: true) } @@ -2052,14 +1979,14 @@ extension StatusTableViewController: SettingsViewModelDelegate { extension StatusTableViewController: ServicesViewModelDelegate { func addService(withIdentifier identifier: String) { - switch setupService(withIdentifier: identifier) { + switch deviceManager.servicesManager.setupService(withIdentifier: identifier) { case .failure(let error): log.default("Failure to setup service with identifier '%{public}@': %{public}@", identifier, String(describing: error)) case .success(let success): switch success { case .userInteractionRequired(var setupViewController): - setupViewController.serviceCreateDelegate = self - setupViewController.serviceOnboardDelegate = self + setupViewController.serviceCreateDelegate = deviceManager.servicesManager + setupViewController.serviceOnboardDelegate = deviceManager.servicesManager setupViewController.completionDelegate = self show(setupViewController, sender: self) case .createdAndOnboarded: @@ -2077,174 +2004,8 @@ extension StatusTableViewController: ServicesViewModelDelegate { fileprivate func showServiceSettings(_ serviceUI: ServiceUI) { var settingsViewController = serviceUI.settingsViewController(colorPalette: .default) - settingsViewController.serviceOnboardDelegate = self + settingsViewController.serviceOnboardDelegate = deviceManager.servicesManager settingsViewController.completionDelegate = self show(settingsViewController, sender: self) } } - -extension StatusTableViewController: ServiceCreateDelegate { - func serviceCreateNotifying(didCreateService service: Service) { - log.default("Service with identifier '%{public}@' created", service.serviceIdentifier) - deviceManager.servicesManager.addActiveService(service) - } -} - -extension StatusTableViewController: ServiceOnboardDelegate { - func serviceOnboardNotifying(didOnboardService service: Service) { - precondition(service.isOnboarded) - log.default("Service with identifier '%{public}@' onboarded", service.serviceIdentifier) - } -} - -// MARK: - Onboarding - -extension StatusTableViewController { - fileprivate func setupOnboarding(withIdentifier identifier: String) { - guard let onboardingUIType = deviceManager.pluginManager.getOnboardingTypeByIdentifier(identifier) else { - return - } - - let onboarding = onboardingUIType.createOnboarding() - var onboardingViewController = onboarding.onboardingViewController(cgmManagerProvider: self, - pumpManagerProvider: self, - serviceProvider: self, - displayGlucoseUnitObservable: deviceManager.displayGlucoseUnitObservable, - colorPalette: .default) - onboardingViewController.onboardingDelegate = self - onboardingViewController.cgmManagerCreateDelegate = self - onboardingViewController.cgmManagerOnboardDelegate = self - onboardingViewController.pumpManagerCreateDelegate = self - onboardingViewController.pumpManagerOnboardDelegate = self - onboardingViewController.serviceCreateDelegate = self - onboardingViewController.serviceOnboardDelegate = self - onboardingViewController.completionDelegate = self - - present(onboardingViewController, animated: true) - } -} - -struct UnknownIdentifierError: Error {} - -extension StatusTableViewController: CGMManagerProvider { - var activeCGMManager: CGMManager? { deviceManager.cgmManager } - - var availableCGMManagers: [CGMManagerDescriptor] { deviceManager.availableCGMManagers } - - func setupCGMManager(withIdentifier identifier: String) -> Swift.Result, Error> { - if let cgmManager = deviceManager.setupCGMManagerFromPumpManager(withIdentifier: identifier) { - return .success(.createdAndOnboarded(cgmManager)) - } - - switch setupCGMManagerUI(withIdentifier: identifier) { - case .failure(let error): - return .failure(error) - case .success(let success): - switch success { - case .userInteractionRequired(let viewController): - return .success(.userInteractionRequired(viewController)) - case .createdAndOnboarded(let cgmManagerUI): - return .success(.createdAndOnboarded(cgmManagerUI)) - } - } - } - - fileprivate func setupCGMManagerUI(withIdentifier identifier: String) -> Swift.Result, Error> { - guard let cgmManagerUIType = deviceManager.cgmManagerTypeByIdentifier(identifier) else { - return .failure(UnknownIdentifierError()) - } - - let result = cgmManagerUIType.setupViewController(colorPalette: .default) - if case .createdAndOnboarded(let cgmManagerUI) = result { - deviceManager.cgmManager = cgmManagerUI - } - - return .success(result) - } -} - -extension StatusTableViewController: PumpManagerProvider { - var activePumpManager: PumpManager? { deviceManager.pumpManager } - - var availablePumpManagers: [PumpManagerDescriptor] { deviceManager.availablePumpManagers } - - func setupPumpManager(withIdentifier identifier: String, initialSettings settings: PumpManagerSetupSettings) -> Swift.Result, Error> { - switch setupPumpManagerUI(withIdentifier: identifier, initialSettings: settings) { - case .failure(let error): - return .failure(error) - case .success(let success): - switch success { - case .userInteractionRequired(let viewController): - return .success(.userInteractionRequired(viewController)) - case .createdAndOnboarded(let pumpManagerUI): - return .success(.createdAndOnboarded(pumpManagerUI)) - } - } - } - - fileprivate func setupPumpManagerUI(withIdentifier identifier: String, initialSettings settings: PumpManagerSetupSettings) -> Swift.Result, Error> { - guard let pumpManagerUIType = deviceManager.pumpManagerTypeByIdentifier(identifier) else { - return .failure(UnknownIdentifierError()) - } - - let result = pumpManagerUIType.setupViewController(initialSettings: settings, colorPalette: .default) - if case .createdAndOnboarded(let pumpManagerUI) = result { - if let basalRateSchedule = deviceManager.loopManager.basalRateSchedule { - pumpManagerUI.syncBasalRateSchedule(items: basalRateSchedule.items, completion: { _ in }) - } - deviceManager.pumpManager = pumpManagerUI - } - - return .success(result) - } -} - -extension StatusTableViewController: ServiceProvider { - var activeServices: [Service] { deviceManager.servicesManager.activeServices } - - var availableServices: [ServiceDescriptor] { deviceManager.servicesManager.availableServices } - - func setupService(withIdentifier identifier: String) -> Swift.Result, Error> { - switch setupServiceUI(withIdentifier: identifier) { - case .failure(let error): - return .failure(error) - case .success(let success): - switch success { - case .userInteractionRequired(let viewController): - return .success(.userInteractionRequired(viewController)) - case .createdAndOnboarded(let serviceUI): - return .success(.createdAndOnboarded(serviceUI)) - } - } - } - - fileprivate func setupServiceUI(withIdentifier identifier: String) -> Swift.Result, Error> { - guard let serviceUIType = deviceManager.servicesManager.serviceUITypeByIdentifier(identifier) else { - return .failure(UnknownIdentifierError()) - } - - let result = serviceUIType.setupViewController(colorPalette: .default) - if case .createdAndOnboarded(let serviceUI) = result { - deviceManager.servicesManager.addActiveService(serviceUI) - } - - return .success(result) - } -} - -extension StatusTableViewController: OnboardingDelegate { - func onboardingNotifying(hasNewTherapySettings therapySettings: TherapySettings) { - log.default("Onboarding has new therapy settings") - - deviceManager.loopManager.settings.glucoseTargetRangeSchedule = therapySettings.glucoseTargetRangeSchedule - deviceManager.loopManager.settings.preMealTargetRange = therapySettings.preMealTargetRange - deviceManager.loopManager.settings.legacyWorkoutTargetRange = therapySettings.workoutTargetRange - deviceManager.loopManager.settings.suspendThreshold = therapySettings.suspendThreshold - deviceManager.loopManager.settings.maximumBolus = therapySettings.maximumBolus - deviceManager.loopManager.settings.maximumBasalRatePerHour = therapySettings.maximumBasalRatePerHour - deviceManager.loopManager.insulinSensitivitySchedule = therapySettings.insulinSensitivitySchedule - deviceManager.loopManager.carbRatioSchedule = therapySettings.carbRatioSchedule - deviceManager.loopManager.basalRateSchedule = therapySettings.basalRateSchedule - deviceManager.loopManager.insulinModelSettings = therapySettings.insulinModelSettings - } -} diff --git a/LoopTests/LoopSettingsAlerterTests.swift b/LoopTests/LoopSettingsAlerterTests.swift index c24917beeb..a56516f8d3 100644 --- a/LoopTests/LoopSettingsAlerterTests.swift +++ b/LoopTests/LoopSettingsAlerterTests.swift @@ -35,7 +35,7 @@ class LoopSettingsAlerterTests: XCTestCase, LoopSettingsAlerterDelegate { func testWorkoutOverrideReminderElasped() { testExpectation = self.expectation(description: #function) - let loopSettingsAlerter = LoopSettingsAlerter(alertPresenter: self, workoutOverrideReminderInterval: -.seconds(1)) // the elasped time will always be greater than a negative number + let loopSettingsAlerter = LoopSettingsAlerter(alertIssuer: self, workoutOverrideReminderInterval: -.seconds(1)) // the elasped time will always be greater than a negative number loopSettingsAlerter.delegate = self NotificationCenter.default.post(name: .LoopRunning, object: nil) @@ -47,7 +47,7 @@ class LoopSettingsAlerterTests: XCTestCase, LoopSettingsAlerterDelegate { func testWorkoutOverrideReminderRepeated() { testExpectation = self.expectation(description: #function) - let loopSettingsAlerter = LoopSettingsAlerter(alertPresenter: self, workoutOverrideReminderInterval: -.seconds(1)) // the elasped time will always be greater than a negative number + let loopSettingsAlerter = LoopSettingsAlerter(alertIssuer: self, workoutOverrideReminderInterval: -.seconds(1)) // the elasped time will always be greater than a negative number loopSettingsAlerter.delegate = self NotificationCenter.default.post(name: .LoopRunning, object: nil) @@ -65,7 +65,7 @@ class LoopSettingsAlerterTests: XCTestCase, LoopSettingsAlerterDelegate { } func testWorkoutOverrideReminderNotElasped() { - let loopSettingsAlerter = LoopSettingsAlerter(alertPresenter: self) + let loopSettingsAlerter = LoopSettingsAlerter(alertIssuer: self) loopSettingsAlerter.delegate = self NotificationCenter.default.post(name: .LoopRunning, object: nil) @@ -75,7 +75,7 @@ class LoopSettingsAlerterTests: XCTestCase, LoopSettingsAlerterDelegate { } } -extension LoopSettingsAlerterTests: AlertPresenter { +extension LoopSettingsAlerterTests: AlertIssuer { func issueAlert(_ alert: Alert) { self.alert = alert testExpectation.fulfill() diff --git a/LoopTests/Managers/Alerts/AlertManagerTests.swift b/LoopTests/Managers/Alerts/AlertManagerTests.swift index 5ed91d64aa..4b6865bd83 100644 --- a/LoopTests/Managers/Alerts/AlertManagerTests.swift +++ b/LoopTests/Managers/Alerts/AlertManagerTests.swift @@ -13,7 +13,7 @@ import XCTest class AlertManagerTests: XCTestCase { - class MockPresenter: AlertPresenter { + class MockIssuer: AlertIssuer { var issuedAlert: Alert? func issueAlert(_ alert: Alert) { issuedAlert = alert @@ -63,6 +63,11 @@ class AlertManagerTests: XCTestCase { } } + class MockPresenter: AlertPresenter { + func present(_ viewControllerToPresent: UIViewController, animated: Bool, completion: (() -> Void)?) { completion?() } + func dismiss(animated: Bool, completion: (() -> Void)?) { completion?() } + } + class MockSoundVendor: AlertSoundVendor { func getSoundBaseURL() -> URL? { // Hm. It's not easy to make a "fake" URL, so we'll use this one: @@ -113,6 +118,7 @@ class AlertManagerTests: XCTestCase { var mockFileManager: MockFileManager! var mockPresenter: MockPresenter! + var mockIssuer: MockIssuer! var mockUserNotificationCenter: MockUserNotificationCenter! var mockAlertStore: MockAlertStore! var alertManager: AlertManager! @@ -126,10 +132,11 @@ class AlertManagerTests: XCTestCase { override func setUp() { mockFileManager = MockFileManager() mockPresenter = MockPresenter() + mockIssuer = MockIssuer() mockUserNotificationCenter = MockUserNotificationCenter() mockAlertStore = MockAlertStore() - alertManager = AlertManager(rootViewController: UIViewController(), - handlers: [mockPresenter], + alertManager = AlertManager(alertPresenter: mockPresenter, + handlers: [mockIssuer], userNotificationCenter: mockUserNotificationCenter, fileManager: mockFileManager, alertStore: mockAlertStore) @@ -141,14 +148,14 @@ class AlertManagerTests: XCTestCase { func testIssueAlertOnHandlerCalled() { alertManager.issueAlert(mockAlert) - XCTAssertEqual(mockAlert.identifier, mockPresenter.issuedAlert?.identifier) - XCTAssertNil(mockPresenter.retractedAlertIdentifier) + XCTAssertEqual(mockAlert.identifier, mockIssuer.issuedAlert?.identifier) + XCTAssertNil(mockIssuer.retractedAlertIdentifier) } func testRetractAlertOnHandlerCalled() { alertManager.retractAlert(identifier: mockAlert.identifier) - XCTAssertNil(mockPresenter.issuedAlert) - XCTAssertEqual(mockAlert.identifier, mockPresenter.retractedAlertIdentifier) + XCTAssertNil(mockIssuer.issuedAlert) + XCTAssertEqual(mockAlert.identifier, mockIssuer.retractedAlertIdentifier) } func testAlertResponderAcknowledged() { @@ -200,12 +207,12 @@ class AlertManagerTests: XCTestCase { foregroundContent: content, backgroundContent: content, trigger: .immediate) mockAlertStore.storedAlerts = [StoredAlert(from: alert, context: mockAlertStore.managedObjectContext)] - alertManager = AlertManager(rootViewController: UIViewController(), - handlers: [mockPresenter], + alertManager = AlertManager(alertPresenter: mockPresenter, + handlers: [mockIssuer], userNotificationCenter: mockUserNotificationCenter, fileManager: mockFileManager, alertStore: mockAlertStore) - XCTAssertEqual(alert, mockPresenter.issuedAlert) + XCTAssertEqual(alert, mockIssuer.issuedAlert) } } @@ -218,13 +225,13 @@ class AlertManagerTests: XCTestCase { let storedAlert = StoredAlert(from: alert, context: mockAlertStore.managedObjectContext) storedAlert.issuedDate = date mockAlertStore.storedAlerts = [storedAlert] - alertManager = AlertManager(rootViewController: UIViewController(), - handlers: [mockPresenter], + alertManager = AlertManager(alertPresenter: mockPresenter, + handlers: [mockIssuer], userNotificationCenter: mockUserNotificationCenter, fileManager: mockFileManager, alertStore: mockAlertStore) let expected = Alert(identifier: Self.mockIdentifier, foregroundContent: content, backgroundContent: content, trigger: .immediate) - XCTAssertEqual(expected, mockPresenter.issuedAlert) + XCTAssertEqual(expected, mockIssuer.issuedAlert) } } @@ -237,8 +244,8 @@ class AlertManagerTests: XCTestCase { let storedAlert = StoredAlert(from: alert, context: mockAlertStore.managedObjectContext) storedAlert.issuedDate = date mockAlertStore.storedAlerts = [storedAlert] - alertManager = AlertManager(rootViewController: UIViewController(), - handlers: [mockPresenter], + alertManager = AlertManager(alertPresenter: mockPresenter, + handlers: [mockIssuer], userNotificationCenter: mockUserNotificationCenter, fileManager: mockFileManager, alertStore: mockAlertStore) @@ -246,12 +253,12 @@ class AlertManagerTests: XCTestCase { // The trigger for this should be `.delayed` by "something less than 15 seconds", // but the exact value depends on the speed of executing this test. // As long as it is <= 15 seconds, we call it good. - XCTAssertNotNil(mockPresenter.issuedAlert) - switch mockPresenter.issuedAlert?.trigger { + XCTAssertNotNil(mockIssuer.issuedAlert) + switch mockIssuer.issuedAlert?.trigger { case .some(.delayed(let interval)): XCTAssertLessThanOrEqual(interval, 15.0) default: - XCTFail("Wrong trigger \(String(describing: mockPresenter.issuedAlert?.trigger))") + XCTFail("Wrong trigger \(String(describing: mockIssuer.issuedAlert?.trigger))") } } } @@ -265,13 +272,13 @@ class AlertManagerTests: XCTestCase { let storedAlert = StoredAlert(from: alert, context: mockAlertStore.managedObjectContext) storedAlert.issuedDate = date mockAlertStore.storedAlerts = [storedAlert] - alertManager = AlertManager(rootViewController: UIViewController(), - handlers: [mockPresenter], + alertManager = AlertManager(alertPresenter: mockPresenter, + handlers: [mockIssuer], userNotificationCenter: mockUserNotificationCenter, fileManager: mockFileManager, alertStore: mockAlertStore) - XCTAssertEqual(alert, mockPresenter.issuedAlert) + XCTAssertEqual(alert, mockIssuer.issuedAlert) } } } diff --git a/LoopTests/Managers/Alerts/InAppModalAlertPresenterTests.swift b/LoopTests/Managers/Alerts/InAppModalAlertIssuerTests.swift similarity index 85% rename from LoopTests/Managers/Alerts/InAppModalAlertPresenterTests.swift rename to LoopTests/Managers/Alerts/InAppModalAlertIssuerTests.swift index d236075775..d4e8874fa7 100644 --- a/LoopTests/Managers/Alerts/InAppModalAlertPresenterTests.swift +++ b/LoopTests/Managers/Alerts/InAppModalAlertIssuerTests.swift @@ -1,5 +1,5 @@ // -// InAppModalAlertPresenterTests.swift +// InAppModalAlertIssuerTests.swift // LoopTests // // Created by Rick Pasetto on 4/15/20. @@ -10,7 +10,7 @@ import LoopKit import XCTest @testable import Loop -class InAppModalAlertPresenterTests: XCTestCase { +class InAppModalAlertIssuerTests: XCTestCase { class MockAlertAction: UIAlertAction { typealias Handler = ((UIAlertAction) -> Void) @@ -40,7 +40,7 @@ class InAppModalAlertPresenterTests: XCTestCase { } } - class MockViewController: UIViewController { + class MockViewController: UIViewController, AlertPresenter { var viewControllerPresented: UIViewController? var autoComplete = true var completion: (() -> Void)? @@ -56,7 +56,7 @@ class InAppModalAlertPresenterTests: XCTestCase { completion?() } } - + class MockSoundPlayer: AlertSoundPlayer { var vibrateCalled = false func vibrate() { @@ -83,31 +83,30 @@ class InAppModalAlertPresenterTests: XCTestCase { var mockAlertManagerResponder: MockAlertManagerResponder! var mockViewController: MockViewController! var mockSoundPlayer: MockSoundPlayer! - var inAppModalAlertPresenter: InAppModalAlertPresenter! + var inAppModalAlertIssuer: InAppModalAlertIssuer! override func setUp() { mockAlertManagerResponder = MockAlertManagerResponder() mockViewController = MockViewController() mockSoundPlayer = MockSoundPlayer() - let newTimerFunc: InAppModalAlertPresenter.TimerFactoryFunction = { timeInterval, repeats, block in + let newTimerFunc: InAppModalAlertIssuer.TimerFactoryFunction = { timeInterval, repeats, block in let timer = Timer(timeInterval: timeInterval, repeats: repeats) { _ in block?() } self.mockTimer = timer self.mockTimerTimeInterval = timeInterval self.mockTimerRepeats = repeats return timer } - inAppModalAlertPresenter = - InAppModalAlertPresenter(rootViewController: mockViewController, - alertManagerResponder: mockAlertManagerResponder, - soundPlayer: mockSoundPlayer, - newActionFunc: MockAlertAction.init, - newTimerFunc: newTimerFunc) + inAppModalAlertIssuer = InAppModalAlertIssuer(alertPresenter: mockViewController, + alertManagerResponder: mockAlertManagerResponder, + soundPlayer: mockSoundPlayer, + newActionFunc: MockAlertAction.init, + newTimerFunc: newTimerFunc) } func testIssueImmediateAlert() { let alert = Alert(identifier: alertIdentifier, foregroundContent: foregroundContent, backgroundContent: backgroundContent, trigger: .immediate) - inAppModalAlertPresenter.issueAlert(alert) + inAppModalAlertIssuer.issueAlert(alert) waitOnMain() let alertController = mockViewController.viewControllerPresented as? UIAlertController @@ -124,13 +123,13 @@ class InAppModalAlertPresenterTests: XCTestCase { backgroundContent: backgroundContent, trigger: .immediate, sound: .sound(name: soundName)) - inAppModalAlertPresenter.issueAlert(alert) + inAppModalAlertIssuer.issueAlert(alert) waitOnMain() let alertController = mockViewController.viewControllerPresented as? UIAlertController XCTAssertNotNil(alertController) XCTAssertEqual("FOREGROUND", alertController?.title) - XCTAssertEqual("\(InAppModalAlertPresenterTests.managerIdentifier)-\(soundName)", mockSoundPlayer.urlPlayed?.lastPathComponent) + XCTAssertEqual("\(InAppModalAlertIssuerTests.managerIdentifier)-\(soundName)", mockSoundPlayer.urlPlayed?.lastPathComponent) XCTAssertTrue(mockSoundPlayer.vibrateCalled) } @@ -140,7 +139,7 @@ class InAppModalAlertPresenterTests: XCTestCase { backgroundContent: backgroundContent, trigger: .immediate, sound: .vibrate) - inAppModalAlertPresenter.issueAlert(alert) + inAppModalAlertIssuer.issueAlert(alert) waitOnMain() let alertController = mockViewController.viewControllerPresented as? UIAlertController @@ -156,7 +155,7 @@ class InAppModalAlertPresenterTests: XCTestCase { backgroundContent: backgroundContent, trigger: .immediate, sound: .silence) - inAppModalAlertPresenter.issueAlert(alert) + inAppModalAlertIssuer.issueAlert(alert) waitOnMain() let alertController = mockViewController.viewControllerPresented as? UIAlertController @@ -168,11 +167,11 @@ class InAppModalAlertPresenterTests: XCTestCase { func testRemoveImmediateAlert() { let alert = Alert(identifier: alertIdentifier, foregroundContent: foregroundContent, backgroundContent: backgroundContent, trigger: .immediate) - inAppModalAlertPresenter.issueAlert(alert) + inAppModalAlertIssuer.issueAlert(alert) waitOnMain() var dismissed = false - inAppModalAlertPresenter.removeDeliveredAlert(identifier: alert.identifier) { + inAppModalAlertIssuer.removeDeliveredAlert(identifier: alert.identifier) { dismissed = true } @@ -186,17 +185,17 @@ class InAppModalAlertPresenterTests: XCTestCase { let alert = Alert(identifier: alertIdentifier, foregroundContent: foregroundContent, backgroundContent: backgroundContent, trigger: .immediate) mockViewController.autoComplete = false - inAppModalAlertPresenter.issueAlert(alert) + inAppModalAlertIssuer.issueAlert(alert) waitOnMain() mockViewController.viewControllerPresented = nil - inAppModalAlertPresenter.issueAlert(alert) + inAppModalAlertIssuer.issueAlert(alert) XCTAssertNil(mockViewController.viewControllerPresented) } func testIssueImmediateAlertWithoutForegroundContentDoesNothing() { let alert = Alert(identifier: alertIdentifier, foregroundContent: nil, backgroundContent: backgroundContent, trigger: .immediate) - inAppModalAlertPresenter.issueAlert(alert) + inAppModalAlertIssuer.issueAlert(alert) waitOnMain() XCTAssertNil(mockViewController.viewControllerPresented) @@ -204,7 +203,7 @@ class InAppModalAlertPresenterTests: XCTestCase { func testIssueImmediateAlertAcknowledgement() { let alert = Alert(identifier: alertIdentifier, foregroundContent: foregroundContent, backgroundContent: backgroundContent, trigger: .immediate) - inAppModalAlertPresenter.issueAlert(alert) + inAppModalAlertIssuer.issueAlert(alert) waitOnMain() let action = (mockViewController.viewControllerPresented as? UIAlertController)?.actions[0] as? MockAlertAction XCTAssertNotNil(action) @@ -216,7 +215,7 @@ class InAppModalAlertPresenterTests: XCTestCase { func testIssueDelayedAlert() { let alert = Alert(identifier: alertIdentifier, foregroundContent: foregroundContent, backgroundContent: backgroundContent, trigger: .delayed(interval: 0.1)) mockViewController.autoComplete = false - inAppModalAlertPresenter.issueAlert(alert) + inAppModalAlertIssuer.issueAlert(alert) waitOnMain() // Timer should be created but won't fire yet @@ -235,13 +234,13 @@ class InAppModalAlertPresenterTests: XCTestCase { func testIssueDelayedAlertTwiceOnlyOneWorks() { let alert = Alert(identifier: alertIdentifier, foregroundContent: foregroundContent, backgroundContent: backgroundContent, trigger: .delayed(interval: 0.1)) mockViewController.autoComplete = false - inAppModalAlertPresenter.issueAlert(alert) + inAppModalAlertIssuer.issueAlert(alert) waitOnMain() guard let firstTimer = mockTimer else { XCTFail(); return } mockTimer = nil // This should not schedule another timer - inAppModalAlertPresenter.issueAlert(alert) + inAppModalAlertIssuer.issueAlert(alert) waitOnMain() XCTAssertNil(mockTimer) @@ -255,7 +254,7 @@ class InAppModalAlertPresenterTests: XCTestCase { func testIssueDelayedAlertWithoutForegroundContentDoesNothing() { let alert = Alert(identifier: alertIdentifier, foregroundContent: nil, backgroundContent: backgroundContent, trigger: .delayed(interval: 0.1)) - inAppModalAlertPresenter.issueAlert(alert) + inAppModalAlertIssuer.issueAlert(alert) waitOnMain() XCTAssertNil(mockViewController.viewControllerPresented) @@ -263,11 +262,11 @@ class InAppModalAlertPresenterTests: XCTestCase { func testRetractAlert() { let alert = Alert(identifier: alertIdentifier, foregroundContent: foregroundContent, backgroundContent: backgroundContent, trigger: .delayed(interval: 0.1)) - inAppModalAlertPresenter.issueAlert(alert) + inAppModalAlertIssuer.issueAlert(alert) waitOnMain() XCTAssert(mockTimer?.isValid == true) - inAppModalAlertPresenter.retractAlert(identifier: alert.identifier) + inAppModalAlertIssuer.retractAlert(identifier: alert.identifier) waitOnMain() XCTAssert(mockTimer?.isValid == false) @@ -276,7 +275,7 @@ class InAppModalAlertPresenterTests: XCTestCase { func testIssueRepeatingAlert() { let alert = Alert(identifier: alertIdentifier, foregroundContent: foregroundContent, backgroundContent: backgroundContent, trigger: .repeating(repeatInterval: 0.1)) mockViewController.autoComplete = false - inAppModalAlertPresenter.issueAlert(alert) + inAppModalAlertIssuer.issueAlert(alert) waitOnMain() // Timer should be created but won't fire yet diff --git a/LoopTests/Managers/Alerts/UserNotificationAlertPresenterTests.swift b/LoopTests/Managers/Alerts/UserNotificationAlertIssuerTests.swift similarity index 87% rename from LoopTests/Managers/Alerts/UserNotificationAlertPresenterTests.swift rename to LoopTests/Managers/Alerts/UserNotificationAlertIssuerTests.swift index 061f1ae36b..6fdf1d3ee0 100644 --- a/LoopTests/Managers/Alerts/UserNotificationAlertPresenterTests.swift +++ b/LoopTests/Managers/Alerts/UserNotificationAlertIssuerTests.swift @@ -1,5 +1,5 @@ // -// UserNotificationAlertPresenterTests.swift +// UserNotificationAlertIssuerTests.swift // LoopTests // // Created by Rick Pasetto on 4/15/20. @@ -10,9 +10,9 @@ import LoopKit import XCTest @testable import Loop -class UserNotificationAlertPresenterTests: XCTestCase { +class UserNotificationAlertIssuerTests: XCTestCase { - var userNotificationAlertPresenter: UserNotificationAlertPresenter! + var userNotificationAlertIssuer: UserNotificationAlertIssuer! let alertIdentifier = Alert.Identifier(managerIdentifier: "foo", alertIdentifier: "bar") let foregroundContent = Alert.Content(title: "FOREGROUND", body: "foreground", acknowledgeActionButtonLabel: "") @@ -22,13 +22,13 @@ class UserNotificationAlertPresenterTests: XCTestCase { override func setUp() { mockUserNotificationCenter = MockUserNotificationCenter() - userNotificationAlertPresenter = - UserNotificationAlertPresenter(userNotificationCenter: mockUserNotificationCenter) + userNotificationAlertIssuer = + UserNotificationAlertIssuer(userNotificationCenter: mockUserNotificationCenter) } func testIssueImmediateAlert() { let alert = Alert(identifier: alertIdentifier, foregroundContent: foregroundContent, backgroundContent: backgroundContent, trigger: .immediate) - userNotificationAlertPresenter.issueAlert(alert, timestamp: Date.distantPast) + userNotificationAlertIssuer.issueAlert(alert, timestamp: Date.distantPast) waitOnMain() @@ -49,7 +49,7 @@ class UserNotificationAlertPresenterTests: XCTestCase { func testIssueImmediateCriticalAlert() { let backgroundContent = Alert.Content(title: "BACKGROUND", body: "background", acknowledgeActionButtonLabel: "", isCritical: true) let alert = Alert(identifier: alertIdentifier, foregroundContent: foregroundContent, backgroundContent: backgroundContent, trigger: .immediate) - userNotificationAlertPresenter.issueAlert(alert, timestamp: Date.distantPast) + userNotificationAlertIssuer.issueAlert(alert, timestamp: Date.distantPast) waitOnMain() @@ -69,7 +69,7 @@ class UserNotificationAlertPresenterTests: XCTestCase { func testIssueDelayedAlert() { let alert = Alert(identifier: alertIdentifier, foregroundContent: foregroundContent, backgroundContent: backgroundContent, trigger: .delayed(interval: 0.1)) - userNotificationAlertPresenter.issueAlert(alert, timestamp: Date.distantPast) + userNotificationAlertIssuer.issueAlert(alert, timestamp: Date.distantPast) waitOnMain() @@ -90,7 +90,7 @@ class UserNotificationAlertPresenterTests: XCTestCase { func testIssueRepeatingAlert() { let alert = Alert(identifier: alertIdentifier, foregroundContent: foregroundContent, backgroundContent: backgroundContent, trigger: .repeating(repeatInterval: 100)) - userNotificationAlertPresenter.issueAlert(alert, timestamp: Date.distantPast) + userNotificationAlertIssuer.issueAlert(alert, timestamp: Date.distantPast) waitOnMain() @@ -111,12 +111,12 @@ class UserNotificationAlertPresenterTests: XCTestCase { func testRetractAlert() { let alert = Alert(identifier: alertIdentifier, foregroundContent: foregroundContent, backgroundContent: backgroundContent, trigger: .immediate) - userNotificationAlertPresenter.issueAlert(alert) + userNotificationAlertIssuer.issueAlert(alert) waitOnMain() mockUserNotificationCenter.deliverAll() - userNotificationAlertPresenter.retractAlert(identifier: alert.identifier) + userNotificationAlertIssuer.retractAlert(identifier: alert.identifier) waitOnMain() XCTAssertTrue(mockUserNotificationCenter.pendingRequests.isEmpty) @@ -126,7 +126,7 @@ class UserNotificationAlertPresenterTests: XCTestCase { func testDoesNotShowIfNoBackgroundContent() { let alert = Alert(identifier: alertIdentifier, foregroundContent: foregroundContent, backgroundContent: nil, trigger: .immediate) - userNotificationAlertPresenter.issueAlert(alert) + userNotificationAlertIssuer.issueAlert(alert) waitOnMain()