From dae9b622a28b563db0cb2c06191fcb57bfcb57c2 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 21 Aug 2023 18:05:46 -0700 Subject: [PATCH 01/19] Overwrite profile_expire_date for GitHub built --- Scripts/capture-build-details.sh | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index 66f827d7c3..de8a438b96 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -89,5 +89,14 @@ then if [ -n "$branch" ]; then plutil -replace com-loopkit-LoopWorkspace-git-branch -string "${branch}" "${info_plist_path}" fi + # determine if this is a GitHub Action build (with 90 day expiration) + folderName=$(pwd) + runnerString="/Users/runner" + if [ "${folderName:0:13}" == "$runnerString" ]; then + # overwrite profile_expire_date + profile_expire_date=$(date -j -v+90d +"%Y-%m-%dT%H:%M:%SZ") + echo "runnerString detected, update profile_expire_date to ${profile_expire_date}" + plutil -replace com-loopkit-Loop-profile-expiration -date "${profile_expire_date}" "${info_plist_path}" + fi popd . > /dev/null fi From c68d2cc2f7521678b18bc3cb17a9f53aa5f36442 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Tue, 22 Aug 2023 15:48:22 -0700 Subject: [PATCH 02/19] use GITHUB_ACTION to update profile expire date --- Scripts/capture-build-details.sh | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index de8a438b96..ea7b1f27f8 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -72,6 +72,17 @@ if [ -e "${provisioning_profile_path}" ]; then profile_expire_date=$(security cms -D -i "${provisioning_profile_path}" | plutil -p - | grep ExpirationDate | cut -b 23-) # Convert to plutil format profile_expire_date=$(date -j -f "%Y-%m-%d %H:%M:%S" "${profile_expire_date}" +"%Y-%m-%dT%H:%M:%SZ") + # Handle github action, testflight builds that expire <= 90 days + if [ -n "$GITHUB_ACTIONS" ]; then + github_expire_date=$(date -j -v+90d +"%Y-%m-%dT%H:%M:%SZ") + + if [ "$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "${github_expire_date}" +%s)" -lt "$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "${profile_expire_date}" +%s)" ]; then + profile_expire_date=$github_expire_date + else + echo "GitHub Actions detected, expiration date is not more than 90 days in the future." + fi + fi + plutil -replace com-loopkit-Loop-profile-expiration -date "${profile_expire_date}" "${info_plist_path}" else warn "Invalid provisioning profile path ${provisioning_profile_path}" @@ -89,14 +100,5 @@ then if [ -n "$branch" ]; then plutil -replace com-loopkit-LoopWorkspace-git-branch -string "${branch}" "${info_plist_path}" fi - # determine if this is a GitHub Action build (with 90 day expiration) - folderName=$(pwd) - runnerString="/Users/runner" - if [ "${folderName:0:13}" == "$runnerString" ]; then - # overwrite profile_expire_date - profile_expire_date=$(date -j -v+90d +"%Y-%m-%dT%H:%M:%SZ") - echo "runnerString detected, update profile_expire_date to ${profile_expire_date}" - plutil -replace com-loopkit-Loop-profile-expiration -date "${profile_expire_date}" "${info_plist_path}" - fi popd . > /dev/null fi From 2b58a68e81e71986771be7273d3242182a1d85b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Sun, 27 Aug 2023 21:16:31 +0200 Subject: [PATCH 03/19] Detection of github build, calculate date for testflight expire --- Common/Models/BuildDetails.swift | 4 + Loop.xcodeproj/project.pbxproj | 8 +- Loop/Managers/AppExpirationAlerter.swift | 134 +++++++++++++++++++ Loop/Managers/LoopAppManager.swift | 2 +- Loop/Managers/ProfileExpirationAlerter.swift | 86 ------------ Loop/Views/SettingsView.swift | 55 +++++--- Scripts/capture-build-details.sh | 15 +-- 7 files changed, 187 insertions(+), 117 deletions(-) create mode 100644 Loop/Managers/AppExpirationAlerter.swift delete mode 100644 Loop/Managers/ProfileExpirationAlerter.swift diff --git a/Common/Models/BuildDetails.swift b/Common/Models/BuildDetails.swift index 63517e7e79..0badb257ac 100644 --- a/Common/Models/BuildDetails.swift +++ b/Common/Models/BuildDetails.swift @@ -65,5 +65,9 @@ class BuildDetails { var workspaceGitBranch: String? { return dict["com-loopkit-LoopWorkspace-git-branch"] as? String } + + var isGitHubBuild: Bool? { + return dict["com-loopkit-GitHub-build"] as? Bool + } } diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index aaa0a470ac..ecba1c2d01 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -479,7 +479,7 @@ C1EF747228D6A44A00C8C083 /* CrashRecoveryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1EF747128D6A44A00C8C083 /* CrashRecoveryManager.swift */; }; C1F00C60285A802A006302C5 /* SwiftCharts in Frameworks */ = {isa = PBXBuildFile; productRef = C1F00C5F285A802A006302C5 /* SwiftCharts */; }; C1F00C78285A8256006302C5 /* SwiftCharts in Embed Frameworks */ = {isa = PBXBuildFile; productRef = C1F00C5F285A802A006302C5 /* SwiftCharts */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - C1F2075C26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */; }; + C1F2075C26D6F9B0007AB7EB /* AppExpirationAlerter.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */; }; C1F7822627CC056900C0919A /* SettingsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F7822527CC056900C0919A /* SettingsManager.swift */; }; C1F8B243223E73FD00DD66CF /* BolusProgressTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F8B1D122375E4200DD66CF /* BolusProgressTableViewCell.swift */; }; C1FB428C217806A400FAB378 /* StateColorPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1FB428B217806A300FAB378 /* StateColorPalette.swift */; }; @@ -1565,7 +1565,7 @@ C1EB0D22299581D900628475 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/ckcomplication.strings; sourceTree = ""; }; C1EE9E802A38D0FB0064784A /* BuildDetails.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = BuildDetails.plist; sourceTree = ""; }; C1EF747128D6A44A00C8C083 /* CrashRecoveryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashRecoveryManager.swift; sourceTree = ""; }; - C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileExpirationAlerter.swift; sourceTree = ""; }; + C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExpirationAlerter.swift; sourceTree = ""; }; C1F48FF62995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; C1F48FF72995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; C1F48FF82995821600C8BD69 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; @@ -2307,7 +2307,7 @@ 1DA6499D2441266400F61E75 /* Alerts */, E95D37FF24EADE68005E2F50 /* Store Protocols */, E9B355232935906B0076AB04 /* Missed Meal Detection */, - C1F2075B26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift */, + C1F2075B26D6F9B0007AB7EB /* AppExpirationAlerter.swift */, A96DAC2B2838F31200D94E38 /* SharedLogging.swift */, 7E69CFFB2A16A77E00203CBD /* ResetLoopManager.swift */, 84AA81E42A4A3981000B658B /* DeeplinkManager.swift */, @@ -3654,7 +3654,7 @@ C1D289B522F90A52003FFBD9 /* BasalDeliveryState.swift in Sources */, 4F2C15821E074FC600E160D4 /* NSTimeInterval.swift in Sources */, 4311FB9B1F37FE1B00D4C0A7 /* TitleSubtitleTextFieldTableViewCell.swift in Sources */, - C1F2075C26D6F9B0007AB7EB /* ProfileExpirationAlerter.swift in Sources */, + C1F2075C26D6F9B0007AB7EB /* AppExpirationAlerter.swift in Sources */, B4FEEF7D24B8A71F00A8DF9B /* DeviceDataManager+DeviceStatus.swift in Sources */, 142CB7592A60BF2E0075748A /* EditMode.swift in Sources */, E95D380324EADF36005E2F50 /* CarbStoreProtocol.swift in Sources */, diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift new file mode 100644 index 0000000000..23ceb1e7a3 --- /dev/null +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -0,0 +1,134 @@ +// +// AppExpirationAlerter.swift +// Loop +// +// Created by Pete Schwamb on 8/21/21. +// Copyright © 2021 LoopKit Authors. All rights reserved. +// + +import Foundation +import UserNotifications +import LoopCore + + +class AppExpirationAlerter { + + static let expirationAlertWindow: TimeInterval = .days(20) + static let settingsPageExpirationWarningModeWindow: TimeInterval = .days(3) + + static func alertIfNeeded(viewControllerToPresentFrom: UIViewController) { + + let now = Date() + + guard let profileExpiration = BuildDetails.default.profileExpiration, now > profileExpiration - expirationAlertWindow else { + return + } + + let expirationDate = calculateExpirationDate(profileExpiration: profileExpiration) + + let timeUntilExpiration = expirationDate.timeIntervalSince(now) + + let minimumTimeBetweenAlerts: TimeInterval = timeUntilExpiration > .hours(24) ? .days(2) : .hours(1) + + if let lastAlertDate = UserDefaults.appGroup?.lastProfileExpirationAlertDate { + guard now > lastAlertDate + minimumTimeBetweenAlerts else { + return + } + } + + let formatter = DateComponentsFormatter() + formatter.allowedUnits = [.day, .hour] + formatter.unitsStyle = .full + formatter.zeroFormattingBehavior = .dropLeading + formatter.maximumUnitCount = 1 + let timeUntilExpirationStr = formatter.string(from: timeUntilExpiration) + + let alertMessage = createVerboseAlertMessage(timeUntilExpirationStr: timeUntilExpirationStr!) + + var dialog: UIAlertController + if isTestFlightBuild() { + dialog = UIAlertController( + title: NSLocalizedString("TestFlight Expires Soon", comment: "The title for notification of upcoming TestFlight expiration"), + message: alertMessage, + preferredStyle: .alert) + dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil)) + dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming TestFlight expiration"), style: .default, handler: { (_) in + UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/")!) + })) + + } else { + dialog = UIAlertController( + title: NSLocalizedString("Profile Expires Soon", comment: "The title for notification of upcoming profile expiration"), + message: alertMessage, + preferredStyle: .alert) + dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil)) + dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming profile expiration"), style: .default, handler: { (_) in + UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) + })) + } + viewControllerToPresentFrom.present(dialog, animated: true, completion: nil) + + UserDefaults.appGroup?.lastProfileExpirationAlertDate = now + } + + static func createVerboseAlertMessage(timeUntilExpirationStr:String) -> String { + if isTestFlightBuild() { + return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to update before that, with a new provisioning profile.", comment: "Format string for body for notification of upcoming provisioning profile expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) + } else { + return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to rebuild before that.", comment: "Format string for body for notification of upcoming provisioning profile expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) + } + } + + static func isNearExpiration(expirationDate:Date) -> Bool { + return expirationDate.timeIntervalSinceNow < settingsPageExpirationWarningModeWindow + } + + static func createProfileExpirationSettingsMessage(expirationDate:Date) -> String { + let nearExpiration = isNearExpiration(expirationDate: expirationDate) + let maxUnitCount = nearExpiration ? 2 : 1 // only include hours in the msg if near expiration + let readableRelativeTime: String? = relativeTimeFormatter(maxUnitCount: maxUnitCount).string(from: expirationDate.timeIntervalSinceNow) + let relativeTimeRemaining: String = readableRelativeTime ?? NSLocalizedString("Unknown time", comment: "Unknown amount of time in settings' profile expiration section") + let verboseMessage = createVerboseAlertMessage(timeUntilExpirationStr: relativeTimeRemaining) + let conciseMessage = relativeTimeRemaining + NSLocalizedString(" remaining", comment: "remaining time in setting's profile expiration section") + return nearExpiration ? verboseMessage : conciseMessage + } + + private static func relativeTimeFormatter(maxUnitCount:Int) -> DateComponentsFormatter { + let formatter = DateComponentsFormatter() + let includeHours = maxUnitCount == 2 + formatter.allowedUnits = includeHours ? [.day, .hour] : [.day] + formatter.unitsStyle = .full + formatter.zeroFormattingBehavior = .dropLeading + formatter.maximumUnitCount = maxUnitCount + return formatter; + } + + static func buildDate() -> Date? { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEE MMM d HH:mm:ss 'UTC' yyyy" + dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Set locale to ensure parsing works + + guard let dateString = BuildDetails.default.buildDateString, + let date = dateFormatter.date(from: dateString) else { + return nil + } + + return date + } + + static func isTestFlightBuild() -> Bool { + return BuildDetails.default.isGitHubBuild ?? false + } + + static func calculateExpirationDate(profileExpiration: Date) -> Date { + let isTestFlight = isTestFlightBuild() + + if isTestFlight, let buildDate = buildDate() { + let testflightExpiration = Calendar.current.date(byAdding: .day, value: 90, to: buildDate)! + + return profileExpiration < testflightExpiration ? profileExpiration : testflightExpiration + } else { + return profileExpiration + } + } +} diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 43c62d128b..9edf481ba2 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -323,7 +323,7 @@ class LoopAppManager: NSObject { func didBecomeActive() { if let rootViewController = rootViewController { - ProfileExpirationAlerter.alertIfNeeded(viewControllerToPresentFrom: rootViewController) + AppExpirationAlerter.alertIfNeeded(viewControllerToPresentFrom: rootViewController) } settingsManager?.didBecomeActive() deviceDataManager?.didBecomeActive() diff --git a/Loop/Managers/ProfileExpirationAlerter.swift b/Loop/Managers/ProfileExpirationAlerter.swift deleted file mode 100644 index 3aa742732b..0000000000 --- a/Loop/Managers/ProfileExpirationAlerter.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// ProfileExpirationAlerter.swift -// Loop -// -// Created by Pete Schwamb on 8/21/21. -// Copyright © 2021 LoopKit Authors. All rights reserved. -// - -import Foundation -import UserNotifications -import LoopCore - - -class ProfileExpirationAlerter { - - static let expirationAlertWindow: TimeInterval = .days(20) - static let settingsPageExpirationWarningModeWindow: TimeInterval = .days(3) - - static func alertIfNeeded(viewControllerToPresentFrom: UIViewController) { - - let now = Date() - - guard let profileExpiration = BuildDetails.default.profileExpiration, now > profileExpiration - expirationAlertWindow else { - return - } - - let timeUntilExpiration = profileExpiration.timeIntervalSince(now) - - let minimumTimeBetweenAlerts: TimeInterval = timeUntilExpiration > .hours(24) ? .days(2) : .hours(1) - - if let lastAlertDate = UserDefaults.appGroup?.lastProfileExpirationAlertDate { - guard now > lastAlertDate + minimumTimeBetweenAlerts else { - return - } - } - - let formatter = DateComponentsFormatter() - formatter.allowedUnits = [.day, .hour] - formatter.unitsStyle = .full - formatter.zeroFormattingBehavior = .dropLeading - formatter.maximumUnitCount = 1 - let timeUntilExpirationStr = formatter.string(from: timeUntilExpiration) - - let alertMessage = createVerboseAlertMessage(timeUntilExpirationStr: timeUntilExpirationStr!) - - let dialog = UIAlertController( - title: NSLocalizedString("Profile Expires Soon", comment: "The title for notification of upcoming profile expiration"), - message: alertMessage, - preferredStyle: .alert) - dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil)) - dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming profile expiration"), style: .default, handler: { (_) in - UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) - })) - viewControllerToPresentFrom.present(dialog, animated: true, completion: nil) - - UserDefaults.appGroup?.lastProfileExpirationAlertDate = now - } - - static func createVerboseAlertMessage(timeUntilExpirationStr:String) -> String { - return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to update before that, with a new provisioning profile.", comment: "Format string for body for notification of upcoming provisioning profile expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) - } - - static func isNearProfileExpiration(profileExpiration:Date) -> Bool { - return profileExpiration.timeIntervalSinceNow < settingsPageExpirationWarningModeWindow - } - - static func createProfileExpirationSettingsMessage(profileExpiration:Date) -> String { - let nearExpiration = isNearProfileExpiration(profileExpiration: profileExpiration) - let maxUnitCount = nearExpiration ? 2 : 1 // only include hours in the msg if near expiration - let readableRelativeTime: String? = relativeTimeFormatter(maxUnitCount: maxUnitCount).string(from: profileExpiration.timeIntervalSinceNow) - let relativeTimeRemaining: String = readableRelativeTime ?? NSLocalizedString("Unknown time", comment: "Unknown amount of time in settings' profile expiration section") - let verboseMessage = createVerboseAlertMessage(timeUntilExpirationStr: relativeTimeRemaining) - let conciseMessage = relativeTimeRemaining + NSLocalizedString(" remaining", comment: "remaining time in setting's profile expiration section") - return nearExpiration ? verboseMessage : conciseMessage - } - - private static func relativeTimeFormatter(maxUnitCount:Int) -> DateComponentsFormatter { - let formatter = DateComponentsFormatter() - let includeHours = maxUnitCount == 2 - formatter.allowedUnits = includeHours ? [.day, .hour] : [.day] - formatter.unitsStyle = .full - formatter.zeroFormattingBehavior = .dropLeading - formatter.maximumUnitCount = maxUnitCount - return formatter; - } -} diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 0b4cb55133..a07d623f40 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -417,25 +417,48 @@ extension SettingsView { DIY loop specific component to show users the amount of time remaining on their build before a rebuild is necessary. */ private func profileExpirationSection(profileExpiration:Date) -> some View { - let nearExpiration : Bool = ProfileExpirationAlerter.isNearProfileExpiration(profileExpiration: profileExpiration) - let profileExpirationMsg = ProfileExpirationAlerter.createProfileExpirationSettingsMessage(profileExpiration: profileExpiration) - let readableExpirationTime = Self.dateFormatter.string(from: profileExpiration) + let expirationDate = AppExpirationAlerter.calculateExpirationDate(profileExpiration: profileExpiration) + let isTestFlight = AppExpirationAlerter.isTestFlightBuild() - return Section(header: SectionHeader(label: NSLocalizedString("App Profile", comment: "Settings app profile section")), - footer: Text(NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime)) { - if(nearExpiration) { - Text(profileExpirationMsg).foregroundColor(.red) - } else { - HStack { - Text("Profile Expiration", comment: "Settings App Profile expiration view") - Spacer() - Text(profileExpirationMsg).foregroundColor(Color.secondary) + let nearExpiration : Bool = AppExpirationAlerter.isNearExpiration(expirationDate: expirationDate) + let profileExpirationMsg = AppExpirationAlerter.createProfileExpirationSettingsMessage(expirationDate: expirationDate) + let readableExpirationTime = Self.dateFormatter.string(from: expirationDate) + + if isTestFlight { + return Section(header: SectionHeader(label: NSLocalizedString("TestFlight", comment: "Settings app TestFlight section")), + footer: Text(NSLocalizedString("TestFlight expires ", comment: "Time that build expires") + readableExpirationTime)) { + if(nearExpiration) { + Text(profileExpirationMsg).foregroundColor(.red) + } else { + HStack { + Text("TestFlight Expiration", comment: "Settings TestFlight expiration view") + Spacer() + Text(profileExpirationMsg).foregroundColor(Color.secondary) + } + } + Button(action: { + UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/")!) + }) { + Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) } } - Button(action: { - UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) - }) { - Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) + } else { + return Section(header: SectionHeader(label: NSLocalizedString("App Profile", comment: "Settings app profile section")), + footer: Text(NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime)) { + if(nearExpiration) { + Text(profileExpirationMsg).foregroundColor(.red) + } else { + HStack { + Text("Profile Expiration", comment: "Settings App Profile expiration view") + Spacer() + Text(profileExpirationMsg).foregroundColor(Color.secondary) + } + } + Button(action: { + UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) + }) { + Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) + } } } } diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index ea7b1f27f8..5431b4f239 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -72,16 +72,6 @@ if [ -e "${provisioning_profile_path}" ]; then profile_expire_date=$(security cms -D -i "${provisioning_profile_path}" | plutil -p - | grep ExpirationDate | cut -b 23-) # Convert to plutil format profile_expire_date=$(date -j -f "%Y-%m-%d %H:%M:%S" "${profile_expire_date}" +"%Y-%m-%dT%H:%M:%SZ") - # Handle github action, testflight builds that expire <= 90 days - if [ -n "$GITHUB_ACTIONS" ]; then - github_expire_date=$(date -j -v+90d +"%Y-%m-%dT%H:%M:%SZ") - - if [ "$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "${github_expire_date}" +%s)" -lt "$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "${profile_expire_date}" +%s)" ]; then - profile_expire_date=$github_expire_date - else - echo "GitHub Actions detected, expiration date is not more than 90 days in the future." - fi - fi plutil -replace com-loopkit-Loop-profile-expiration -date "${profile_expire_date}" "${info_plist_path}" else @@ -102,3 +92,8 @@ then fi popd . > /dev/null fi + +# Handle github action +if [ -n "$GITHUB_ACTIONS" ]; then + plutil -replace com-loopkit-GitHub-build -bool true "${info_plist_path}" +fi From 78a75cdf718e90f7ec9debb7e0b8ea0a99383bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 28 Aug 2023 09:51:48 +0200 Subject: [PATCH 04/19] TF text adjustments --- Loop/Managers/AppExpirationAlerter.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift index 23ceb1e7a3..08ff824d58 100644 --- a/Loop/Managers/AppExpirationAlerter.swift +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -51,7 +51,7 @@ class AppExpirationAlerter { title: NSLocalizedString("TestFlight Expires Soon", comment: "The title for notification of upcoming TestFlight expiration"), message: alertMessage, preferredStyle: .alert) - dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming profile expiration"), style: .default, handler: nil)) + dialog.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: "Text for ok action on notification of upcoming TestFlight expiration"), style: .default, handler: nil)) dialog.addAction(UIAlertAction(title: NSLocalizedString("More Info", comment: "Text for more info action on notification of upcoming TestFlight expiration"), style: .default, handler: { (_) in UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/")!) })) @@ -73,9 +73,9 @@ class AppExpirationAlerter { static func createVerboseAlertMessage(timeUntilExpirationStr:String) -> String { if isTestFlightBuild() { - return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to update before that, with a new provisioning profile.", comment: "Format string for body for notification of upcoming provisioning profile expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) + return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to rebuild before that.", comment: "Format string for body for notification of upcoming expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) } else { - return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to rebuild before that.", comment: "Format string for body for notification of upcoming provisioning profile expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) + return String(format: NSLocalizedString("%1$@ will stop working in %2$@. You will need to update before that, with a new provisioning profile.", comment: "Format string for body for notification of upcoming provisioning profile expiration. (1: app name) (2: amount of time until expiration"), Bundle.main.bundleDisplayName, timeUntilExpirationStr) } } From 874c393130a8f91c348e64675ec34485b44d17f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 28 Aug 2023 13:10:26 +0200 Subject: [PATCH 05/19] Fix for UTC timezone --- Loop/Managers/AppExpirationAlerter.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift index 08ff824d58..6b8c0a88a2 100644 --- a/Loop/Managers/AppExpirationAlerter.swift +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -107,6 +107,7 @@ class AppExpirationAlerter { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "EEE MMM d HH:mm:ss 'UTC' yyyy" dateFormatter.locale = Locale(identifier: "en_US_POSIX") // Set locale to ensure parsing works + dateFormatter.timeZone = TimeZone(identifier: "UTC") guard let dateString = BuildDetails.default.buildDateString, let date = dateFormatter.date(from: dateString) else { From 211a337a6a1324bd9a489f1b8fc43d5697170037 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 28 Aug 2023 12:49:20 -0700 Subject: [PATCH 06/19] fix for modal alert --- Loop/Managers/AppExpirationAlerter.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift index 6b8c0a88a2..a637927f37 100644 --- a/Loop/Managers/AppExpirationAlerter.swift +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -19,8 +19,8 @@ class AppExpirationAlerter { static func alertIfNeeded(viewControllerToPresentFrom: UIViewController) { let now = Date() - - guard let profileExpiration = BuildDetails.default.profileExpiration, now > profileExpiration - expirationAlertWindow else { + + guard let profileExpiration = BuildDetails.default.profileExpiration else { return } @@ -28,6 +28,10 @@ class AppExpirationAlerter { let timeUntilExpiration = expirationDate.timeIntervalSince(now) + if timeUntilExpiration > expirationAlertWindow { + return + } + let minimumTimeBetweenAlerts: TimeInterval = timeUntilExpiration > .hours(24) ? .days(2) : .hours(1) if let lastAlertDate = UserDefaults.appGroup?.lastProfileExpirationAlertDate { From b292371e9390c4caa5be5ad8f83b6689c8228edf Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 28 Aug 2023 14:42:37 -0700 Subject: [PATCH 07/19] test an alternative method for isTestFlightBuild --- Loop/Managers/AppExpirationAlerter.swift | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift index a637927f37..cbea4f4941 100644 --- a/Loop/Managers/AppExpirationAlerter.swift +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -104,7 +104,7 @@ class AppExpirationAlerter { formatter.unitsStyle = .full formatter.zeroFormattingBehavior = .dropLeading formatter.maximumUnitCount = maxUnitCount - return formatter; + return formatter } static func buildDate() -> Date? { @@ -122,9 +122,23 @@ class AppExpirationAlerter { } static func isTestFlightBuild() -> Bool { - return BuildDetails.default.isGitHubBuild ?? false + // If an "embedded.mobileprovision" is present in the main bundle, then + // this is an Xcode, Ad-Hoc, or Enterprise distribution. Return false. + if Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision") != nil { + return false + } + + // If an app store receipt is not present in the main bundle, then we cannot + // say whether this is a TestFlight or App Store distribution. Return false. + guard let receiptName = Bundle.main.appStoreReceiptURL?.lastPathComponent else { + return false + } + + // A TestFlight distribution presents a "sandboxReceipt", while an App Store + // distribution presents a "receipt". Return true if we have a TestFlight receipt. + return "sandboxReceipt".caseInsensitiveCompare(receiptName) == .orderedSame } - + static func calculateExpirationDate(profileExpiration: Date) -> Date { let isTestFlight = isTestFlightBuild() From 390938075e9e5e9524050dbe4c758e8f3a36141d Mon Sep 17 00:00:00 2001 From: marionbarker Date: Mon, 28 Aug 2023 15:42:34 -0700 Subject: [PATCH 08/19] Revert changes not needed by isTestFlightBuild function --- Common/Models/BuildDetails.swift | 4 ---- Scripts/capture-build-details.sh | 6 ------ 2 files changed, 10 deletions(-) diff --git a/Common/Models/BuildDetails.swift b/Common/Models/BuildDetails.swift index 0badb257ac..63517e7e79 100644 --- a/Common/Models/BuildDetails.swift +++ b/Common/Models/BuildDetails.swift @@ -65,9 +65,5 @@ class BuildDetails { var workspaceGitBranch: String? { return dict["com-loopkit-LoopWorkspace-git-branch"] as? String } - - var isGitHubBuild: Bool? { - return dict["com-loopkit-GitHub-build"] as? Bool - } } diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index 5431b4f239..66f827d7c3 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -72,7 +72,6 @@ if [ -e "${provisioning_profile_path}" ]; then profile_expire_date=$(security cms -D -i "${provisioning_profile_path}" | plutil -p - | grep ExpirationDate | cut -b 23-) # Convert to plutil format profile_expire_date=$(date -j -f "%Y-%m-%d %H:%M:%S" "${profile_expire_date}" +"%Y-%m-%dT%H:%M:%SZ") - plutil -replace com-loopkit-Loop-profile-expiration -date "${profile_expire_date}" "${info_plist_path}" else warn "Invalid provisioning profile path ${provisioning_profile_path}" @@ -92,8 +91,3 @@ then fi popd . > /dev/null fi - -# Handle github action -if [ -n "$GITHUB_ACTIONS" ]; then - plutil -replace com-loopkit-GitHub-build -bool true "${info_plist_path}" -fi From b6eb08beba45cfcfcab004f2999cbc0b2b420822 Mon Sep 17 00:00:00 2001 From: marionbarker Date: Tue, 29 Aug 2023 16:15:42 -0700 Subject: [PATCH 09/19] Add simulator check for completeness --- Loop/Managers/AppExpirationAlerter.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Loop/Managers/AppExpirationAlerter.swift b/Loop/Managers/AppExpirationAlerter.swift index cbea4f4941..e7768f92b6 100644 --- a/Loop/Managers/AppExpirationAlerter.swift +++ b/Loop/Managers/AppExpirationAlerter.swift @@ -122,6 +122,12 @@ class AppExpirationAlerter { } static func isTestFlightBuild() -> Bool { + // If the target environment is a simulator, then + // this is not a TestFlight distribution. Return false. + #if targetEnvironment(simulator) + return false + #endif + // If an "embedded.mobileprovision" is present in the main bundle, then // this is an Xcode, Ad-Hoc, or Enterprise distribution. Return false. if Bundle.main.url(forResource: "embedded", withExtension: "mobileprovision") != nil { From 156401e333ea66f331989f59ed22f0bb189b7ff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 30 Aug 2023 21:52:31 +0200 Subject: [PATCH 10/19] Refactoring and renaming --- Loop/Views/SettingsView.swift | 73 ++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 35 deletions(-) diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index a07d623f40..680adcc539 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -416,50 +416,53 @@ extension SettingsView { /* DIY loop specific component to show users the amount of time remaining on their build before a rebuild is necessary. */ - private func profileExpirationSection(profileExpiration:Date) -> some View { + private func appExpirationSection(profileExpiration: Date) -> some View { let expirationDate = AppExpirationAlerter.calculateExpirationDate(profileExpiration: profileExpiration) let isTestFlight = AppExpirationAlerter.isTestFlightBuild() - - let nearExpiration : Bool = AppExpirationAlerter.isNearExpiration(expirationDate: expirationDate) + let nearExpiration = AppExpirationAlerter.isNearExpiration(expirationDate: expirationDate) let profileExpirationMsg = AppExpirationAlerter.createProfileExpirationSettingsMessage(expirationDate: expirationDate) let readableExpirationTime = Self.dateFormatter.string(from: expirationDate) if isTestFlight { - return Section(header: SectionHeader(label: NSLocalizedString("TestFlight", comment: "Settings app TestFlight section")), - footer: Text(NSLocalizedString("TestFlight expires ", comment: "Time that build expires") + readableExpirationTime)) { - if(nearExpiration) { - Text(profileExpirationMsg).foregroundColor(.red) - } else { - HStack { - Text("TestFlight Expiration", comment: "Settings TestFlight expiration view") - Spacer() - Text(profileExpirationMsg).foregroundColor(Color.secondary) - } - } - Button(action: { - UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/")!) - }) { - Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) - } - } + return createAppExpirationSection( + headerLabel: NSLocalizedString("TestFlight", comment: "Settings app TestFlight section"), + footerLabel: NSLocalizedString("TestFlight expires ", comment: "Time that build expires") + readableExpirationTime, + expirationLabel: NSLocalizedString("TestFlight Expiration", comment: "Settings TestFlight expiration view"), + updateURL: "https://loopkit.github.io/loopdocs/gh-actions/gh-update/", + nearExpiration: nearExpiration, + expirationMessage: profileExpirationMsg + ) } else { - return Section(header: SectionHeader(label: NSLocalizedString("App Profile", comment: "Settings app profile section")), - footer: Text(NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime)) { - if(nearExpiration) { - Text(profileExpirationMsg).foregroundColor(.red) - } else { - HStack { - Text("Profile Expiration", comment: "Settings App Profile expiration view") - Spacer() - Text(profileExpirationMsg).foregroundColor(Color.secondary) - } - } - Button(action: { - UIApplication.shared.open(URL(string: "https://loopkit.github.io/loopdocs/build/updating/")!) - }) { - Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) + return createAppExpirationSection( + headerLabel: NSLocalizedString("App Profile", comment: "Settings app profile section"), + footerLabel: NSLocalizedString("Profile expires ", comment: "Time that profile expires") + readableExpirationTime, + expirationLabel: NSLocalizedString("Profile Expiration", comment: "Settings App Profile expiration view"), + updateURL: "https://loopkit.github.io/loopdocs/build/updating/", + nearExpiration: nearExpiration, + expirationMessage: profileExpirationMsg + ) + } + } + + private func createAppExpirationSection(headerLabel: String, footerLabel: String, expirationLabel: String, updateURL: String, nearExpiration: Bool, expirationMessage: String) -> some View { + return Section( + header: SectionHeader(label: headerLabel), + footer: Text(footerLabel) + ) { + if nearExpiration { + Text(expirationMessage).foregroundColor(.red) + } else { + HStack { + Text(expirationLabel) + Spacer() + Text(expirationMessage).foregroundColor(Color.secondary) } } + Button(action: { + UIApplication.shared.open(URL(string: updateURL)!) + }) { + Text(NSLocalizedString("How to update (LoopDocs)", comment: "The title text for how to update")) + } } } From 180eceb4c05274d96f0dadc01dc03abe746b91ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Wed, 30 Aug 2023 22:05:00 +0200 Subject: [PATCH 11/19] Refactoring and renaming --- Loop/Views/SettingsView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop/Views/SettingsView.swift b/Loop/Views/SettingsView.swift index 680adcc539..958e38f100 100644 --- a/Loop/Views/SettingsView.swift +++ b/Loop/Views/SettingsView.swift @@ -75,7 +75,7 @@ public struct SettingsView: View { supportSection if let profileExpiration = BuildDetails.default.profileExpiration, FeatureFlags.profileExpirationSettingsViewEnabled { - profileExpirationSection(profileExpiration: profileExpiration) + appExpirationSection(profileExpiration: profileExpiration) } } } From ea66ab2f34f0bb34a4a34ee71633f87873bc8932 Mon Sep 17 00:00:00 2001 From: Billy Booth Date: Sat, 9 Sep 2023 12:47:00 -0500 Subject: [PATCH 12/19] Add Hindi Intents --- Common/hi.lproj/Intents.strings | 24 ++++++++++++++++++++++++ Loop.xcodeproj/project.pbxproj | 2 ++ 2 files changed, 26 insertions(+) create mode 100644 Common/hi.lproj/Intents.strings diff --git a/Common/hi.lproj/Intents.strings b/Common/hi.lproj/Intents.strings new file mode 100644 index 0000000000..853af215c0 --- /dev/null +++ b/Common/hi.lproj/Intents.strings @@ -0,0 +1,24 @@ +"80eo5o" = "Add Carb Entry"; + +"9KhaIS" = "I've set the preset"; + +"I4OZy8" = "Enable Override Preset"; + +"OcNxIj" = "Add Carb Entry"; + +"XNNmtH" = "Enable preset in Loop"; + +"ZZ3mtM" = "Enable an override preset in Loop"; + +"b085BW" = "I wasn't able to set the preset."; + +"lYMuWV" = "Override Name"; + +"nDKAmn" = "What's the name of the override you'd like to set?"; + +"oLQSsJ" = "Enable '${overrideName}' Override Preset"; + +"yBzwCL" = "Override Selection"; + +"yc02Yq" = "Add a carb entry to Loop"; + diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 0bc7b03ff7..83abfd98b4 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -787,6 +787,7 @@ 1DC63E7325351BDF004605DA /* TrueTime.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TrueTime.framework; path = Carthage/Build/iOS/TrueTime.framework; sourceTree = ""; }; 1DE09BA824A3E23F009EE9F9 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 1DFE9E162447B6270082C280 /* UserNotificationAlertSchedulerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserNotificationAlertSchedulerTests.swift; sourceTree = ""; }; + 3D03C6DA2AACE6AC00FDE5D2 /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Intents.strings; 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 = ""; }; @@ -4224,6 +4225,7 @@ C1C3127F297E4C0400296DA4 /* ar */, C1C247882995823200371B88 /* sk */, C1C5357529C6346A00E32DF9 /* cs */, + 3D03C6DA2AACE6AC00FDE5D2 /* hi */, ); name = Intents.intentdefinition; sourceTree = ""; From 232b21d071d7663c9f7946d2e101b372fefe7e12 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 9 Sep 2023 19:14:20 -0500 Subject: [PATCH 13/19] Update build destinations --- Loop.xcodeproj/project.pbxproj | 36 +++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/Loop.xcodeproj/project.pbxproj b/Loop.xcodeproj/project.pbxproj index 83abfd98b4..dacc5be194 100644 --- a/Loop.xcodeproj/project.pbxproj +++ b/Loop.xcodeproj/project.pbxproj @@ -24,7 +24,7 @@ 149A28E42A8A63A700052EDF /* FavoriteFoodDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 149A28E32A8A63A700052EDF /* FavoriteFoodDetailView.swift */; }; 14B1735E28AED9EC006CCD7C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735D28AED9EC006CCD7C /* WidgetKit.framework */; }; 14B1736028AED9EC006CCD7C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */; }; - 14B1736928AED9EE006CCD7C /* SmallStatusWidgetExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 14B1735C28AED9EC006CCD7C /* SmallStatusWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 14B1736928AED9EE006CCD7C /* Loop Widget Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 14B1735C28AED9EC006CCD7C /* Loop Widget Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 14B1737228AEDBF6006CCD7C /* BasalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B1736E28AEDBF6006CCD7C /* BasalView.swift */; }; 14B1737328AEDBF6006CCD7C /* SystemStatusWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B1736F28AEDBF6006CCD7C /* SystemStatusWidget.swift */; }; 14B1737428AEDBF6006CCD7C /* GlucoseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14B1737028AEDBF6006CCD7C /* GlucoseView.swift */; }; @@ -719,7 +719,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( - 14B1736928AED9EE006CCD7C /* SmallStatusWidgetExtension.appex in Embed App Extensions */, + 14B1736928AED9EE006CCD7C /* Loop Widget Extension.appex in Embed App Extensions */, E9B07F94253BBA6500BAD8F8 /* Loop Intent Extension.appex in Embed App Extensions */, 4F70C1E81DE8DCA7006380B7 /* Loop Status Extension.appex in Embed App Extensions */, ); @@ -751,7 +751,7 @@ 149A28BA2A853E5100052EDF /* CarbEntryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryViewModel.swift; sourceTree = ""; }; 149A28BC2A853E6C00052EDF /* CarbEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarbEntryView.swift; sourceTree = ""; }; 149A28E32A8A63A700052EDF /* FavoriteFoodDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteFoodDetailView.swift; sourceTree = ""; }; - 14B1735C28AED9EC006CCD7C /* SmallStatusWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; name = SmallStatusWidgetExtension.appex; path = "Loop Widget Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + 14B1735C28AED9EC006CCD7C /* Loop Widget Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Loop Widget Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 14B1735D28AED9EC006CCD7C /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 14B1735F28AED9EC006CCD7C /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 14B1736628AED9EE006CCD7C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1993,7 +1993,7 @@ 43D9FFCF21EAE05D00AF44BF /* LoopCore.framework */, 43D9002A21EB209400AF44BF /* LoopCore.framework */, E9B07F7C253BBA6500BAD8F8 /* Loop Intent Extension.appex */, - 14B1735C28AED9EC006CCD7C /* SmallStatusWidgetExtension.appex */, + 14B1735C28AED9EC006CCD7C /* Loop Widget Extension.appex */, ); name = Products; sourceTree = ""; @@ -2981,7 +2981,7 @@ ); name = "Loop Widget Extension"; productName = SmallStatusWidgetExtension; - productReference = 14B1735C28AED9EC006CCD7C /* SmallStatusWidgetExtension.appex */; + productReference = 14B1735C28AED9EC006CCD7C /* Loop Widget Extension.appex */; productType = "com.apple.product-type.app-extension"; }; 43776F8B1B8022E90074EA36 /* Loop */ = { @@ -4827,6 +4827,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(LOOP_PROVISIONING_PROFILE_SPECIFIER_WIDGET_EXTENSION_DEBUG)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -4871,6 +4875,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(LOOP_PROVISIONING_PROFILE_SPECIFIER_WIDGET_EXTENSION_RELEASE)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; @@ -5128,6 +5136,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -5154,6 +5163,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = 1; }; name = Release; @@ -5407,6 +5417,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(LOOP_PROVISIONING_PROFILE_SPECIFIER_STATUS_EXTENSION_DEBUG)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -5429,6 +5443,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(LOOP_PROVISIONING_PROFILE_SPECIFIER_STATUS_EXTENSION_RELEASE)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = 1; }; name = Release; @@ -5492,6 +5510,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(LOOP_PROVISIONING_PROFILE_SPECIFIER_INTENT_EXTENSION_DEBUG)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = 1; }; name = Debug; @@ -5515,6 +5537,10 @@ PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = "$(LOOP_PROVISIONING_PROFILE_SPECIFIER_INTENT_EXTENSION_RELEASE)"; SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; TARGETED_DEVICE_FAMILY = 1; }; name = Release; From bea91f06dbf6736e160f31329fb003d063337da8 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Fri, 22 Sep 2023 08:15:07 -0500 Subject: [PATCH 14/19] Adding CGM Event Store (#2071) --- Loop/Managers/DeviceDataManager.swift | 54 ++++--- Loop/Managers/LoopAppManager.swift | 2 +- Loop/Managers/RemoteDataServicesManager.swift | 152 ++++++------------ Loop/Managers/SettingsManager.swift | 2 +- Scripts/capture-build-details.sh | 3 +- 5 files changed, 88 insertions(+), 125 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 2e8d157531..95ef432094 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -157,6 +157,8 @@ final class DeviceDataManager { let glucoseStore: GlucoseStore + let cgmEventStore: CgmEventStore + private let cacheStore: PersistenceController let dosingDecisionStore: DosingDecisionStore @@ -337,11 +339,13 @@ final class DeviceDataManager { cgmStalenessMonitor = CGMStalenessMonitor() cgmStalenessMonitor.delegate = glucoseStore + + cgmEventStore = CgmEventStore(cacheStore: cacheStore, cacheLength: localCacheDuration) + + dosingDecisionStore = DosingDecisionStore(store: cacheStore, expireAfter: localCacheDuration) - self.dosingDecisionStore = DosingDecisionStore(store: cacheStore, expireAfter: localCacheDuration) - - self.cgmHasValidSensorSession = false - self.pumpIsAllowingAutomation = true + cgmHasValidSensorSession = false + pumpIsAllowingAutomation = true self.automaticDosingStatus = automaticDosingStatus // HealthStorePreferredGlucoseUnitDidChange will be notified once the user completes the health access form. Set to .milligramsPerDeciliter until then @@ -405,6 +409,7 @@ final class DeviceDataManager { doseStore: doseStore, dosingDecisionStore: dosingDecisionStore, glucoseStore: glucoseStore, + cgmEventStore: cgmEventStore, settingsStore: settingsManager.settingsStore, overrideHistory: overrideHistory, insulinDeliveryStore: doseStore.insulinDeliveryStore @@ -435,6 +440,7 @@ final class DeviceDataManager { doseStore.delegate = self dosingDecisionStore.delegate = self glucoseStore.delegate = self + cgmEventStore.delegate = self doseStore.insulinDeliveryStore.delegate = self remoteDataServicesManager.delegate = self @@ -934,6 +940,16 @@ extension DeviceDataManager: CGMManagerDelegate { } } + func cgmManager(_ manager: LoopKit.CGMManager, hasNew events: [PersistedCgmEvent]) { + Task { + do { + try await cgmEventStore.add(events: events) + } catch { + self.log.error("Error storing cgm events: %{public}@", error.localizedDescription) + } + } + } + func startDateToFilterNewData(for manager: CGMManager) -> Date? { dispatchPrecondition(condition: .onQueue(queue)) return glucoseStore.latestGlucose?.startDate @@ -1191,60 +1207,56 @@ extension DeviceDataManager: PumpManagerOnboardingDelegate { // MARK: - AlertStoreDelegate extension DeviceDataManager: AlertStoreDelegate { - func alertStoreHasUpdatedAlertData(_ alertStore: AlertStore) { - remoteDataServicesManager.alertStoreHasUpdatedAlertData(alertStore) + remoteDataServicesManager.triggerUpload(for: .alert) } - } // MARK: - CarbStoreDelegate extension DeviceDataManager: CarbStoreDelegate { - func carbStoreHasUpdatedCarbData(_ carbStore: CarbStore) { - remoteDataServicesManager.carbStoreHasUpdatedCarbData(carbStore) + remoteDataServicesManager.triggerUpload(for: .carb) } func carbStore(_ carbStore: CarbStore, didError error: CarbStore.CarbStoreError) {} - } // MARK: - DoseStoreDelegate extension DeviceDataManager: DoseStoreDelegate { - func doseStoreHasUpdatedPumpEventData(_ doseStore: DoseStore) { - remoteDataServicesManager.doseStoreHasUpdatedPumpEventData(doseStore) + remoteDataServicesManager.triggerUpload(for: .pumpEvent) } - } // MARK: - DosingDecisionStoreDelegate extension DeviceDataManager: DosingDecisionStoreDelegate { - func dosingDecisionStoreHasUpdatedDosingDecisionData(_ dosingDecisionStore: DosingDecisionStore) { - remoteDataServicesManager.dosingDecisionStoreHasUpdatedDosingDecisionData(dosingDecisionStore) + remoteDataServicesManager.triggerUpload(for: .dosingDecision) } - } // MARK: - GlucoseStoreDelegate extension DeviceDataManager: GlucoseStoreDelegate { - func glucoseStoreHasUpdatedGlucoseData(_ glucoseStore: GlucoseStore) { - remoteDataServicesManager.glucoseStoreHasUpdatedGlucoseData(glucoseStore) + remoteDataServicesManager.triggerUpload(for: .glucose) } - } // MARK: - InsulinDeliveryStoreDelegate extension DeviceDataManager: InsulinDeliveryStoreDelegate { - func insulinDeliveryStoreHasUpdatedDoseData(_ insulinDeliveryStore: InsulinDeliveryStore) { - remoteDataServicesManager.insulinDeliveryStoreHasUpdatedDoseData(insulinDeliveryStore) + remoteDataServicesManager.triggerUpload(for: .dose) } +} +// MARK: - CgmEventStoreDelegate +extension DeviceDataManager: CgmEventStoreDelegate { + func cgmEventStoreHasUpdatedData(_ cgmEventStore: LoopKit.CgmEventStore) { + remoteDataServicesManager.triggerUpload(for: .cgmEvent) + } } + // MARK: - TestingPumpManager extension DeviceDataManager { func deleteTestingPumpData(completion: ((Error?) -> Void)? = nil) { diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 9edf481ba2..95a7ed6d2d 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -603,7 +603,7 @@ extension LoopAppManager: TemporaryScheduleOverrideHistoryDelegate { func temporaryScheduleOverrideHistoryDidUpdate(_ history: TemporaryScheduleOverrideHistory) { UserDefaults.appGroup?.overrideHistory = history - deviceDataManager.remoteDataServicesManager.temporaryScheduleOverrideHistoryDidUpdate() + deviceDataManager.remoteDataServicesManager.triggerUpload(for: .overrides) } } diff --git a/Loop/Managers/RemoteDataServicesManager.swift b/Loop/Managers/RemoteDataServicesManager.swift index 14a3416900..3b86cf7f1e 100644 --- a/Loop/Managers/RemoteDataServicesManager.swift +++ b/Loop/Managers/RemoteDataServicesManager.swift @@ -10,13 +10,14 @@ import os.log import Foundation import LoopKit -enum RemoteDataType: String { +enum RemoteDataType: String, CaseIterable { case alert = "Alert" case carb = "Carb" case dose = "Dose" case dosingDecision = "DosingDecision" case glucose = "Glucose" case pumpEvent = "PumpEvent" + case cgmEvent = "CgmEvent" case settings = "Settings" case overrides = "Overrides" @@ -129,6 +130,8 @@ final class RemoteDataServicesManager { private let glucoseStore: GlucoseStore + private let cgmEventStore: CgmEventStore + private let insulinDeliveryStore: InsulinDeliveryStore private let settingsStore: SettingsStore @@ -141,6 +144,7 @@ final class RemoteDataServicesManager { doseStore: DoseStore, dosingDecisionStore: DosingDecisionStore, glucoseStore: GlucoseStore, + cgmEventStore: CgmEventStore, settingsStore: SettingsStore, overrideHistory: TemporaryScheduleOverrideHistory, insulinDeliveryStore: InsulinDeliveryStore @@ -150,6 +154,7 @@ final class RemoteDataServicesManager { self.doseStore = doseStore self.dosingDecisionStore = dosingDecisionStore self.glucoseStore = glucoseStore + self.cgmEventStore = cgmEventStore self.insulinDeliveryStore = insulinDeliveryStore self.settingsStore = settingsStore self.overrideHistory = overrideHistory @@ -167,13 +172,11 @@ final class RemoteDataServicesManager { } private func clearQueryAnchors(for remoteDataService: RemoteDataService) { - clearAlertQueryAnchor(for: remoteDataService) - clearCarbQueryAnchor(for: remoteDataService) - clearDoseQueryAnchor(for: remoteDataService) - clearDosingDecisionQueryAnchor(for: remoteDataService) - clearGlucoseQueryAnchor(for: remoteDataService) - clearPumpEventQueryAnchor(for: remoteDataService) - clearSettingsQueryAnchor(for: remoteDataService) + for remoteDataType in RemoteDataType.allCases { + dispatchQueue(for: remoteDataService, withRemoteDataType: remoteDataType).async { + UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: remoteDataType) + } + } } func triggerUpload(for triggeringType: RemoteDataType) { @@ -195,6 +198,8 @@ final class RemoteDataServicesManager { remoteDataServices.forEach { self.uploadGlucoseData(to: $0) } case .pumpEvent: remoteDataServices.forEach { self.uploadPumpEventData(to: $0) } + case .cgmEvent: + remoteDataServices.forEach { self.uploadCgmEventData(to: $0) } case .settings: remoteDataServices.forEach { self.uploadSettingsData(to: $0) } case .overrides: @@ -220,11 +225,6 @@ final class RemoteDataServicesManager { } extension RemoteDataServicesManager { - - public func alertStoreHasUpdatedAlertData(_ alertStore: AlertStore) { - triggerUpload(for: .alert) - } - private func uploadAlertData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -257,21 +257,9 @@ extension RemoteDataServicesManager { self.uploadGroup.leave() } } - - private func clearAlertQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .alert).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .alert) - } - } - } extension RemoteDataServicesManager { - - public func carbStoreHasUpdatedCarbData(_ carbStore: CarbStore) { - triggerUpload(for: .carb) - } - private func uploadCarbData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -311,21 +299,9 @@ extension RemoteDataServicesManager { } } } - - private func clearCarbQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .carb).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .carb) - } - } - } extension RemoteDataServicesManager { - - public func insulinDeliveryStoreHasUpdatedDoseData(_ insulinDeliveryStore: InsulinDeliveryStore) { - triggerUpload(for: .dose) - } - private func uploadDoseData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -365,21 +341,9 @@ extension RemoteDataServicesManager { } } } - - private func clearDoseQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .dose).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .dose) - } - } - } extension RemoteDataServicesManager { - - public func dosingDecisionStoreHasUpdatedDosingDecisionData(_ dosingDecisionStore: DosingDecisionStore) { - triggerUpload(for: .dosingDecision) - } - private func uploadDosingDecisionData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -419,21 +383,9 @@ extension RemoteDataServicesManager { } } } - - private func clearDosingDecisionQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .dosingDecision).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .dosingDecision) - } - } - } extension RemoteDataServicesManager { - - public func glucoseStoreHasUpdatedGlucoseData(_ glucoseStore: GlucoseStore) { - triggerUpload(for: .glucose) - } - private func uploadGlucoseData(to remoteDataService: RemoteDataService) { if delegate?.shouldSyncToRemoteService == false { @@ -478,21 +430,9 @@ extension RemoteDataServicesManager { } } } - - private func clearGlucoseQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .glucose).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .glucose) - } - } - } extension RemoteDataServicesManager { - - public func doseStoreHasUpdatedPumpEventData(_ doseStore: DoseStore) { - triggerUpload(for: .pumpEvent) - } - private func uploadPumpEventData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -532,21 +472,9 @@ extension RemoteDataServicesManager { } } } - - private func clearPumpEventQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .pumpEvent).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .pumpEvent) - } - } - } extension RemoteDataServicesManager { - - public func settingsStoreHasUpdatedSettingsData(_ settingsStore: SettingsStore) { - triggerUpload(for: .settings) - } - private func uploadSettingsData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -586,21 +514,9 @@ extension RemoteDataServicesManager { } } } - - private func clearSettingsQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .settings).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .settings) - } - } - } extension RemoteDataServicesManager { - - public func temporaryScheduleOverrideHistoryDidUpdate() { - triggerUpload(for: .overrides) - } - private func uploadTemporaryOverrideData(to remoteDataService: RemoteDataService) { uploadGroup.enter() @@ -629,10 +545,46 @@ extension RemoteDataServicesManager { self.uploadGroup.leave() } } +} - private func clearTemporaryOverrideQueryAnchor(for remoteDataService: RemoteDataService) { - dispatchQueue(for: remoteDataService, withRemoteDataType: .overrides).async { - UserDefaults.appGroup?.deleteQueryAnchor(for: remoteDataService, withRemoteDataType: .overrides) +extension RemoteDataServicesManager { + private func uploadCgmEventData(to remoteDataService: RemoteDataService) { + uploadGroup.enter() + + let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .pumpEvent) + + dispatchQueue(for: remoteDataService, withRemoteDataType: .cgmEvent).async { + let semaphore = DispatchSemaphore(value: 0) + let previousQueryAnchor = UserDefaults.appGroup?.getQueryAnchor(for: remoteDataService, withRemoteDataType: .cgmEvent) ?? CgmEventStore.QueryAnchor() + var continueUpload = false + + self.cgmEventStore.executeCgmEventQuery(fromQueryAnchor: previousQueryAnchor) { result in + switch result { + case .failure(let error): + self.log.error("Error querying pump event data: %{public}@", String(describing: error)) + semaphore.signal() + case .success(let queryAnchor, let data): + remoteDataService.uploadCgmEventData(data) { result in + switch result { + case .failure(let error): + self.log.error("Error synchronizing pump event data: %{public}@", String(describing: error)) + self.uploadFailed(key) + case .success: + UserDefaults.appGroup?.setQueryAnchor(for: remoteDataService, withRemoteDataType: .cgmEvent, queryAnchor) + continueUpload = queryAnchor != previousQueryAnchor + self.uploadSucceeded(key) + } + semaphore.signal() + } + } + } + + semaphore.wait() + self.uploadGroup.leave() + + if continueUpload { + self.uploadPumpEventData(to: remoteDataService) + } } } } diff --git a/Loop/Managers/SettingsManager.swift b/Loop/Managers/SettingsManager.swift index f7421b97f7..e3fdb60bf7 100644 --- a/Loop/Managers/SettingsManager.swift +++ b/Loop/Managers/SettingsManager.swift @@ -211,7 +211,7 @@ class SettingsManager { // MARK: - SettingsStoreDelegate extension SettingsManager: SettingsStoreDelegate { func settingsStoreHasUpdatedSettingsData(_ settingsStore: SettingsStore) { - remoteDataServicesManager?.settingsStoreHasUpdatedSettingsData(settingsStore) + remoteDataServicesManager?.triggerUpload(for: .settings) } } diff --git a/Scripts/capture-build-details.sh b/Scripts/capture-build-details.sh index 66f827d7c3..6122592374 100755 --- a/Scripts/capture-build-details.sh +++ b/Scripts/capture-build-details.sh @@ -10,10 +10,9 @@ SCRIPT_DIRECTORY="$(dirname "${0}")" error() { echo "ERROR: ${*}" >&2 - echo "Usage: ${SCRIPT} [-r|--git-source-root git-source-root] [-p|--provisioning-profile-path provisioning-profile-path] [-i|--info-plist-path info-plist-path]" >&2 + echo "Usage: ${SCRIPT} [-r|--git-source-root git-source-root] [-p|--provisioning-profile-path provisioning-profile-path]" >&2 echo "Parameters:" >&2 echo " -p|--provisioning-profile-path path to the .mobileprovision provisioning profile file to check for expiration; optional, defaults to \${HOME}/Library/MobileDevice/Provisioning Profiles/\${EXPANDED_PROVISIONING_PROFILE}.mobileprovision" >&2 - echo " -i|--info-plist-path path to the Info.plist file to modify; optional, defaults to \${BUILT_PRODUCTS_DIR}/\${INFOPLIST_PATH}" >&2 exit 1 } From c7091c00bb8a9412c881dcf49d30013b121a34dd Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Fri, 22 Sep 2023 15:36:31 -0500 Subject: [PATCH 15/19] Use pluginIdentifier for upload key --- Loop/Managers/RemoteDataServicesManager.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Loop/Managers/RemoteDataServicesManager.swift b/Loop/Managers/RemoteDataServicesManager.swift index c256013ba0..296e3befa9 100644 --- a/Loop/Managers/RemoteDataServicesManager.swift +++ b/Loop/Managers/RemoteDataServicesManager.swift @@ -551,7 +551,7 @@ extension RemoteDataServicesManager { private func uploadCgmEventData(to remoteDataService: RemoteDataService) { uploadGroup.enter() - let key = UploadTaskKey(serviceIdentifier: remoteDataService.serviceIdentifier, remoteDataType: .pumpEvent) + let key = UploadTaskKey(serviceIdentifier: remoteDataService.pluginIdentifier, remoteDataType: .pumpEvent) dispatchQueue(for: remoteDataService, withRemoteDataType: .cgmEvent).async { let semaphore = DispatchSemaphore(value: 0) From 15f05acc362b1193c053765f2fc33793bded744d Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 23 Sep 2023 10:07:09 -0500 Subject: [PATCH 16/19] Revert rawValue key name change for services --- Loop/Managers/Service.swift | 2 +- Loop/Managers/ServicesManager.swift | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Loop/Managers/Service.swift b/Loop/Managers/Service.swift index 1541208712..9f4b2f0eee 100644 --- a/Loop/Managers/Service.swift +++ b/Loop/Managers/Service.swift @@ -21,7 +21,7 @@ let availableStaticServices = staticServices.map { (Type) -> ServiceDescriptor i } func ServiceFromRawValue(_ rawValue: [String: Any]) -> Service? { - guard let serviceIdentifier = rawValue["statefulPluginIdentifier"] as? String, + guard let serviceIdentifier = rawValue["serviceIdentifier"] as? String, let rawState = rawValue["state"] as? Service.RawStateValue, let ServiceType = staticServicesByIdentifier[serviceIdentifier] else { diff --git a/Loop/Managers/ServicesManager.swift b/Loop/Managers/ServicesManager.swift index 7e62e95333..2393ceb073 100644 --- a/Loop/Managers/ServicesManager.swift +++ b/Loop/Managers/ServicesManager.swift @@ -100,7 +100,7 @@ class ServicesManager { } private func serviceTypeFromRawValue(_ rawValue: Service.RawStateValue) -> Service.Type? { - guard let identifier = rawValue["statefulPluginIdentifier"] as? String else { + guard let identifier = rawValue["serviceIdentifier"] as? String else { return nil } @@ -400,3 +400,15 @@ extension ServicesManager: ServiceOnboardingDelegate { extension ServicesManager { var availableSupports: [SupportUI] { activeServices.compactMap { $0 as? SupportUI } } } + +// Service extension for rawValue +extension Service { + typealias RawValue = [String: Any] + + var rawValue: RawValue { + return [ + "serviceIdentifier": pluginIdentifier, + "state": rawState + ] + } +} From 1922a16aec0274a8ed54dcb46b95c2e0538c13f0 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 23 Sep 2023 10:53:15 -0500 Subject: [PATCH 17/19] Allow calls to hasNewPumpEvents that do not replace all pending doses --- Loop/Managers/DeviceDataManager.swift | 10 ++++++++-- Loop/Managers/LoopDataManager.swift | 20 ------------------- .../Store Protocols/DoseStoreProtocol.swift | 4 ++-- 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index c44359e94c..b6cd35d3a6 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -1198,11 +1198,17 @@ extension DeviceDataManager: PumpManagerDelegate { setLastError(error: error) } - func pumpManager(_ pumpManager: PumpManager, hasNewPumpEvents events: [NewPumpEvent], lastReconciliation: Date?, completion: @escaping (_ error: Error?) -> Void) { + func pumpManager( + _ pumpManager: PumpManager, + hasNewPumpEvents events: [NewPumpEvent], + lastReconciliation: Date?, + replacePendingEvents: Bool, + completion: @escaping (_ error: Error?) -> Void) + { dispatchPrecondition(condition: .onQueue(queue)) log.default("PumpManager:%{public}@ hasNewPumpEvents (lastReconciliation = %{public}@)", String(describing: type(of: pumpManager)), String(describing: lastReconciliation)) - loopManager.addPumpEvents(events, lastReconciliation: lastReconciliation) { (error) in + doseStore.addPumpEvents(events, lastReconciliation: lastReconciliation, replacePendingEvents: replacePendingEvents) { (error) in if let error = error { self.log.error("Failed to addPumpEvents to DoseStore: %{public}@", String(describing: error)) } diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index d50fddb73f..b56cddd35b 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -741,26 +741,6 @@ extension LoopDataManager { } } - - /// Adds and stores new pump events - /// - /// - Parameters: - /// - events: The pump events to add - /// - completion: A closure called once upon completion - /// - lastReconciliation: The date that pump events were most recently reconciled against recorded pump history. Pump events are assumed to be reflective of delivery up until this point in time. If reservoir values are recorded after this time, they may be used to supplement event based delivery. - /// - error: An error explaining why the events could not be saved. - func addPumpEvents(_ events: [NewPumpEvent], lastReconciliation: Date?, completion: @escaping (_ error: DoseStore.DoseStoreError?) -> Void) { - doseStore.addPumpEvents(events, lastReconciliation: lastReconciliation) { (error) in - completion(error) - - self.dataAccessQueue.async { - if error == nil { - self.clearCachedInsulinEffects() - } - } - } - } - /// Logs a new external bolus insulin dose in the DoseStore and HealthKit /// /// - Parameters: diff --git a/Loop/Managers/Store Protocols/DoseStoreProtocol.swift b/Loop/Managers/Store Protocols/DoseStoreProtocol.swift index ebbd104da4..dd21ea2a1f 100644 --- a/Loop/Managers/Store Protocols/DoseStoreProtocol.swift +++ b/Loop/Managers/Store Protocols/DoseStoreProtocol.swift @@ -35,8 +35,8 @@ protocol DoseStoreProtocol: AnyObject { var pumpEventQueryAfterDate: Date { get } // MARK: dose management - func addPumpEvents(_ events: [NewPumpEvent], lastReconciliation: Date?, completion: @escaping (_ error: DoseStore.DoseStoreError?) -> Void) - + func addPumpEvents(_ events: [NewPumpEvent], lastReconciliation: Date?, replacePendingEvents: Bool, completion: @escaping (_ error: DoseStore.DoseStoreError?) -> Void) + func addReservoirValue(_ unitVolume: Double, at date: Date, completion: @escaping (_ value: ReservoirValue?, _ previousValue: ReservoirValue?, _ areStoredValuesContinuous: Bool, _ error: DoseStore.DoseStoreError?) -> Void) func getNormalizedDoseEntries(start: Date, end: Date?, completion: @escaping (_ result: DoseStoreResult<[DoseEntry]>) -> Void) From 55cf35a91a06e271f3fd87ebaae8324f84cea126 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Sat, 23 Sep 2023 11:06:50 -0500 Subject: [PATCH 18/19] Update tests --- LoopTests/Mock Stores/MockDoseStore.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/LoopTests/Mock Stores/MockDoseStore.swift b/LoopTests/Mock Stores/MockDoseStore.swift index dc358ec361..207596f31b 100644 --- a/LoopTests/Mock Stores/MockDoseStore.swift +++ b/LoopTests/Mock Stores/MockDoseStore.swift @@ -11,7 +11,6 @@ import LoopKit @testable import Loop class MockDoseStore: DoseStoreProtocol { - var doseHistory: [DoseEntry]? var sensitivitySchedule: InsulinSensitivitySchedule? @@ -55,7 +54,7 @@ class MockDoseStore: DoseStoreProtocol { var lastAddedPumpData: Date - func addPumpEvents(_ events: [NewPumpEvent], lastReconciliation: Date?, completion: @escaping (DoseStore.DoseStoreError?) -> Void) { + func addPumpEvents(_ events: [NewPumpEvent], lastReconciliation: Date?, replacePendingEvents: Bool, completion: @escaping (DoseStore.DoseStoreError?) -> Void) { completion(nil) } From cd1593413c037317bd1434668902681ab11f762d Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Tue, 26 Sep 2023 11:25:06 -0500 Subject: [PATCH 19/19] Fix merge --- Common/Models/PumpManager.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Common/Models/PumpManager.swift b/Common/Models/PumpManager.swift index 5ec574366c..d1a82fa2f4 100644 --- a/Common/Models/PumpManager.swift +++ b/Common/Models/PumpManager.swift @@ -12,13 +12,13 @@ import MockKit import MockKitUI let staticPumpManagersByIdentifier: [String: PumpManagerUI.Type] = [ - MockPumpManager.pluginIdentifier : MockPumpManager.self + MockPumpManager.managerIdentifier : MockPumpManager.self ] var availableStaticPumpManagers: [PumpManagerDescriptor] { if FeatureFlags.allowSimulators { return [ - PumpManagerDescriptor(identifier: MockPumpManager.pluginIdentifier, localizedTitle: MockPumpManager.localizedTitle) + PumpManagerDescriptor(identifier: MockPumpManager.managerIdentifier, localizedTitle: MockPumpManager.localizedTitle) ] } else { return []