Просмотр исходного кода

TidePool implementation : Config to log to the service

TidePool connexion to the service in Settings menu :
- add menu and views to display the views provided by packages
- add TidePoolManager as a injected service
- add BuildDetails.plist with the clientID required by TidePool to access authentication
- store the information in persistedStore

⚠️ : Do not send any data to the service

(cherry picked from commit a3b5989679ea17aa3f28daba697e8381beee71b1)
Pierre L 2 лет назад
Родитель
Сommit
1a4fd6a4cd

+ 8 - 0
BuildDetails.plist

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>TidepoolServiceClientId</key>
+	<string>diy-loop</string>
+</dict>
+</plist>

+ 8 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -283,6 +283,8 @@
 		CD78BB94E43B249D60CC1A1B /* NotificationsConfigRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22963BD06A9C83959D4914E4 /* NotificationsConfigRootView.swift */; };
 		CE1F6DD92BADF4620064EB8D /* PluginManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */; };
 		CE1F6DDB2BAE08B60064EB8D /* TidepoolManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F6DDA2BAE08B60064EB8D /* TidepoolManager.swift */; };
+		CE1F6DE72BAF1A180064EB8D /* BuildDetails.plist in Resources */ = {isa = PBXBuildFile; fileRef = CE1F6DE62BAF1A180064EB8D /* BuildDetails.plist */; };
+		CE1F6DE92BAF37C90064EB8D /* TidePoolConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1F6DE82BAF37C90064EB8D /* TidePoolConfigView.swift */; };
 		CE2FAD3A297D93F0001A872C /* BloodGlucoseExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */; };
 		CE48C86428CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48C86328CA69D5007C0598 /* OmniBLEPumpManagerExtensions.swift */; };
 		CE48C86628CA6B48007C0598 /* OmniPodManagerExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE48C86528CA6B48007C0598 /* OmniPodManagerExtensions.swift */; };
@@ -770,6 +772,8 @@
 		CC6C406D2ACDD69E009B8058 /* RawFetchedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawFetchedProfile.swift; sourceTree = "<group>"; };
 		CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManagerTests.swift; sourceTree = "<group>"; };
 		CE1F6DDA2BAE08B60064EB8D /* TidepoolManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TidepoolManager.swift; sourceTree = "<group>"; };
+		CE1F6DE62BAF1A180064EB8D /* BuildDetails.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = BuildDetails.plist; sourceTree = "<group>"; };
+		CE1F6DE82BAF37C90064EB8D /* TidePoolConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TidePoolConfigView.swift; sourceTree = "<group>"; };
 		CE2FAD39297D93F0001A872C /* BloodGlucoseExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloodGlucoseExtensions.swift; sourceTree = "<group>"; };
 		CE398D012977349800DF218F /* CryptoKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CryptoKit.framework; path = System/Library/Frameworks/CryptoKit.framework; sourceTree = SDKROOT; };
 		CE398D17297C9EE800DF218F /* G7SensorKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = G7SensorKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -1210,6 +1214,7 @@
 			isa = PBXGroup;
 			children = (
 				3811DE3C25C9D4A100A708ED /* SettingsRootView.swift */,
+				CE1F6DE82BAF37C90064EB8D /* TidePoolConfigView.swift */,
 			);
 			path = View;
 			sourceTree = "<group>";
@@ -1448,6 +1453,7 @@
 		388E594F25AD948C0019842D = {
 			isa = PBXGroup;
 			children = (
+				CE1F6DE62BAF1A180064EB8D /* BuildDetails.plist */,
 				FEFA5C0D299F810B00765C17 /* Core_Data.xcdatamodeld */,
 				38F3783A2613555C009DB701 /* Config.xcconfig */,
 				3818AA42274BBC1100843DB3 /* ConfigOverride.xcconfig */,
@@ -2308,6 +2314,7 @@
 			buildActionMask = 2147483647;
 			files = (
 				198377D2266BFFF6004DE65E /* Localizable.strings in Resources */,
+				CE1F6DE72BAF1A180064EB8D /* BuildDetails.plist in Resources */,
 				38DF178D27733E6800B3528F /* snow.sks in Resources */,
 				388E597225AD9CF10019842D /* json in Resources */,
 				38DF178E27733E6800B3528F /* Assets.xcassets in Resources */,
@@ -2503,6 +2510,7 @@
 				38569347270B5DFB0002C50D /* CGMType.swift in Sources */,
 				3821ED4C25DD18BA00BC42AD /* Constants.swift in Sources */,
 				384E803425C385E60086DB71 /* JavaScriptWorker.swift in Sources */,
+				CE1F6DE92BAF37C90064EB8D /* TidePoolConfigView.swift in Sources */,
 				3811DE5D25C9D4D500A708ED /* Publisher.swift in Sources */,
 				E00EEC0727368630002FF094 /* APSAssembly.swift in Sources */,
 				38B4F3AF25E2979F00E76A18 /* IndexedCollection.swift in Sources */,

+ 0 - 4
FreeAPS/Resources/Info.plist

@@ -113,10 +113,6 @@
 		<string>UIInterfaceOrientationPortrait</string>
 		<string>UIInterfaceOrientationPortraitUpsideDown</string>
 	</array>
-	<key>NSCalendarsFullAccessUsageDescription</key>
-	<string>To create events with BG reading values, so that they can be viewed on Apple Watch and CarPlay</string>
-	<key>LSApplicationCategoryType</key>
-	<string></string>
 	<key>UISupportedInterfaceOrientations~ipad</key>
 	<array>
 		<string>UIInterfaceOrientationPortrait</string>

+ 1 - 0
FreeAPS/Sources/Assemblies/NetworkAssembly.swift

@@ -8,5 +8,6 @@ final class NetworkAssembly: Assembly {
         }
 
         container.register(NightscoutManager.self) { r in BaseNightscoutManager(resolver: r) }
+        container.register(TidePoolManager.self) { r in BaseTidePoolManager(resolver: r) }
     }
 }

+ 3 - 1
FreeAPS/Sources/Modules/Settings/SettingsProvider.swift

@@ -1,3 +1,5 @@
 extension Settings {
-    final class Provider: BaseProvider, SettingsProvider {}
+    final class Provider: BaseProvider, SettingsProvider {
+        @Injected() var tidePoolManager: TidePoolManager!
+    }
 }

+ 25 - 0
FreeAPS/Sources/Modules/Settings/SettingsStateModel.swift

@@ -1,3 +1,5 @@
+import LoopKit
+import LoopKitUI
 import SwiftUI
 
 extension Settings {
@@ -5,10 +7,13 @@ extension Settings {
         @Injected() private var broadcaster: Broadcaster!
         @Injected() private var fileManager: FileManager!
         @Injected() private var nightscoutManager: NightscoutManager!
+        @Injected() var pluginManager: PluginManager!
 
         @Published var closedLoop = false
         @Published var debugOptions = false
         @Published var animatedBackground = false
+        @Published var serviceUIType: ServiceUI.Type?
+        @Published var setupTidePool = false
 
         private(set) var buildNumber = ""
         private(set) var versionNumber = ""
@@ -49,6 +54,8 @@ extension Settings {
             copyrightNotice = Bundle.main.infoDictionary?["NSHumanReadableCopyright"] as? String ?? ""
 
             subscribeSetting(\.animatedBackground, on: $animatedBackground) { animatedBackground = $0 }
+
+            serviceUIType = pluginManager.getServiceTypeByIdentifier("TidepoolService")
         }
 
         func logItems() -> [URL] {
@@ -82,3 +89,21 @@ extension Settings.StateModel: SettingsObserver {
         debugOptions = settings.debugOptions
     }
 }
+
+extension Settings.StateModel: ServiceOnboardingDelegate {
+    func serviceOnboarding(didCreateService service: Service) {
+        debug(.nightscout, "Service with identifier \(service.pluginIdentifier) created")
+        provider.tidePoolManager.addTidePoolService(service: service)
+    }
+
+    func serviceOnboarding(didOnboardService service: Service) {
+        precondition(service.isOnboarded)
+        debug(.nightscout, "Service with identifier \(service.pluginIdentifier) onboarded")
+    }
+}
+
+extension Settings.StateModel: CompletionDelegate {
+    func completionNotifyingDidComplete(_: CompletionNotifying) {
+        setupTidePool = false
+    }
+}

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

@@ -1,4 +1,6 @@
 import HealthKit
+import LoopKit
+import LoopKitUI
 import SwiftUI
 import Swinject
 
@@ -34,6 +36,11 @@ extension Settings {
 
                 Section {
                     Text("Nightscout").navigationLink(to: .nighscoutConfig, from: self)
+
+                    Text("TidePool")
+                        .onTapGesture {
+                            state.setupTidePool = true
+                        }
                     if HKHealthStore.isHealthDataAvailable() {
                         Text("Apple Health").navigationLink(to: .healthkit, from: self)
                     }
@@ -131,6 +138,26 @@ extension Settings {
             .sheet(isPresented: $showShareSheet) {
                 ShareSheet(activityItems: state.logItems())
             }
+            .sheet(isPresented: $state.setupTidePool) {
+                if let serviceUIType = state.serviceUIType,
+                   let pluginHost = state.provider.tidePoolManager.getTidePoolPluginHost()
+                {
+                    if let serviceUI = state.provider.tidePoolManager.getTidePoolServiceUI() {
+                        TidePoolSettingsView(
+                            serviceUI: serviceUI,
+                            serviceOnBoardDelegate: self.state,
+                            serviceDelegate: self.state
+                        )
+                    } else {
+                        TidePoolSetupView(
+                            serviceUIType: serviceUIType,
+                            pluginHost: pluginHost,
+                            serviceOnBoardDelegate: self.state,
+                            serviceDelegate: self.state
+                        )
+                    }
+                }
+            }
             .onAppear(perform: configureView)
             .navigationTitle("Settings")
             .navigationBarItems(leading: Button("Close", action: state.hideSettingsModal))

+ 45 - 0
FreeAPS/Sources/Modules/Settings/View/TidePoolConfigView.swift

@@ -0,0 +1,45 @@
+import Foundation
+import LoopKit
+import LoopKitUI
+import SwiftUI
+
+struct TidePoolSetupView: UIViewControllerRepresentable {
+    let serviceUIType: ServiceUI.Type
+    let pluginHost: PluginHost
+    let serviceOnBoardDelegate: ServiceOnboardingDelegate
+    let serviceDelegate: CompletionDelegate
+
+    func makeUIViewController(context _: UIViewControllerRepresentableContext<TidePoolSetupView>) -> UIViewController {
+        let result = serviceUIType.setupViewController(
+            colorPalette: .default,
+            pluginHost: pluginHost
+        )
+        switch result {
+        case let .createdAndOnboarded(serviceUI):
+            serviceOnBoardDelegate.serviceOnboarding(didCreateService: serviceUI)
+            serviceOnBoardDelegate.serviceOnboarding(didOnboardService: serviceUI)
+            return UIViewController()
+        case var .userInteractionRequired(setupViewControllerUI):
+            setupViewControllerUI.serviceOnboardingDelegate = serviceOnBoardDelegate
+            setupViewControllerUI.completionDelegate = serviceDelegate
+            return setupViewControllerUI
+        }
+    }
+
+    func updateUIViewController(_: UIViewController, context _: UIViewControllerRepresentableContext<TidePoolSetupView>) {}
+}
+
+struct TidePoolSettingsView: UIViewControllerRepresentable {
+    let serviceUI: ServiceUI
+    let serviceOnBoardDelegate: ServiceOnboardingDelegate
+    let serviceDelegate: CompletionDelegate?
+
+    func makeUIViewController(context _: UIViewControllerRepresentableContext<TidePoolSettingsView>) -> UIViewController {
+        var vc = serviceUI.settingsViewController(colorPalette: .default)
+        vc.completionDelegate = serviceDelegate
+        vc.serviceOnboardingDelegate = serviceOnBoardDelegate
+        return vc
+    }
+
+    func updateUIViewController(_: UIViewController, context _: UIViewControllerRepresentableContext<TidePoolSettingsView>) {}
+}

+ 106 - 1
FreeAPS/Sources/Services/Network/TidepoolManager.swift

@@ -1,10 +1,13 @@
 import Combine
 import Foundation
+import LoopKit
 import LoopKitUI
 import Swinject
-import UIKit
 
 protocol TidePoolManager {
+    func addTidePoolService(service: Service)
+    func getTidePoolServiceUI() -> ServiceUI?
+    func getTidePoolPluginHost() -> PluginHost?
     func deleteCarbs(at date: Date, isFPU: Bool?, fpuID: String?, syncID: String)
     func deleteInsulin(at date: Date)
     func uploadStatus()
@@ -16,17 +19,66 @@ protocol TidePoolManager {
 
 final class BaseTidePoolManager: TidePoolManager, Injectable {
     @Injected() private var broadcaster: Broadcaster!
+    @Injected() private var pluginManager: PluginManager!
 
     private let processQueue = DispatchQueue(label: "BaseNetworkManager.processQueue")
     private var ping: TimeInterval?
+    private var tidePoolService: RemoteDataService? {
+        didSet {
+            if let tidePoolService = tidePoolService {
+                rawTidePoolManager = tidePoolService.rawValue
+            }
+        }
+    }
 
     private var lifetime = Lifetime()
 
+    @PersistedProperty(key: "TidePoolState") var rawTidePoolManager: Service.RawValue?
+
     init(resolver: Resolver) {
         injectServices(resolver)
+        loadTidePoolManager()
         subscribe()
     }
 
+    /// load the TidePool Remote Data Service if available
+    fileprivate func loadTidePoolManager() {
+        if let rawTidePoolManager = rawTidePoolManager {
+            tidePoolService = tidePoolServiceFromRaw(rawTidePoolManager)
+            tidePoolService?.serviceDelegate = self
+            tidePoolService?.stateDelegate = self
+        }
+    }
+
+    /// allows to acces to tidePoolService as a simple ServiceUI
+    func getTidePoolServiceUI() -> ServiceUI? {
+        if let tidePoolService = self.tidePoolService {
+            return tidePoolService as! any ServiceUI as ServiceUI
+        } else {
+            return nil
+        }
+    }
+
+    func getTidePoolPluginHost() -> PluginHost? {
+        self as PluginHost
+    }
+
+    func addTidePoolService(service: Service) {
+        tidePoolService = service as! any RemoteDataService as RemoteDataService
+    }
+
+    /// load the TidePool Remote Data Service from raw storage
+    private func tidePoolServiceFromRaw(_ rawValue: [String: Any]) -> RemoteDataService? {
+        guard let rawState = rawValue["state"] as? Service.RawStateValue,
+              let serviceType = pluginManager.getServiceTypeByIdentifier("TidepoolService")
+        else {
+            return nil
+        }
+        if let service = serviceType.init(rawState: rawState) {
+            return service as! any RemoteDataService as RemoteDataService
+        } else { return nil }
+    }
+
     private func subscribe() {
         broadcaster.register(PumpHistoryObserver.self, observer: self)
         broadcaster.register(CarbsObserver.self, observer: self)
@@ -63,3 +115,56 @@ extension BaseTidePoolManager: CarbsObserver {
 extension BaseTidePoolManager: TempTargetsObserver {
     func tempTargetsDidUpdate(_: [TempTarget]) {}
 }
+
+extension BaseTidePoolManager: ServiceDelegate {
+    var hostIdentifier: String {
+        "com.loopkit.Loop" // To check
+    }
+
+    var hostVersion: String {
+        var semanticVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as! String
+
+        while semanticVersion.split(separator: ".").count < 3 {
+            semanticVersion += ".0"
+        }
+
+        semanticVersion += "+\(Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as! String)"
+
+        return semanticVersion
+    }
+
+    func issueAlert(_: LoopKit.Alert) {}
+
+    func retractAlert(identifier _: LoopKit.Alert.Identifier) {}
+
+    func enactRemoteOverride(name _: String, durationTime _: TimeInterval?, remoteAddress _: String) async throws {}
+
+    func cancelRemoteOverride() async throws {}
+
+    func deliverRemoteCarbs(
+        amountInGrams _: Double,
+        absorptionTime _: TimeInterval?,
+        foodType _: String?,
+        startDate _: Date?
+    ) async throws {}
+
+    func deliverRemoteBolus(amountInUnits _: Double) async throws {}
+}
+
+extension BaseTidePoolManager: StatefulPluggableDelegate {
+    func pluginDidUpdateState(_: LoopKit.StatefulPluggable) {}
+
+    func pluginWantsDeletion(_: LoopKit.StatefulPluggable) {}
+}
+
+// Service extension for rawValue
+extension Service {
+    typealias RawValue = [String: Any]
+
+    var rawValue: RawValue {
+        [
+            "serviceIdentifier": pluginIdentifier,
+            "state": rawState
+        ]
+    }
+}