diff --git a/CHANGELOG.md b/CHANGELOG.md index e14f0ef..ae46b06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## [2.0.1] - 17.04.2022 + +* Add workout route as query method, remove it as event stream +* Minor fixes in the code + ## [2.0.0] - 09.03.2022 * A way not to provide a predicate where it can be omitted diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 6441c57..98997e5 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -2,10 +2,10 @@ PODS: - Flutter (1.0.0) - flutter_local_notifications (0.0.1): - Flutter - - health_kit_reporter (1.5.1): + - health_kit_reporter (1.5.2): - Flutter - HealthKitReporter - - HealthKitReporter (1.6.2) + - HealthKitReporter (1.6.3) DEPENDENCIES: - Flutter (from `Flutter`) @@ -27,9 +27,9 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 - health_kit_reporter: 60f4e901356f465779dc4a9a315a045721c52f78 - HealthKitReporter: e17195a40b8245d8bfa1d80e52bc8297eae9d3be + health_kit_reporter: 6da8c20414d6a53cf2babcfd0314ea05a943bd3e + HealthKitReporter: 42ebaa50989c4e4f6171b2caa3388c53519bea33 PODFILE CHECKSUM: a75497545d4391e2d394c3668e20cfb1c2bbd4aa -COCOAPODS: 1.11.0 +COCOAPODS: 1.11.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index d47536b..2bb2ba4 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -41,6 +41,8 @@ 586D67CD4E75BA279D8761E9 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 788ED29B2803689500B4EDFC /* Info-Release.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "Info-Release.plist"; sourceTree = ""; }; + 789DA6722802F98A005C51A4 /* RunnerRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerRelease.entitlements; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -48,7 +50,7 @@ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info-Debug.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-Debug.plist"; sourceTree = ""; }; 9EEFACBE630AB7D54BBDC1AE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; D20DBD967B4CDA1DD77298C0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -100,11 +102,13 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 789DA6722802F98A005C51A4 /* RunnerRelease.entitlements */, 1726D0AA255C690100A8CD8B /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, + 97C147021CF9000F007C117D /* Info-Debug.plist */, + 788ED29B2803689500B4EDFC /* Info-Release.plist */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, @@ -163,7 +167,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1200; + LastUpgradeCheck = 1300; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -365,6 +369,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = MAVP5V2N7V; ENABLE_BITCODE = NO; @@ -372,7 +378,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -384,6 +390,7 @@ MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.kvs.healthKitReporterExample; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -506,6 +513,8 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = MAVP5V2N7V; ENABLE_BITCODE = NO; @@ -513,7 +522,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -525,6 +534,7 @@ MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.kvs.healthKitReporterExample; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -538,7 +548,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = MAVP5V2N7V; ENABLE_BITCODE = NO; @@ -546,7 +558,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_FILE = "Runner/Info-$(CONFIGURATION).plist"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -558,6 +570,7 @@ MARKETING_VERSION = 1.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.kvs.healthKitReporterExample; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; diff --git a/example/ios/Runner/Info-Debug.plist b/example/ios/Runner/Info-Debug.plist new file mode 100644 index 0000000..1a9bdaf --- /dev/null +++ b/example/ios/Runner/Info-Debug.plist @@ -0,0 +1,57 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + health_kit_reporter_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + NSBonjourServices + + _dartobservatory._tcp + + NSHealthShareUsageDescription + Share your data with our super cool app + NSHealthUpdateUsageDescription + This super cool app wants to write some data + UIBackgroundModes + + fetch + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info-Release.plist similarity index 100% rename from example/ios/Runner/Info.plist rename to example/ios/Runner/Info-Release.plist diff --git a/example/ios/Runner/RunnerRelease.entitlements b/example/ios/Runner/RunnerRelease.entitlements new file mode 100644 index 0000000..dab226c --- /dev/null +++ b/example/ios/Runner/RunnerRelease.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.developer.healthkit + + com.apple.developer.healthkit.access + + com.apple.developer.healthkit.background-delivery + + + diff --git a/example/lib/main.dart b/example/lib/main.dart index fc44fd4..11a2b1c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -162,6 +162,11 @@ class _MyAppState extends State { queryHeartbeatSeries(); }, child: Text('heartbeatSeriesQuery')), + ElevatedButton( + onPressed: () { + workoutRouteQuery(); + }, + child: Text('workoutRouteQuery')), ElevatedButton( onPressed: () { querySources(); @@ -237,11 +242,6 @@ class _MyAppState extends State { statisticsCollectionQuery(); }, child: Text('statisticsCollectionQuery')), - ElevatedButton( - onPressed: () { - workoutRouteQuery(); - }, - child: Text('workoutRouteQuery')), ], ), Column( @@ -456,6 +456,15 @@ class _MyAppState extends State { } } + void workoutRouteQuery() async { + try { + final series = await HealthKitReporter.workoutRouteQuery(_predicate); + print('workoutRoutes: ${series.map((e) => e.map).toList()}'); + } catch (e) { + print(e); + } + } + void queryCharacteristics() async { try { final characteristics = await HealthKitReporter.characteristicsQuery(); @@ -509,15 +518,6 @@ class _MyAppState extends State { } } - void workoutRouteQuery() { - final sub = - HealthKitReporter.workoutRouteQuery(_predicate, onUpdate: (serie) { - print('Updates for workoutRouteQuery'); - print(serie.map); - }); - print('workoutRouteQuery: $sub'); - } - void anchoredObjectQuery(List identifiers) { final sub = HealthKitReporter.anchoredObjectQuery(identifiers, _predicate, onUpdate: (samples, deletedObjects) { diff --git a/example/pubspec.lock b/example/pubspec.lock index 6842867..ea840ba 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.1" + version: "2.8.2" boolean_selector: dependency: transitive description: @@ -21,7 +21,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: @@ -49,7 +49,7 @@ packages: name: cupertino_icons url: "https://pub.dartlang.org" source: hosted - version: "1.0.2" + version: "1.0.4" fake_async: dependency: transitive description: @@ -68,14 +68,14 @@ packages: name: flutter_local_notifications url: "https://pub.dartlang.org" source: hosted - version: "5.0.0-nullsafety.1" + version: "5.0.0+4" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "3.0.0-nullsafety.4" + version: "3.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -87,14 +87,21 @@ packages: path: ".." relative: true source: path - version: "1.5.1" + version: "2.0.0" matcher: dependency: transitive description: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" meta: dependency: transitive description: @@ -115,14 +122,14 @@ packages: name: platform url: "https://pub.dartlang.org" source: hosted - version: "3.0.0" + version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.2" sky_engine: dependency: transitive description: flutter @@ -169,14 +176,14 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.8" timezone: dependency: transitive description: name: timezone url: "https://pub.dartlang.org" source: hosted - version: "0.7.0-nullsafety.0" + version: "0.7.0" typed_data: dependency: transitive description: @@ -190,7 +197,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.14.0 <3.0.0" flutter: ">=1.20.0" diff --git a/ios/Classes/EventChannel.swift b/ios/Classes/EventChannel.swift index 1bc15cb..fa08c2e 100644 --- a/ios/Classes/EventChannel.swift +++ b/ios/Classes/EventChannel.swift @@ -12,7 +12,6 @@ enum EventChannel: String, CaseIterable { case statisticsCollectionQuery = "health_kit_reporter_event_channel_statistics_collection_query" case activitySummaryQuery = "health_kit_reporter_event_channel_query_activity_summary" case anchoredObjectQuery = "health_kit_reporter_event_channel_anchored_object_query" - case workoutRouteQuery = "health_kit_reporter_event_channel_workout_route_query" func combinedWith(identifier: String) -> String { "\(self.rawValue)_\(identifier)" diff --git a/ios/Classes/Extensions+SwiftHealthKitReporterPlugin.swift b/ios/Classes/Extensions+SwiftHealthKitReporterPlugin.swift new file mode 100644 index 0000000..2583f6a --- /dev/null +++ b/ios/Classes/Extensions+SwiftHealthKitReporterPlugin.swift @@ -0,0 +1,1497 @@ +// +// Extensions+SwiftHealthKitReporterPlugin.swift +// health_kit_reporter +// +// Created by Victor Kachalov on 17.04.22. +// + +import Flutter +import HealthKitReporter + +// MARK: - MethodCall +extension SwiftHealthKitReporterPlugin { + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let reporter = self.reporter else { + result( + FlutterError( + code: className, + message: "Reporter is nil", + details: nil + ) + ) + return + } + guard let method = Method(rawValue: call.method) else { + result( + FlutterError( + code: className, + message: "Please check the method", + details: "Provided method: \(call.method)" + ) + ) + return + } + switch method { + case .requestAuthorization: + guard let arguments = call.arguments as? [String: [String]] else { + throwNoArgumentsError(result: result) + return + } + requestAuthorization( + reporter: reporter, + arguments: arguments, + result: result + ) + case .preferredUnits: + guard let arguments = call.arguments as? [String] else { + throwNoArgumentsError(result: result) + return + } + preferredUnits( + reporter: reporter, + arguments: arguments, + result: result + ) + case .characteristicsQuery: + characteristicsQuery(reporter: reporter, result: result) + case .quantityQuery: + guard let arguments = call.arguments as? [String: Any] else { + throwNoArgumentsError(result: result) + return + } + quantityQuery( + reporter: reporter, + arguments: arguments, + result: result + ) + case .categoryQuery: + guard let arguments = call.arguments as? [String: Any] else { + throwNoArgumentsError(result: result) + return + } + categoryQuery( + reporter: reporter, + arguments: arguments, + result: result + ) + case .workoutQuery: + guard let arguments = call.arguments as? [String: Double] else { + throwNoArgumentsError(result: result) + return + } + workoutQuery( + reporter: reporter, + arguments: arguments, + result: result + ) + case .electrocardiogramQuery: + guard let arguments = call.arguments as? [String: Double] else { + throwNoArgumentsError(result: result) + return + } + electrocardiogramQuery( + reporter: reporter, + arguments: arguments, + result: result + ) + case .sampleQuery: + guard let arguments = call.arguments as? [String: Any] else { + throwNoArgumentsError(result: result) + return + } + sampleQuery( + reporter: reporter, + arguments: arguments, + result: result + ) + case .statisticsQuery: + guard let arguments = call.arguments as? [String: Any] else { + throwNoArgumentsError(result: result) + return + } + statisticsQuery( + reporter: reporter, + arguments: arguments, + result: result + ) + case .heartbeatSeriesQuery: + guard let arguments = call.arguments as? [String: Double] else { + throwNoArgumentsError(result: result) + return + } + heartbeatSeriesQuery( + reporter: reporter, + arguments: arguments, + result: result + ) + case .workoutRouteQuery: + guard let arguments = call.arguments as? [String: Double] else { + throwNoArgumentsError(result: result) + return + } + workoutRouteQuery( + reporter: reporter, + arguments: arguments, + result: result + ) + case .queryActivitySummary: + guard let arguments = call.arguments as? [String: Double] else { + throwNoArgumentsError(result: result) + return + } + queryActivitySummary( + reporter: reporter, + arguments: arguments, + result: result + ) + case .sourceQuery: + guard let arguments = call.arguments as? [String: Any] else { + throwNoArgumentsError(result: result) + return + } + sourceQuery( + reporter: reporter, + arguments: arguments, + result: result + ) + case .correlationQuery: + guard let arguments = call.arguments as? [String: Any] else { + throwNoArgumentsError(result: result) + return + } + correlationQuery( + reporter: reporter, + arguments: arguments, + result: result + ) + case .enableBackgroundDelivery: + guard let arguments = call.arguments as? [String: Any] else { + throwNoArgumentsError(result: result) + return + } + enableBackgroundDelivery( + reporter: reporter, + arguments: arguments, + result: result + ) + case .disableAllBackgroundDelivery: + disableAllBackgroundDelivery( + reporter: reporter, + result: result + ) + case .disableBackgroundDelivery: + guard let arguments = call.arguments as? [String: String] else { + throwNoArgumentsError(result: result) + return + } + disableBackgroundDelivery( + reporter: reporter, + arguments: arguments, + result: result + ) + case .startWatchApp: + guard let arguments = call.arguments as? [String: Any] else { + throwNoArgumentsError(result: result) + return + } + startWatchApp( + reporter: reporter, + arguments: arguments, + result: result + ) + case .isAuthorizedToWrite: + guard let arguments = call.arguments as? [String: String] else { + throwNoArgumentsError(result: result) + return + } + isAuthorizedToWrite( + reporter: reporter, + arguments: arguments, + result: result + ) + case .addCategory: + guard let arguments = call.arguments as? [String: Any] else { + throwNoArgumentsError(result: result) + return + } + addCategory( + reporter: reporter, + arguments: arguments, + result: result + ) + case .addQuantity: + guard let arguments = call.arguments as? [String: Any] else { + throwNoArgumentsError(result: result) + return + } + addQuantity( + reporter: reporter, + arguments: arguments, + result: result + ) + case .delete: + guard let arguments = call.arguments as? [String: Any] else { + throwNoArgumentsError(result: result) + return + } + delete( + reporter: reporter, + arguments: arguments, + result: result + ) + case .deleteObjects: + guard let arguments = call.arguments as? [String: Any] else { + throwNoArgumentsError(result: result) + return + } + deleteObjects( + reporter: reporter, + arguments: arguments, + result: result + ) + case .save: + guard let arguments = call.arguments as? [String: Any] else { + throwNoArgumentsError(result: result) + return + } + save( + reporter: reporter, + arguments: arguments, + result: result + ) + } + } +} +// MARK: - Method Call methods +extension SwiftHealthKitReporterPlugin { + private func requestAuthorization( + reporter: HealthKitReporter, + arguments: [String: [String]], + result: @escaping FlutterResult + ) { + let toReadArguments = arguments["toRead"] + let toWriteArguments = arguments["toWrite"] + reporter.manager.requestAuthorization( + toRead: toReadArguments != nil + ? parse(arguments: toReadArguments!) + : [], + toWrite: toWriteArguments != nil + ? parse(arguments: toWriteArguments!) + : [] + ) { (success, error) in + guard error == nil else { + result( + FlutterError( + code: "RequestAuthorization", + message: "Error in displaying Apple Health permission screen", + details: error.debugDescription + ) + ) + return + } + result(success) + } + } + private func preferredUnits( + reporter: HealthKitReporter, + arguments: [String], + result: @escaping FlutterResult + ) { + var quantityTypes: [QuantityType] = [] + for argument in arguments { + guard let quantityType = try? QuantityType.make(from: argument) else { + result( + FlutterError( + code: className, + message: "Error in parsing quantity type", + details: "Identifier unknown: \(argument)" + ) + ) + return + } + quantityTypes.append(quantityType) + } + reporter.manager.preferredUnits( + for: quantityTypes + ) { (preferredUnits, error) in + guard error == nil else { + result( + FlutterError( + code: "PreferredUnits", + message: "Error in getting preferred units", + details: error.debugDescription + ) + ) + return + } + do { + result(try preferredUnits.encoded()) + } catch { + result( + FlutterError( + code: "PreferredUnits", + message: "Error in json encoding of preferred units: \(preferredUnits)", + details: error + ) + ) + } + } + } + private func characteristicsQuery( + reporter: HealthKitReporter, + result: FlutterResult + ) { + do { + let characteristics = reporter.reader.characteristics() + result(try characteristics.encoded()) + } catch { + throwPlatformError(result: result, error: error) + } + } + private func quantityQuery( + reporter: HealthKitReporter, + arguments: [String: Any], + result: @escaping FlutterResult + ) { + guard + let identifier = arguments["identifier"] as? String, + let unit = arguments["unit"] as? String, + let startTimestamp = arguments["startTimestamp"] as? Double, + let endTimestamp = arguments["endTimestamp"] as? Double + else { + throwParsingArgumentsError(result: result, arguments: arguments) + return + } + do { + let type = try QuantityType.make(from: identifier) + let predicate = NSPredicate.samplesPredicate( + startDate: Date.make(from: startTimestamp), + endDate: Date.make(from: endTimestamp) + ) + let query = try reporter.reader.quantityQuery( + type: type, + unit: unit, + predicate: predicate + ) { (quantities, error) in + guard error == nil else { + result( + FlutterError( + code: "QuantityQuery", + message: "Error in quantityQuery for identifier: \(identifier)", + details: error.debugDescription + ) + ) + return + } + do { + result(try quantities.encoded()) + } catch { + result( + FlutterError( + code: "QuantityQuery", + message: "Error in json encoding of quantities: \(quantities)", + details: error + ) + ) + } + } + reporter.manager.executeQuery(query) + } catch { + throwPlatformError(result: result, error: error) + } + } + private func categoryQuery( + reporter: HealthKitReporter, + arguments: [String: Any], + result: @escaping FlutterResult + ) { + guard + let identifier = arguments["identifier"] as? String, + let startTimestamp = arguments["startTimestamp"] as? Double, + let endTimestamp = arguments["endTimestamp"] as? Double + else { + throwParsingArgumentsError(result: result, arguments: arguments) + return + } + do { + let type = try CategoryType.make(from: identifier) + let predicate = NSPredicate.samplesPredicate( + startDate: Date.make(from: startTimestamp), + endDate: Date.make(from: endTimestamp) + ) + let query = try reporter.reader.categoryQuery( + type: type, + predicate: predicate + ) { (categories, error) in + guard error == nil else { + result( + FlutterError( + code: "CategoryQuery", + message: "Error in categoryQuery for identifier: \(identifier)", + details: error.debugDescription + ) + ) + return + } + do { + result(try categories.encoded()) + } catch { + result( + FlutterError( + code: "CategoryQuery", + message: "Error in json encoding of categories: \(categories)", + details: error + ) + ) + } + } + reporter.manager.executeQuery(query) + } catch { + throwPlatformError(result: result, error: error) + } + } + private func workoutQuery( + reporter: HealthKitReporter, + arguments: [String: Double], + result: @escaping FlutterResult + ) { + guard + let startTimestamp = arguments["startTimestamp"], + let endTimestamp = arguments["endTimestamp"] + else { + throwParsingArgumentsError(result: result, arguments: arguments) + return + } + let predicate = NSPredicate.samplesPredicate( + startDate: Date.make(from: startTimestamp), + endDate: Date.make(from: endTimestamp) + ) + do { + let query = try reporter.reader.workoutQuery( + predicate: predicate + ) { (workouts, error) in + guard error == nil else { + result( + FlutterError( + code: "WorkoutQuery", + message: "Error in workoutQuery", + details: error.debugDescription + ) + ) + return + } + do { + result(try workouts.encoded()) + } catch { + result( + FlutterError( + code: "WorkoutQuery", + message: "Error in json encoding of workouts: \(workouts)", + details: error + ) + ) + } + } + reporter.manager.executeQuery(query) + } catch { + result( + FlutterError( + code: className, + message: "Error in workoutQuery initialization", + details: error + ) + ) + } + } + private func electrocardiogramQuery( + reporter: HealthKitReporter, + arguments: [String: Double], + result: @escaping FlutterResult + ) { + guard + let startTimestamp = arguments["startTimestamp"], + let endTimestamp = arguments["endTimestamp"] + else { + throwParsingArgumentsError(result: result, arguments: arguments) + return + } + let predicate = NSPredicate.samplesPredicate( + startDate: Date.make(from: startTimestamp), + endDate: Date.make(from: endTimestamp) + ) + if #available(iOS 14.0, *) { + do { + let query = try reporter.reader.electrocardiogramQuery( + predicate: predicate + ) { (electrocardiograms, error) in + guard error == nil else { + result( + FlutterError( + code: "ElectrocardiogramQuery", + message: "Error in electrocardiogramQuery", + details: error.debugDescription + ) + ) + return + } + do { + result(try electrocardiograms.encoded()) + } catch { + result( + FlutterError( + code: "ElectrocardiogramQuery", + message: "Error in json encoding of electrocardiograms: \(electrocardiograms)", + details: error + ) + ) + } + } + reporter.manager.executeQuery(query) + } catch { + result( + FlutterError( + code: className, + message: "Error electrocardiograms query initialization", + details: error + ) + ) + } + } else { + result( + FlutterError( + code: className, + message: "Error in platform version.", + details: "Electrocardiogram query is available for iOS 14." + ) + ) + } + } + private func sampleQuery( + reporter: HealthKitReporter, + arguments: [String: Any], + result: @escaping FlutterResult + ) { + guard + let identifier = arguments["identifier"] as? String, + let startTimestamp = arguments["startTimestamp"] as? Double, + let endTimestamp = arguments["endTimestamp"] as? Double + else { + throwParsingArgumentsError(result: result, arguments: arguments) + return + } + guard let type = identifier.objectType as? SampleType else { + result( + FlutterError( + code: className, + message: "Error in parsing identifier: \(identifier)", + details: "Invalid identifier for any existing ObjectType" + ) + ) + return + } + let predicate = NSPredicate.samplesPredicate( + startDate: Date.make(from: startTimestamp), + endDate: Date.make(from: endTimestamp) + ) + do { + let query = try reporter.reader.sampleQuery( + type: type, + predicate: predicate + ) { (_, samples, error) in + guard error == nil else { + result( + FlutterError( + code: "SampleQuery", + message: "Error in sampleQuery", + details: error.debugDescription + ) + ) + return + } + var jsonArray: [String] = [] + for sample in samples { + do { + let encoded = try sample.encoded() + jsonArray.append(encoded) + } catch { + result( + FlutterError( + code: "SampleQuery", + message: "Error in json encoding of sample: \(sample). Continue", + details: error + ) + ) + continue + } + } + result(jsonArray) + } + reporter.manager.executeQuery(query) + } catch { + result( + FlutterError( + code: className, + message: "Error in sampleQuery initialization", + details: error + ) + ) + } + } + private func statisticsQuery( + reporter: HealthKitReporter, + arguments: [String: Any], + result: @escaping FlutterResult + ) { + guard + let identifier = arguments["identifier"] as? String, + let unit = arguments["unit"] as? String, + let startTimestamp = arguments["startTimestamp"] as? Double, + let endTimestamp = arguments["endTimestamp"] as? Double + else { + throwParsingArgumentsError(result: result, arguments: arguments) + return + } + guard let type = identifier.objectType as? QuantityType else { + result( + FlutterError( + code: className, + message: "Error in parsing identifier: \(identifier)", + details: "Invalid identifier for any existing QuantityType" + ) + ) + return + } + let predicate = NSPredicate.samplesPredicate( + startDate: Date.make(from: startTimestamp), + endDate: Date.make(from: endTimestamp) + ) + do { + let query = try reporter.reader.statisticsQuery( + type: type, + unit: unit, + predicate: predicate + ) { (statistics, error) in + guard error == nil else { + result( + FlutterError( + code: "StatisticsQuery", + message: "Error in statisticsQuery", + details: error.debugDescription + ) + ) + return + } + do { + guard let statistics = statistics else { + result( + FlutterError( + code: "StatisticsQuery", + message: "Statistics samples was null", + details: nil + ) + ) + return + } + result(try statistics.encoded()) + } catch { + result( + FlutterError( + code: "StatisticsQuery", + message: "Error in json encoding of statistics: \(String(describing: statistics))", + details: error + ) + ) + } + } + reporter.manager.executeQuery(query) + } catch { + result( + FlutterError( + code: className, + message: "Error in statisticsQuery initialization", + details: error + ) + ) + } + } + private func heartbeatSeriesQuery( + reporter: HealthKitReporter, + arguments: [String: Double], + result: @escaping FlutterResult + ) { + guard + let startTimestamp = arguments["startTimestamp"], + let endTimestamp = arguments["endTimestamp"] + else { + throwParsingArgumentsError(result: result, arguments: arguments) + return + } + if #available(iOS 13.0, *) { + let predicate = NSPredicate.samplesPredicate( + startDate: Date.make(from: startTimestamp), + endDate: Date.make(from: endTimestamp) + ) + do { + let query = try reporter.reader.heartbeatSeriesQuery( + predicate: predicate + ) { (series, error) in + guard error == nil else { + result( + FlutterError( + code: "HeartbeatSeriesQuery", + message: "Error in heartbeatSeriesQuery", + details: error.debugDescription + ) + ) + return + } + do { + result(try series.encoded()) + } catch { + result( + FlutterError( + code: "HeartbeatSeriesQuery", + message: "Error in json encoding of beat by beat series: \(series)", + details: error + ) + ) + } + } + reporter.manager.executeQuery(query) + } catch let error { + result( + FlutterError( + code: className, + message: "Error in heartbeatSeriesQuery initialization", + details: error + ) + ) + } + } else { + result( + FlutterError( + code: className, + message: "Error in platform version.", + details: "HeartbeatSeries query is available for iOS 13." + ) + ) + } + } + private func workoutRouteQuery( + reporter: HealthKitReporter, + arguments: [String: Double], + result: @escaping FlutterResult + ) { + guard + let startTimestamp = arguments["startTimestamp"], + let endTimestamp = arguments["endTimestamp"] + else { + throwParsingArgumentsError(result: result, arguments: arguments) + return + } + if #available(iOS 11.0, *) { + let predicate = NSPredicate.samplesPredicate( + startDate: Date.make(from: startTimestamp), + endDate: Date.make(from: endTimestamp) + ) + do { + let query = try reporter.reader.workoutRouteQuery( + predicate: predicate + ) { (routes, error) in + guard error == nil else { + result( + FlutterError( + code: "WorkoutRouteQuery", + message: "Error in workoutRouteQuery", + details: error.debugDescription + ) + ) + return + } + do { + result(try routes.encoded()) + } catch { + result( + FlutterError( + code: "WorkoutRouteQuery", + message: "Error in json encoding of workout routes: \(routes)", + details: error + ) + ) + } + } + reporter.manager.executeQuery(query) + } catch let error { + result( + FlutterError( + code: className, + message: "Error in workoutRouteQuery initialization", + details: error + ) + ) + } + } else { + result( + FlutterError( + code: className, + message: "Error in platform version.", + details: "WorkoutRoute query is available for iOS 11." + ) + ) + } + } + private func queryActivitySummary( + reporter: HealthKitReporter, + arguments: [String: Double], + result: @escaping FlutterResult + ) { + guard + let startTimestamp = arguments["startTimestamp"], + let endTimestamp = arguments["endTimestamp"] + else { + throwParsingArgumentsError(result: result, arguments: arguments) + return + } + if #available(iOS 9.3, *) { + let startDate = Date.make(from: startTimestamp) + let endDate = Date.make(from: endTimestamp) + let units: Set = [ + .day, + .month, + .year, + .era + ] + let calendar = Calendar.current + var startDateComponents = calendar.dateComponents(units, from: startDate) + startDateComponents.calendar = calendar + var endDateComponents = calendar.dateComponents(units, from: endDate) + endDateComponents.calendar = calendar + let predicate = NSPredicate.activitySummaryPredicateBetween( + start: startDateComponents, + end: endDateComponents + ) + let query = reporter.reader.queryActivitySummary( + predicate: predicate, + monitorUpdates: false + ) { (activitySummaries, error) in + guard error == nil else { + result( + FlutterError( + code: "QueryActivitySummary", + message: "Error in queryActivitySummary", + details: error.debugDescription + ) + ) + return + } + do { + result(try activitySummaries.encoded()) + } catch { + result( + FlutterError( + code: "QueryActivitySummary", + message: "Error in json encoding of activitySummaries: \(activitySummaries)", + details: error + ) + ) + } + } + reporter.manager.executeQuery(query) + } else { + result( + FlutterError( + code: className, + message: "Error in platform version.", + details: "ActivitySummary query is available for iOS 9.3." + ) + ) + } + } + private func sourceQuery( + reporter: HealthKitReporter, + arguments: [String: Any], + result: @escaping FlutterResult + ) { + guard + let identifier = arguments["identifier"] as? String, + let startTimestamp = arguments["startTimestamp"] as? Double, + let endTimestamp = arguments["endTimestamp"] as? Double + else { + throwParsingArgumentsError(result: result, arguments: arguments) + return + } + guard let type = identifier.objectType as? SampleType else { + result( + FlutterError( + code: className, + message: "Error in parsing identifier: \(identifier)", + details: "Invalid identifier for any existing ObjectType" + ) + ) + return + } + let predicate = NSPredicate.samplesPredicate( + startDate: Date.make(from: startTimestamp), + endDate: Date.make(from: endTimestamp) + ) + do { + let query = try reporter.reader.sourceQuery( + type: type, + predicate: predicate + ) { (sources, error) in + guard error == nil else { + result( + FlutterError( + code: "SourceQuery", + message: "Error in sourceQuery", + details: error.debugDescription + ) + ) + return + } + do { + result(try sources.encoded()) + } catch { + result( + FlutterError( + code: "SourceQuery", + message: "Error in json encoding of sources: \(sources)", + details: error + ) + ) + } + } + reporter.manager.executeQuery(query) + } catch { + result( + FlutterError( + code: className, + message: "Error in sourceQuery initialization", + details: error + ) + ) + } + } + private func correlationQuery( + reporter: HealthKitReporter, + arguments: [String: Any], + result: @escaping FlutterResult + ) { + guard + let identifier = arguments["identifier"] as? String, + let startTimestamp = arguments["startTimestamp"] as? Double, + let endTimestamp = arguments["endTimestamp"] as? Double + else { + throwParsingArgumentsError(result: result, arguments: arguments) + return + } + guard let type = identifier.objectType as? CorrelationType else { + result( + FlutterError( + code: className, + message: "Error in parsing identifier: \(identifier)", + details: "Invalid identifier for any existing CorrelationType" + ) + ) + return + } + let predicate = NSPredicate.samplesPredicate( + startDate: Date.make(from: startTimestamp), + endDate: Date.make(from: endTimestamp) + ) + var typePredicates: [String: NSPredicate] = [:] + if let typePredicatesArgument = arguments["typePredicates"] as? [String: [String: Double]] { + for (key, value) in typePredicatesArgument { + guard + let startTimestamp = value["startTimestamp"], + let endTimestamp = value["endTimestamp"] + else { + continue + } + let typePredicate = NSPredicate.samplesPredicate( + startDate: Date.make(from: startTimestamp), + endDate: Date.make(from: endTimestamp) + ) + typePredicates[key] = typePredicate + } + } + do { + let query = try reporter.reader.correlationQuery( + type: type, + predicate: predicate, + typePredicates: typePredicates + ) { (correlations, error) in + guard error == nil else { + result( + FlutterError( + code: "CorrelationQuery", + message: "Error in correlationQuery", + details: error.debugDescription + ) + ) + return + } + do { + result(try correlations.encoded()) + } catch { + result( + FlutterError( + code: "CorrelationQuery", + message: "Error in json encoding of correlations: \(correlations)", + details: error + ) + ) + } + } + reporter.manager.executeQuery(query) + } catch { + result( + FlutterError( + code: className, + message: "Error in correlationQuery initialization", + details: error + ) + ) + } + } + private func enableBackgroundDelivery( + reporter: HealthKitReporter, + arguments: [String: Any], + result: @escaping FlutterResult + ) { + guard + let identifier = arguments["identifier"] as? String, + let frequency = arguments["frequency"] as? Int + else { + throwParsingArgumentsError(result: result, arguments: arguments) + return + } + guard let type = identifier.objectType else { + result( + FlutterError( + code: className, + message: "Error in parsing identifier: \(identifier)", + details: "Invalid identifier for any existing ObjectType" + ) + ) + return + } + do { + let updateFrequency = try UpdateFrequency.make(from: frequency) + reporter.observer.enableBackgroundDelivery( + type: type, + frequency: updateFrequency + ) { (success, error) in + guard error == nil else { + result( + FlutterError( + code: "EnableBackgroundDelivery", + message: "Error in enableBackgroundDelivery", + details: error.debugDescription + ) + ) + return + } + result(success) + } + } catch { + result( + FlutterError( + code: className, + message: "Error in parsing frequency: \(frequency)", + details: error + ) + ) + } + } + private func disableAllBackgroundDelivery( + reporter: HealthKitReporter, + result: @escaping FlutterResult + ) { + reporter.observer.disableAllBackgroundDelivery { (success, error) in + guard error == nil else { + result( + FlutterError( + code: "DisableAllBackgroundDelivery", + message: "Error in disableAllBackgroundDelivery", + details: error.debugDescription + ) + ) + return + } + result(success) + } + } + private func disableBackgroundDelivery( + reporter: HealthKitReporter, + arguments: [String: String], + result: @escaping FlutterResult + ) { + guard let identifier = arguments["identifier"] else { + throwParsingArgumentsError(result: result, arguments: arguments) + return + } + guard let type = identifier.objectType else { + result( + FlutterError( + code: className, + message: "Error in parsing identifier: \(identifier)", + details: "Invalid identifier for any existing ObjectType" + ) + ) + return + } + reporter.observer.disableBackgroundDelivery(type: type) { (success, error) in + guard error == nil else { + result( + FlutterError( + code: "DisableBackgroundDelivery", + message: "Error in disableBackgroundDelivery", + details: error.debugDescription + ) + ) + return + } + result(success) + } + } + private func startWatchApp( + reporter: HealthKitReporter, + arguments: [String: Any], + result: @escaping FlutterResult + ) { + if #available(iOS 10.0, *) { + do { + let workoutConfiguration = try WorkoutConfiguration.make(from: arguments) + reporter.manager.startWatchApp(with: workoutConfiguration) { (success, error) in + guard error == nil else { + result( + FlutterError( + code: "StartWatchApp", + message: "Error in startWatchApp", + details: error.debugDescription + ) + ) + return + } + result(success) + } + } catch { + result( + FlutterError( + code: className, + message: "Error in creating WorkoutConfiguration", + details: error + ) + ) + } + } else { + result( + FlutterError( + code: className, + message: "Error in platform version.", + details: "StartWatchApp is available for iOS 9.3." + ) + ) + } + } + private func isAuthorizedToWrite( + reporter: HealthKitReporter, + arguments: [String: String], + result: @escaping FlutterResult + ) { + guard let identifier = arguments["identifier"] else { + throwParsingArgumentsError(result: result, arguments: arguments) + return + } + guard let type = identifier.objectType else { + result( + FlutterError( + code: className, + message: "Error in parsing identifier: \(identifier)", + details: "Invalid identifier for any existing ObjectType" + ) + ) + return + } + do { + let isAuthorizedToWrite = try reporter.writer.isAuthorizedToWrite(type: type) + result(isAuthorizedToWrite) + } catch { + let message = "Error in writing authorization status for identifier: \(identifier)" + result( + FlutterError( + code: className, + message: message, + details: error + ) + ) + } + } + private func addCategory( + reporter: HealthKitReporter, + arguments: [String: Any], + result: @escaping FlutterResult + ) { + guard + let category = arguments["categories"] as? [[String: Any]], + let workout = arguments["workout"] as? [String: Any] + else { + throwParsingArgumentsError(result: result, arguments: arguments) + return + } + let device = arguments["device"] as? [String: Any] + do { + reporter.writer.addCategory( + try category.map { + try Category.make(from: $0) + }, + from: device != nil + ? try Device.make(from: device!) + : nil, + to: try Workout.make(from: workout) + ) { (success, error) in + guard error == nil else { + result( + FlutterError( + code: "AddCategory", + message: "\(#line). Error in addCategory", + details: error.debugDescription + ) + ) + return + } + result(success) + } + } catch { + throwPlatformError(result: result, error: error) + } + } + private func addQuantity( + reporter: HealthKitReporter, + arguments: [String: Any], + result: @escaping FlutterResult + ) { + guard + let quantity = arguments["quantities"] as? [[String: Any]], + let workout = arguments["workout"] as? [String: Any] + else { + throwParsingArgumentsError(result: result, arguments: arguments) + return + } + let device = arguments["device"] as? [String: Any] + do { + reporter.writer.addQuantitiy(try quantity.map { + try Quantity.make(from: $0) + }, + from: device != nil + ? try Device.make(from: device!) + : nil, + to: try Workout.make(from: workout)) { (success, error) in + guard error == nil else { + result( + FlutterError( + code: "AddQuantity", + message: "Error in addQuantity", + details: error.debugDescription + ) + ) + return + } + result(success) + } + } catch { + throwPlatformError(result: result, error: error) + } + } + private func delete( + reporter: HealthKitReporter, + arguments: [String: Any], + result: @escaping FlutterResult + ) { + do { + let sample = try parse(arguments: arguments) + reporter.writer.delete(sample: sample) { (success, error) in + guard error == nil else { + result( + FlutterError( + code: "Delete", + message: "Error in delete", + details: error.debugDescription + ) + ) + return + } + result(success) + } + } catch { + throwPlatformError(result: result, error: error) + } + } + private func deleteObjects( + reporter: HealthKitReporter, + arguments: [String: Any], + result: @escaping FlutterResult + ) { + guard + let identifier = arguments["identifier"] as? String, + let startTimestamp = arguments["startTimestamp"] as? Double, + let endTimestamp = arguments["endTimestamp"] as? Double + else { + throwParsingArgumentsError(result: result, arguments: arguments) + return + } + guard let type = identifier.objectType else { + result( + FlutterError( + code: className, + message: "Error in parsing identifier: \(identifier)", + details: "Invalid identifier for any existing ObjectType" + ) + ) + return + } + let predicate = NSPredicate.samplesPredicate( + startDate: Date.make(from: startTimestamp), + endDate: Date.make(from: endTimestamp) + ) + reporter.writer.deleteObjects( + of: type, + predicate: predicate + ) { (success, count, error) in + guard error == nil else { + result( + FlutterError( + code: "DeleteObjects", + message: "Error in delete", + details: error.debugDescription + ) + ) + return + } + let resultDictionary: [String: Any] = [ + "status": success, + "count": count + ] + result(resultDictionary) + } + } + private func save( + reporter: HealthKitReporter, + arguments: [String: Any], + result: @escaping FlutterResult + ) { + do { + let sample = try parse(arguments: arguments) + reporter.writer.save(sample: sample) { (success, error) in + guard error == nil else { + result( + FlutterError( + code: "Save", + message: "Error in save", + details: error.debugDescription + ) + ) + return + } + result(success) + } + } catch { + throwPlatformError(result: result, error: error) + } + } +} +// MARK: - Helper functions +extension SwiftHealthKitReporterPlugin { + private func parse(arguments: [String]) -> [ObjectType] { + var types: [ObjectType] = [] + for argument in arguments { + if let type = argument.objectType { + types.append(type) + } + } + return types + } + private func parse(arguments: [String]) -> [SampleType] { + var types: [SampleType] = [] + for argument in arguments { + if let type = argument.objectType as? SampleType { + types.append(type) + } + } + return types + } + private func parse(arguments: [String: Any]) throws -> Sample { + if let quantity = arguments["quantity"] as? [String: Any] { + let sample = try Quantity.make(from: quantity) + return sample.copyWith( + startTimestamp: sample.startTimestamp.secondsSince1970, + endTimestamp: sample.endTimestamp.secondsSince1970 + ) + } + if let category = arguments["category"] as? [String: Any] { + let sample = try Category.make(from: category) + return sample.copyWith( + startTimestamp: sample.startTimestamp.secondsSince1970, + endTimestamp: sample.endTimestamp.secondsSince1970 + ) + } + if let workout = arguments["workout"] as? [String: Any] { + let sample = try Workout.make(from: workout) + return sample.copyWith( + startTimestamp: sample.startTimestamp.secondsSince1970, + endTimestamp: sample.endTimestamp.secondsSince1970, + workoutEvents: sample.workoutEvents.map { event in + event.copyWith( + startTimestamp: event.startTimestamp.secondsSince1970, + endTimestamp: event.endTimestamp.secondsSince1970 + ) + } + ) + } + throw HealthKitError.invalidValue("Invalid arguments: \(arguments)") + } + private func throwParsingArgumentsError( + result: FlutterResult, + arguments: Any + ) { + result( + FlutterError( + code: className, + message: "Error in parsing arguments.", + details: "Arguments: \(String(describing: arguments))." + ) + ) + } + private func throwNoArgumentsError(result: FlutterResult) { + result( + FlutterError( + code: className, + message: "Error call arguments.", + details: "No arguments" + ) + ) + } + private func throwPlatformError( + result: FlutterResult, + error: Error + ) { + result( + FlutterError( + code: className, + message: "Error in platform method.", + details: error + ) + ) + } + private func throwSystemVersionError(result: FlutterResult) { + result( + FlutterError( + code: className, + message: "The current system version does not support method", + details: nil + ) + ) + } +} + diff --git a/ios/Classes/StreamHandlerFactory.swift b/ios/Classes/StreamHandlerFactory.swift index 2bb014d..ca55347 100644 --- a/ios/Classes/StreamHandlerFactory.swift +++ b/ios/Classes/StreamHandlerFactory.swift @@ -22,8 +22,6 @@ final class StreamHandlerFactory: NSObject { } case .anchoredObjectQuery: return AnchoredObjectQueryStreamHandler.make(with: reporter) - case .workoutRouteQuery: - return WorkoutRouteQueryStreamHandler.make(with: reporter) } } } diff --git a/ios/Classes/SwiftHealthKitReporterPlugin.swift b/ios/Classes/SwiftHealthKitReporterPlugin.swift index 328ad56..c706c03 100644 --- a/ios/Classes/SwiftHealthKitReporterPlugin.swift +++ b/ios/Classes/SwiftHealthKitReporterPlugin.swift @@ -2,7 +2,7 @@ import Flutter import HealthKitReporter public class SwiftHealthKitReporterPlugin: NSObject, FlutterPlugin { - private enum Method: String { + enum Method: String { case requestAuthorization case preferredUnits case characteristicsQuery @@ -13,6 +13,7 @@ public class SwiftHealthKitReporterPlugin: NSObject, FlutterPlugin { case sampleQuery case statisticsQuery case heartbeatSeriesQuery + case workoutRouteQuery case queryActivitySummary case sourceQuery case correlationQuery @@ -50,7 +51,6 @@ public class SwiftHealthKitReporterPlugin: NSObject, FlutterPlugin { print(error) } } - private static func registerMethodChannel( registrar: FlutterPluginRegistrar, binaryMessenger: FlutterBinaryMessenger, @@ -78,1406 +78,3 @@ public class SwiftHealthKitReporterPlugin: NSObject, FlutterPlugin { } } } -// MARK: - MethodCall -extension SwiftHealthKitReporterPlugin { - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - guard let reporter = self.reporter else { - result( - FlutterError( - code: className, - message: "Reporter is nil", - details: nil - ) - ) - return - } - guard let method = Method.init(rawValue: call.method) else { - result( - FlutterError( - code: className, - message: "Please check the method", - details: "Provided method: \(call.method)" - ) - ) - return - } - switch method { - case .requestAuthorization: - guard let arguments = call.arguments as? [String: [String]] else { - throwNoArgumentsError(result: result) - return - } - requestAuthorization( - reporter: reporter, - arguments: arguments, - result: result - ) - case .preferredUnits: - guard let arguments = call.arguments as? [String] else { - throwNoArgumentsError(result: result) - return - } - preferredUnits( - reporter: reporter, - arguments: arguments, - result: result - ) - case .characteristicsQuery: - characteristicsQuery(reporter: reporter, result: result) - case .quantityQuery: - guard let arguments = call.arguments as? [String: Any] else { - throwNoArgumentsError(result: result) - return - } - quantityQuery( - reporter: reporter, - arguments: arguments, - result: result - ) - case .categoryQuery: - guard let arguments = call.arguments as? [String: Any] else { - throwNoArgumentsError(result: result) - return - } - categoryQuery( - reporter: reporter, - arguments: arguments, - result: result - ) - case .workoutQuery: - guard let arguments = call.arguments as? [String: Double] else { - throwNoArgumentsError(result: result) - return - } - workoutQuery( - reporter: reporter, - arguments: arguments, - result: result - ) - case .electrocardiogramQuery: - guard let arguments = call.arguments as? [String: Double] else { - throwNoArgumentsError(result: result) - return - } - electrocardiogramQuery( - reporter: reporter, - arguments: arguments, - result: result - ) - case .sampleQuery: - guard let arguments = call.arguments as? [String: Any] else { - throwNoArgumentsError(result: result) - return - } - sampleQuery( - reporter: reporter, - arguments: arguments, - result: result - ) - case .statisticsQuery: - guard let arguments = call.arguments as? [String: Any] else { - throwNoArgumentsError(result: result) - return - } - statisticsQuery( - reporter: reporter, - arguments: arguments, - result: result - ) - case .heartbeatSeriesQuery: - guard let arguments = call.arguments as? [String: Double] else { - throwNoArgumentsError(result: result) - return - } - heartbeatSeriesQuery( - reporter: reporter, - arguments: arguments, - result: result - ) - case .queryActivitySummary: - if #available(iOS 9.3, *) { - guard let arguments = call.arguments as? [String: Double] else { - throwNoArgumentsError(result: result) - return - } - queryActivitySummary( - reporter: reporter, - arguments: arguments, - result: result - ) - } else { - throwSystemVersionError(result: result) - } - case .sourceQuery: - guard let arguments = call.arguments as? [String: Any] else { - throwNoArgumentsError(result: result) - return - } - sourceQuery( - reporter: reporter, - arguments: arguments, - result: result - ) - case .correlationQuery: - guard let arguments = call.arguments as? [String: Any] else { - throwNoArgumentsError(result: result) - return - } - correlationQuery( - reporter: reporter, - arguments: arguments, - result: result - ) - case .enableBackgroundDelivery: - guard let arguments = call.arguments as? [String: Any] else { - throwNoArgumentsError(result: result) - return - } - enableBackgroundDelivery( - reporter: reporter, - arguments: arguments, - result: result - ) - case .disableAllBackgroundDelivery: - disableAllBackgroundDelivery( - reporter: reporter, - result: result - ) - case .disableBackgroundDelivery: - guard let arguments = call.arguments as? [String: String] else { - throwNoArgumentsError(result: result) - return - } - disableBackgroundDelivery( - reporter: reporter, - arguments: arguments, - result: result - ) - case .startWatchApp: - if #available(iOS 10.0, *) { - guard let arguments = call.arguments as? [String: Any] else { - throwNoArgumentsError(result: result) - return - } - startWatchApp( - reporter: reporter, - arguments: arguments, - result: result - ) - } else { - throwSystemVersionError(result: result) - } - case .isAuthorizedToWrite: - guard let arguments = call.arguments as? [String: String] else { - throwNoArgumentsError(result: result) - return - } - isAuthorizedToWrite( - reporter: reporter, - arguments: arguments, - result: result - ) - case .addCategory: - guard let arguments = call.arguments as? [String: Any] else { - throwNoArgumentsError(result: result) - return - } - addCategory( - reporter: reporter, - arguments: arguments, - result: result - ) - case .addQuantity: - guard let arguments = call.arguments as? [String: Any] else { - throwNoArgumentsError(result: result) - return - } - addQuantity( - reporter: reporter, - arguments: arguments, - result: result - ) - case .delete: - guard let arguments = call.arguments as? [String: Any] else { - throwNoArgumentsError(result: result) - return - } - delete( - reporter: reporter, - arguments: arguments, - result: result - ) - case .deleteObjects: - guard let arguments = call.arguments as? [String: Any] else { - throwNoArgumentsError(result: result) - return - } - deleteObjects( - reporter: reporter, - arguments: arguments, - result: result - ) - case .save: - guard let arguments = call.arguments as? [String: Any] else { - throwNoArgumentsError(result: result) - return - } - save( - reporter: reporter, - arguments: arguments, - result: result - ) - } - } -} -// MARK: - Method Call methods -extension SwiftHealthKitReporterPlugin { - private func requestAuthorization( - reporter: HealthKitReporter, - arguments: [String: [String]], - result: @escaping FlutterResult - ) { - let toReadArguments = arguments["toRead"] - let toWriteArguments = arguments["toWrite"] - reporter.manager.requestAuthorization( - toRead: toReadArguments != nil - ? parse(arguments: toReadArguments!) - : [], - toWrite: toWriteArguments != nil - ? parse(arguments: toWriteArguments!) - : [] - ) { (success, error) in - guard error == nil else { - result( - FlutterError( - code: "RequestAuthorization", - message: "Error in displaying Apple Health permission screen", - details: error.debugDescription - ) - ) - return - } - result(success) - } - } - private func preferredUnits( - reporter: HealthKitReporter, - arguments: [String], - result: @escaping FlutterResult - ) { - var quantityTypes: [QuantityType] = [] - for argument in arguments { - guard let quantityType = try? QuantityType.make(from: argument) else { - result( - FlutterError( - code: className, - message: "Error in parsing quantity type", - details: "Identifier unknown: \(argument)" - ) - ) - return - } - quantityTypes.append(quantityType) - } - reporter.manager.preferredUnits( - for: quantityTypes - ) { (preferredUnits, error) in - guard error == nil else { - result( - FlutterError( - code: "PreferredUnits", - message: "Error in getting preferred units", - details: error.debugDescription - ) - ) - return - } - do { - result(try preferredUnits.encoded()) - } catch { - result( - FlutterError( - code: "PreferredUnits", - message: "Error in json encoding of preferred units: \(preferredUnits)", - details: error - ) - ) - } - } - } - private func characteristicsQuery( - reporter: HealthKitReporter, - result: FlutterResult - ) { - do { - let characteristics = reporter.reader.characteristics() - result(try characteristics.encoded()) - } catch { - throwPlatformError(result: result, error: error) - } - } - private func quantityQuery( - reporter: HealthKitReporter, - arguments: [String: Any], - result: @escaping FlutterResult - ) { - guard - let identifier = arguments["identifier"] as? String, - let unit = arguments["unit"] as? String, - let startTimestamp = arguments["startTimestamp"] as? Double, - let endTimestamp = arguments["endTimestamp"] as? Double - else { - throwParsingArgumentsError(result: result, arguments: arguments) - return - } - do { - let type = try QuantityType.make(from: identifier) - let predicate = NSPredicate.samplesPredicate( - startDate: Date.make(from: startTimestamp), - endDate: Date.make(from: endTimestamp) - ) - let query = try reporter.reader.quantityQuery( - type: type, - unit: unit, - predicate: predicate - ) { (quantities, error) in - guard error == nil else { - result( - FlutterError( - code: "QuantityQuery", - message: "Error in quantityQuery for identifier: \(identifier)", - details: error.debugDescription - ) - ) - return - } - do { - result(try quantities.encoded()) - } catch { - result( - FlutterError( - code: "QuantityQuery", - message: "Error in json encoding of quantities: \(quantities)", - details: error - ) - ) - } - } - reporter.manager.executeQuery(query) - } catch { - throwPlatformError(result: result, error: error) - } - } - private func categoryQuery( - reporter: HealthKitReporter, - arguments: [String: Any], - result: @escaping FlutterResult - ) { - guard - let identifier = arguments["identifier"] as? String, - let startTimestamp = arguments["startTimestamp"] as? Double, - let endTimestamp = arguments["endTimestamp"] as? Double - else { - throwParsingArgumentsError(result: result, arguments: arguments) - return - } - do { - let type = try CategoryType.make(from: identifier) - let predicate = NSPredicate.samplesPredicate( - startDate: Date.make(from: startTimestamp), - endDate: Date.make(from: endTimestamp) - ) - let query = try reporter.reader.categoryQuery( - type: type, - predicate: predicate - ) { (categories, error) in - guard error == nil else { - result( - FlutterError( - code: "CategoryQuery", - message: "Error in categoryQuery for identifier: \(identifier)", - details: error.debugDescription - ) - ) - return - } - do { - result(try categories.encoded()) - } catch { - result( - FlutterError( - code: "CategoryQuery", - message: "Error in json encoding of categories: \(categories)", - details: error - ) - ) - } - } - reporter.manager.executeQuery(query) - } catch { - throwPlatformError(result: result, error: error) - } - } - private func workoutQuery( - reporter: HealthKitReporter, - arguments: [String: Double], - result: @escaping FlutterResult - ) { - guard - let startTimestamp = arguments["startTimestamp"], - let endTimestamp = arguments["endTimestamp"] - else { - throwParsingArgumentsError(result: result, arguments: arguments) - return - } - let predicate = NSPredicate.samplesPredicate( - startDate: Date.make(from: startTimestamp), - endDate: Date.make(from: endTimestamp) - ) - do { - let query = try reporter.reader.workoutQuery( - predicate: predicate - ) { (workouts, error) in - guard error == nil else { - result( - FlutterError( - code: "WorkoutQuery", - message: "Error in workoutQuery", - details: error.debugDescription - ) - ) - return - } - do { - result(try workouts.encoded()) - } catch { - result( - FlutterError( - code: "WorkoutQuery", - message: "Error in json encoding of workouts: \(workouts)", - details: error - ) - ) - } - } - reporter.manager.executeQuery(query) - } catch { - result( - FlutterError( - code: className, - message: "Error in workoutQuery initialization", - details: error - ) - ) - } - } - private func electrocardiogramQuery( - reporter: HealthKitReporter, - arguments: [String: Double], - result: @escaping FlutterResult - ) { - guard - let startTimestamp = arguments["startTimestamp"], - let endTimestamp = arguments["endTimestamp"] - else { - throwParsingArgumentsError(result: result, arguments: arguments) - return - } - let predicate = NSPredicate.samplesPredicate( - startDate: Date.make(from: startTimestamp), - endDate: Date.make(from: endTimestamp) - ) - if #available(iOS 14.0, *) { - do { - let query = try reporter.reader.electrocardiogramQuery( - predicate: predicate - ) { (electrocardiograms, error) in - guard error == nil else { - result( - FlutterError( - code: "ElectrocardiogramQuery", - message: "Error in electrocardiogramQuery", - details: error.debugDescription - ) - ) - return - } - do { - result(try electrocardiograms.encoded()) - } catch { - result( - FlutterError( - code: "ElectrocardiogramQuery", - message: "Error in json encoding of electrocardiograms: \(electrocardiograms)", - details: error - ) - ) - } - } - reporter.manager.executeQuery(query) - } catch { - result( - FlutterError( - code: className, - message: "Error electrocardiograms query initialization", - details: error - ) - ) - } - } else { - result( - FlutterError( - code: className, - message: "Error in platform version.", - details: "Electrocardiogram query is available for iOS 14." - ) - ) - } - } - private func sampleQuery( - reporter: HealthKitReporter, - arguments: [String: Any], - result: @escaping FlutterResult - ) { - guard - let identifier = arguments["identifier"] as? String, - let startTimestamp = arguments["startTimestamp"] as? Double, - let endTimestamp = arguments["endTimestamp"] as? Double - else { - throwParsingArgumentsError(result: result, arguments: arguments) - return - } - guard let type = identifier.objectType as? SampleType else { - result( - FlutterError( - code: className, - message: "Error in parsing identifier: \(identifier)", - details: "Invalid identifier for any existing ObjectType" - ) - ) - return - } - let predicate = NSPredicate.samplesPredicate( - startDate: Date.make(from: startTimestamp), - endDate: Date.make(from: endTimestamp) - ) - do { - let query = try reporter.reader.sampleQuery( - type: type, - predicate: predicate - ) { (_, samples, error) in - guard error == nil else { - result( - FlutterError( - code: "SampleQuery", - message: "Error in sampleQuery", - details: error.debugDescription - ) - ) - return - } - var jsonArray: [String] = [] - for sample in samples { - do { - let encoded = try sample.encoded() - jsonArray.append(encoded) - } catch { - result( - FlutterError( - code: "SampleQuery", - message: "Error in json encoding of sample: \(sample). Continue", - details: error - ) - ) - continue - } - } - result(jsonArray) - } - reporter.manager.executeQuery(query) - } catch { - result( - FlutterError( - code: className, - message: "Error in sampleQuery initialization", - details: error - ) - ) - } - } - private func statisticsQuery( - reporter: HealthKitReporter, - arguments: [String: Any], - result: @escaping FlutterResult - ) { - guard - let identifier = arguments["identifier"] as? String, - let unit = arguments["unit"] as? String, - let startTimestamp = arguments["startTimestamp"] as? Double, - let endTimestamp = arguments["endTimestamp"] as? Double - else { - throwParsingArgumentsError(result: result, arguments: arguments) - return - } - guard let type = identifier.objectType as? QuantityType else { - result( - FlutterError( - code: className, - message: "Error in parsing identifier: \(identifier)", - details: "Invalid identifier for any existing QuantityType" - ) - ) - return - } - let predicate = NSPredicate.samplesPredicate( - startDate: Date.make(from: startTimestamp), - endDate: Date.make(from: endTimestamp) - ) - do { - let query = try reporter.reader.statisticsQuery( - type: type, - unit: unit, - predicate: predicate - ) { (statistics, error) in - guard error == nil else { - result( - FlutterError( - code: "StatisticsQuery", - message: "Error in statisticsQuery", - details: error.debugDescription - ) - ) - return - } - do { - guard let statistics = statistics else { - result( - FlutterError( - code: "StatisticsQuery", - message: "Statistics samples was null", - details: nil - ) - ) - return - } - result(try statistics.encoded()) - } catch { - result( - FlutterError( - code: "StatisticsQuery", - message: "Error in json encoding of statistics: \(String(describing: statistics))", - details: error - ) - ) - } - } - reporter.manager.executeQuery(query) - } catch { - result( - FlutterError( - code: className, - message: "Error in statisticsQuery initialization", - details: error - ) - ) - } - } - private func heartbeatSeriesQuery( - reporter: HealthKitReporter, - arguments: [String: Double], - result: @escaping FlutterResult - ) { - guard - let startTimestamp = arguments["startTimestamp"], - let endTimestamp = arguments["endTimestamp"] - else { - throwParsingArgumentsError(result: result, arguments: arguments) - return - } - let predicate = NSPredicate.samplesPredicate( - startDate: Date.make(from: startTimestamp), - endDate: Date.make(from: endTimestamp) - ) - if #available(iOS 13.0, *) { - do { - let query = try reporter.reader.heartbeatSeriesQuery( - predicate: predicate - ) { (series, error) in - guard error == nil else { - result( - FlutterError( - code: "HeartbeatSeriesQuery", - message: "Error in heartbeatSeriesQuery", - details: error.debugDescription - ) - ) - return - } - do { - result(try series.encoded()) - } catch { - result( - FlutterError( - code: "HeartbeatSeriesQuery", - message: "Error in json encoding of beat by beat series: \(series)", - details: error - ) - ) - } - } - reporter.manager.executeQuery(query) - } catch { - result( - FlutterError( - code: className, - message: "Error in heartbeatSeriesQuery initialization", - details: "HeartbeatSeries query is available for iOS 13." - ) - ) - } - } else { - result( - FlutterError( - code: className, - message: "Error in platform version.", - details: "HeartbeatSeries query is available for iOS 13." - ) - ) - } - } - @available(iOS 9.3, *) - private func queryActivitySummary( - reporter: HealthKitReporter, - arguments: [String: Double], - result: @escaping FlutterResult - ) { - guard - let startTimestamp = arguments["startTimestamp"], - let endTimestamp = arguments["endTimestamp"] - else { - throwParsingArgumentsError(result: result, arguments: arguments) - return - } - let startDate = Date.make(from: startTimestamp) - let endDate = Date.make(from: endTimestamp) - let units: Set = [ - .day, - .month, - .year, - .era - ] - let calendar = Calendar.current - var startDateComponents = calendar.dateComponents(units, from: startDate) - startDateComponents.calendar = calendar - var endDateComponents = calendar.dateComponents(units, from: endDate) - endDateComponents.calendar = calendar - let predicate = NSPredicate.activitySummaryPredicateBetween( - start: startDateComponents, - end: endDateComponents - ) - let query = reporter.reader.queryActivitySummary( - predicate: predicate, - monitorUpdates: false - ) { (activitySummaries, error) in - guard error == nil else { - result( - FlutterError( - code: "QueryActivitySummary", - message: "Error in queryActivitySummary", - details: error.debugDescription - ) - ) - return - } - do { - result(try activitySummaries.encoded()) - } catch { - result( - FlutterError( - code: "QueryActivitySummary", - message: "Error in json encoding of activitySummaries: \(activitySummaries)", - details: error - ) - ) - } - } - reporter.manager.executeQuery(query) - } - private func sourceQuery( - reporter: HealthKitReporter, - arguments: [String: Any], - result: @escaping FlutterResult - ) { - guard - let identifier = arguments["identifier"] as? String, - let startTimestamp = arguments["startTimestamp"] as? Double, - let endTimestamp = arguments["endTimestamp"] as? Double - else { - throwParsingArgumentsError(result: result, arguments: arguments) - return - } - guard let type = identifier.objectType as? SampleType else { - result( - FlutterError( - code: className, - message: "Error in parsing identifier: \(identifier)", - details: "Invalid identifier for any existing ObjectType" - ) - ) - return - } - let predicate = NSPredicate.samplesPredicate( - startDate: Date.make(from: startTimestamp), - endDate: Date.make(from: endTimestamp) - ) - do { - let query = try reporter.reader.sourceQuery( - type: type, - predicate: predicate - ) { (sources, error) in - guard error == nil else { - result( - FlutterError( - code: "SourceQuery", - message: "Error in sourceQuery", - details: error.debugDescription - ) - ) - return - } - do { - result(try sources.encoded()) - } catch { - result( - FlutterError( - code: "SourceQuery", - message: "Error in json encoding of sources: \(sources)", - details: error - ) - ) - } - } - reporter.manager.executeQuery(query) - } catch { - result( - FlutterError( - code: className, - message: "Error in sourceQuery initialization", - details: error - ) - ) - } - } - private func correlationQuery( - reporter: HealthKitReporter, - arguments: [String: Any], - result: @escaping FlutterResult - ) { - guard - let identifier = arguments["identifier"] as? String, - let startTimestamp = arguments["startTimestamp"] as? Double, - let endTimestamp = arguments["endTimestamp"] as? Double - else { - throwParsingArgumentsError(result: result, arguments: arguments) - return - } - guard let type = identifier.objectType as? CorrelationType else { - result( - FlutterError( - code: className, - message: "Error in parsing identifier: \(identifier)", - details: "Invalid identifier for any existing CorrelationType" - ) - ) - return - } - let predicate = NSPredicate.samplesPredicate( - startDate: Date.make(from: startTimestamp), - endDate: Date.make(from: endTimestamp) - ) - var typePredicates: [String: NSPredicate] = [:] - if let typePredicatesArgument = arguments["typePredicates"] as? [String: [String: Double]] { - for (key, value) in typePredicatesArgument { - guard - let startTimestamp = value["startTimestamp"], - let endTimestamp = value["endTimestamp"] - else { - continue - } - let typePredicate = NSPredicate.samplesPredicate( - startDate: Date.make(from: startTimestamp), - endDate: Date.make(from: endTimestamp) - ) - typePredicates[key] = typePredicate - } - } - do { - let query = try reporter.reader.correlationQuery( - type: type, - predicate: predicate, - typePredicates: typePredicates - ) { (correlations, error) in - guard error == nil else { - result( - FlutterError( - code: "CorrelationQuery", - message: "Error in correlationQuery", - details: error.debugDescription - ) - ) - return - } - do { - result(try correlations.encoded()) - } catch { - result( - FlutterError( - code: "CorrelationQuery", - message: "Error in json encoding of correlations: \(correlations)", - details: error - ) - ) - } - } - reporter.manager.executeQuery(query) - } catch { - result( - FlutterError( - code: className, - message: "Error in correlationQuery initialization", - details: error - ) - ) - } - } - private func enableBackgroundDelivery( - reporter: HealthKitReporter, - arguments: [String: Any], - result: @escaping FlutterResult - ) { - guard - let identifier = arguments["identifier"] as? String, - let frequency = arguments["frequency"] as? Int - else { - throwParsingArgumentsError(result: result, arguments: arguments) - return - } - guard let type = identifier.objectType else { - result( - FlutterError( - code: className, - message: "Error in parsing identifier: \(identifier)", - details: "Invalid identifier for any existing ObjectType" - ) - ) - return - } - do { - let updateFrequency = try UpdateFrequency.make(from: frequency) - reporter.observer.enableBackgroundDelivery( - type: type, - frequency: updateFrequency - ) { (success, error) in - guard error == nil else { - result( - FlutterError( - code: "EnableBackgroundDelivery", - message: "Error in enableBackgroundDelivery", - details: error.debugDescription - ) - ) - return - } - result(success) - } - } catch { - result( - FlutterError( - code: className, - message: "Error in parsing frequency: \(frequency)", - details: error - ) - ) - } - } - private func disableAllBackgroundDelivery( - reporter: HealthKitReporter, - result: @escaping FlutterResult - ) { - reporter.observer.disableAllBackgroundDelivery { (success, error) in - guard error == nil else { - result( - FlutterError( - code: "DisableAllBackgroundDelivery", - message: "Error in disableAllBackgroundDelivery", - details: error.debugDescription - ) - ) - return - } - result(success) - } - } - private func disableBackgroundDelivery( - reporter: HealthKitReporter, - arguments: [String: String], - result: @escaping FlutterResult - ) { - guard let identifier = arguments["identifier"] else { - throwParsingArgumentsError(result: result, arguments: arguments) - return - } - guard let type = identifier.objectType else { - result( - FlutterError( - code: className, - message: "Error in parsing identifier: \(identifier)", - details: "Invalid identifier for any existing ObjectType" - ) - ) - return - } - reporter.observer.disableBackgroundDelivery(type: type) { (success, error) in - guard error == nil else { - result( - FlutterError( - code: "DisableBackgroundDelivery", - message: "Error in disableBackgroundDelivery", - details: error.debugDescription - ) - ) - return - } - result(success) - } - } - @available(iOS 10.0, *) - private func startWatchApp( - reporter: HealthKitReporter, - arguments: [String: Any], - result: @escaping FlutterResult - ) { - do { - let workoutConfiguration = try WorkoutConfiguration.make(from: arguments) - reporter.manager.startWatchApp(with: workoutConfiguration) { (success, error) in - guard error == nil else { - result( - FlutterError( - code: "StartWatchApp", - message: "Error in startWatchApp", - details: error.debugDescription - ) - ) - return - } - result(success) - } - } catch { - result( - FlutterError( - code: className, - message: "Error in creating WorkoutConfiguration", - details: error - ) - ) - } - } - private func isAuthorizedToWrite( - reporter: HealthKitReporter, - arguments: [String: String], - result: @escaping FlutterResult - ) { - guard let identifier = arguments["identifier"] else { - throwParsingArgumentsError(result: result, arguments: arguments) - return - } - guard let type = identifier.objectType else { - result( - FlutterError( - code: className, - message: "Error in parsing identifier: \(identifier)", - details: "Invalid identifier for any existing ObjectType" - ) - ) - return - } - do { - let isAuthorizedToWrite = try reporter.writer.isAuthorizedToWrite(type: type) - result(isAuthorizedToWrite) - } catch { - let message = "Error in writing authorization status for identifier: \(identifier)" - result( - FlutterError( - code: className, - message: message, - details: error - ) - ) - } - } - private func addCategory( - reporter: HealthKitReporter, - arguments: [String: Any], - result: @escaping FlutterResult - ) { - guard - let category = arguments["categories"] as? [[String: Any]], - let workout = arguments["workout"] as? [String: Any] - else { - throwParsingArgumentsError(result: result, arguments: arguments) - return - } - let device = arguments["device"] as? [String: Any] - do { - reporter.writer.addCategory( - try category.map { - try Category.make(from: $0) - }, - from: device != nil - ? try Device.make(from: device!) - : nil, - to: try Workout.make(from: workout) - ) { (success, error) in - guard error == nil else { - result( - FlutterError( - code: "AddCategory", - message: "\(#line). Error in addCategory", - details: error.debugDescription - ) - ) - return - } - result(success) - } - } catch { - throwPlatformError(result: result, error: error) - } - } - private func addQuantity( - reporter: HealthKitReporter, - arguments: [String: Any], - result: @escaping FlutterResult - ) { - guard - let quantity = arguments["quantities"] as? [[String: Any]], - let workout = arguments["workout"] as? [String: Any] - else { - throwParsingArgumentsError(result: result, arguments: arguments) - return - } - let device = arguments["device"] as? [String: Any] - do { - reporter.writer.addQuantitiy(try quantity.map { - try Quantity.make(from: $0) - }, - from: device != nil - ? try Device.make(from: device!) - : nil, - to: try Workout.make(from: workout)) { (success, error) in - guard error == nil else { - result( - FlutterError( - code: "AddQuantity", - message: "Error in addQuantity", - details: error.debugDescription - ) - ) - return - } - result(success) - } - } catch { - throwPlatformError(result: result, error: error) - } - } - private func delete( - reporter: HealthKitReporter, - arguments: [String: Any], - result: @escaping FlutterResult - ) { - do { - let sample = try parse(arguments: arguments) - reporter.writer.delete(sample: sample) { (success, error) in - guard error == nil else { - result( - FlutterError( - code: "Delete", - message: "Error in delete", - details: error.debugDescription - ) - ) - return - } - result(success) - } - } catch { - throwPlatformError(result: result, error: error) - } - } - private func deleteObjects( - reporter: HealthKitReporter, - arguments: [String: Any], - result: @escaping FlutterResult - ) { - guard - let identifier = arguments["identifier"] as? String, - let startTimestamp = arguments["startTimestamp"] as? Double, - let endTimestamp = arguments["endTimestamp"] as? Double - else { - throwParsingArgumentsError(result: result, arguments: arguments) - return - } - guard let type = identifier.objectType else { - result( - FlutterError( - code: className, - message: "Error in parsing identifier: \(identifier)", - details: "Invalid identifier for any existing ObjectType" - ) - ) - return - } - let predicate = NSPredicate.samplesPredicate( - startDate: Date.make(from: startTimestamp), - endDate: Date.make(from: endTimestamp) - ) - reporter.writer.deleteObjects( - of: type, - predicate: predicate - ) { (success, count, error) in - guard error == nil else { - result( - FlutterError( - code: "DeleteObjects", - message: "Error in delete", - details: error.debugDescription - ) - ) - return - } - let resultDictionary: [String: Any] = [ - "status": success, - "count": count - ] - result(resultDictionary) - } - } - private func save( - reporter: HealthKitReporter, - arguments: [String: Any], - result: @escaping FlutterResult - ) { - do { - let sample = try parse(arguments: arguments) - reporter.writer.save(sample: sample) { (success, error) in - guard error == nil else { - result( - FlutterError( - code: "Save", - message: "Error in save", - details: error.debugDescription - ) - ) - return - } - result(success) - } - } catch { - throwPlatformError(result: result, error: error) - } - } -} -// MARK: - Helper functions -extension SwiftHealthKitReporterPlugin { - private func parse(arguments: [String]) -> [ObjectType] { - var types: [ObjectType] = [] - for argument in arguments { - if let type = argument.objectType { - types.append(type) - } - } - return types - } - private func parse(arguments: [String]) -> [SampleType] { - var types: [SampleType] = [] - for argument in arguments { - if let type = argument.objectType as? SampleType { - types.append(type) - } - } - return types - } - private func parse(arguments: [String: Any]) throws -> Sample { - if let quantity = arguments["quantity"] as? [String: Any] { - let sample = try Quantity.make(from: quantity) - return sample.copyWith( - startTimestamp: sample.startTimestamp.secondsSince1970, - endTimestamp: sample.endTimestamp.secondsSince1970 - ) - } - if let category = arguments["category"] as? [String: Any] { - let sample = try Category.make(from: category) - return sample.copyWith( - startTimestamp: sample.startTimestamp.secondsSince1970, - endTimestamp: sample.endTimestamp.secondsSince1970 - ) - } - if let workout = arguments["workout"] as? [String: Any] { - let sample = try Workout.make(from: workout) - return sample.copyWith( - startTimestamp: sample.startTimestamp.secondsSince1970, - endTimestamp: sample.endTimestamp.secondsSince1970, - workoutEvents: sample.workoutEvents.map { event in - event.copyWith( - startTimestamp: event.startTimestamp.secondsSince1970, - endTimestamp: event.endTimestamp.secondsSince1970 - ) - } - ) - } - throw HealthKitError.invalidValue("Invalid arguments: \(arguments)") - } - private func throwParsingArgumentsError( - result: FlutterResult, - arguments: Any - ) { - result( - FlutterError( - code: className, - message: "Error in parsing arguments.", - details: "Arguments: \(String(describing: arguments))." - ) - ) - } - private func throwNoArgumentsError(result: FlutterResult) { - result( - FlutterError( - code: className, - message: "Error call arguments.", - details: "No arguments" - ) - ) - } - private func throwPlatformError( - result: FlutterResult, - error: Error - ) { - result( - FlutterError( - code: className, - message: "Error in platform method.", - details: error - ) - ) - } - private func throwSystemVersionError(result: FlutterResult) { - result( - FlutterError( - code: className, - message: "The current system version does not support method", - details: nil - ) - ) - } -} diff --git a/ios/Classes/WorkoutRouteQueryStreamHandler.swift b/ios/Classes/WorkoutRouteQueryStreamHandler.swift deleted file mode 100644 index 724ab5f..0000000 --- a/ios/Classes/WorkoutRouteQueryStreamHandler.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// WorkoutRouteQueryStreamHandler.swift -// health_kit_reporter -// -// Created by Victor Kachalov on 09.12.20. -// - -import Foundation -import HealthKitReporter - -public final class WorkoutRouteQueryStreamHandler: NSObject { - public let reporter: HealthKitReporter - public var activeQueries = Set() - public var plannedQueries = Set() - - init(reporter: HealthKitReporter) { - self.reporter = reporter - } -} -// MARK: - StreamHandlerProtocol -extension WorkoutRouteQueryStreamHandler: StreamHandlerProtocol { - public func setQueries(arguments: [String: Any], events: @escaping FlutterEventSink) throws { - guard - let startTimestamp = arguments["startTimestamp"] as? Double, - let endTimestamp = arguments["endTimestamp"] as? Double - else { - return - } - let predicate = NSPredicate.samplesPredicate( - startDate: Date.make(from: startTimestamp), - endDate: Date.make(from: endTimestamp) - ) - if #available(iOS 13.0, *) { - let query = try reporter.reader.workoutRouteQuery( - predicate: predicate - ) { (workoutRoute, error) in - guard - error == nil, - let workoutRoute = workoutRoute - else { - return - } - do { - events(try workoutRoute.encoded()) - } catch { - events(nil) - } - } - plannedQueries.insert(query) - } else { - events(nil) - } - } - - public static func make(with reporter: HealthKitReporter) -> WorkoutRouteQueryStreamHandler { - WorkoutRouteQueryStreamHandler(reporter: reporter) - } -} - -// MARK: - FlutterStreamHandler -extension WorkoutRouteQueryStreamHandler: FlutterStreamHandler { - public func onListen( - withArguments arguments: Any?, - eventSink events: @escaping FlutterEventSink - ) -> FlutterError? { - handleOnListen(withArguments: arguments, eventSink: events) - } - public func onCancel(withArguments arguments: Any?) -> FlutterError? { - handleOnCancel(withArguments: arguments) - } -} diff --git a/lib/health_kit_reporter.dart b/lib/health_kit_reporter.dart index 1a3729c..ba99ffd 100644 --- a/lib/health_kit_reporter.dart +++ b/lib/health_kit_reporter.dart @@ -134,29 +134,6 @@ class HealthKitReporter { static const EventChannel _anchoredObjectQueryChannel = EventChannel('health_kit_reporter_event_channel_anchored_object_query'); - /// [EventChannel] link to [SwiftHealthKitReporterPlugin.swift] - /// Will handle event exchanges of the plugin. - /// - static const EventChannel _workoutRouteQueryChannel = - EventChannel('health_kit_reporter_event_channel_workout_route_query'); - - /// Sets subscription for [WorkoutRoute] series. - /// Will call [onUpdate] callback, if - /// there were new series came from enumeration block until it is done. - /// Provide the [predicate] to set the date interval. - /// - static StreamSubscription workoutRouteQuery(Predicate predicate, - {required Function(WorkoutRoute) onUpdate}) { - final arguments = predicate.map; - return _workoutRouteQueryChannel - .receiveBroadcastStream(arguments) - .listen((event) { - final json = jsonDecode(event); - final workoutRoute = WorkoutRoute.fromJson(json); - onUpdate(workoutRoute); - }); - } - /// Sets subscription for data changes. /// Will call [onUpdate] callback, if /// there were changes regarding to the provided [identifier] @@ -330,7 +307,7 @@ class HealthKitReporter { return Characteristic.fromJson(map); } - /// Returns [HeartbeatSeriesSample] sample for the provided time interval predicate [predicate]. + /// Returns [HeartbeatSeries] sample for the provided time interval predicate [predicate]. /// static Future> heartbeatSeriesQuery( Predicate predicate) async { @@ -346,6 +323,22 @@ class HealthKitReporter { return series; } + /// Returns [WorkoutRoute] sample for the provided time interval predicate [predicate]. + /// + static Future> workoutRouteQuery( + Predicate predicate) async { + final arguments = predicate.map; + final result = + await _methodChannel.invokeMethod('workoutRouteQuery', arguments); + final List list = jsonDecode(result); + final routes = []; + for (final Map map in list) { + final sample = WorkoutRoute.fromJson(map); + routes.add(sample); + } + return routes; + } + /// Returns [Quantity] samples for the provided [type], /// the preferred [unit] and the time interval predicate [predicate]. /// diff --git a/lib/model/payload/heartbeat_series.dart b/lib/model/payload/heartbeat_series.dart index e871e42..d4f0ca3 100644 --- a/lib/model/payload/heartbeat_series.dart +++ b/lib/model/payload/heartbeat_series.dart @@ -55,6 +55,14 @@ class HeartbeatSeries extends Sample { json, HeartbeatSeriesHarmonized.fromJson(json['harmonized'])); } +/// Equivalent of [HeartbeatSeries.Harmonized] +/// from [HealthKitReporter] https://cocoapods.org/pods/HealthKitReporter +/// +/// Supports [map] representation. +/// +/// Has a [HeartbeatSeriesHarmonized.fromJson] constructor +/// to create instances from JSON payload coming from iOS native code. +/// class HeartbeatSeriesHarmonized { const HeartbeatSeriesHarmonized( this.count, diff --git a/lib/model/payload/workout_route.dart b/lib/model/payload/workout_route.dart index a0d5f5b..f54fc6c 100644 --- a/lib/model/payload/workout_route.dart +++ b/lib/model/payload/workout_route.dart @@ -1,3 +1,9 @@ +import 'package:health_kit_reporter/model/type/workout_type.dart'; + +import 'device.dart'; +import 'sample.dart'; +import 'source_revision.dart'; + /// Equivalent of [WorkoutRoute] /// from [HealthKitReporter] https://cocoapods.org/pods/HealthKitReporter /// @@ -6,10 +12,83 @@ /// Has a [WorkoutRoute.fromJson] constructor /// to create instances from JSON payload coming from iOS native code. /// -/// Requires [SeriesType] permissions provided. +/// Requires [SeriesType], [WorkoutType] permissions provided. /// -class WorkoutRoute { +class WorkoutRoute extends Sample { const WorkoutRoute( + String uuid, + String identifier, + num startTimestamp, + num endTimestamp, + Device? device, + SourceRevision sourceRevision, + WorkoutRouteHarmonized harmonized, + ) : super( + uuid, + identifier, + startTimestamp, + endTimestamp, + device, + sourceRevision, + harmonized, + ); + + /// General map representation + /// + @override + Map get map => { + 'uuid': uuid, + 'identifier': identifier, + 'startTimestamp': startTimestamp, + 'endTimestamp': endTimestamp, + 'device': device?.map, + 'sourceRevision': sourceRevision.map, + 'harmonized': harmonized.map, + }; + + /// General constructor from JSON payload + /// + WorkoutRoute.fromJson(Map json) + : super.from(json, WorkoutRouteHarmonized.fromJson(json['harmonized'])); +} + +/// Equivalent of [WorkoutRoute.Harmonized] +/// from [HealthKitReporter] https://cocoapods.org/pods/HealthKitReporter +/// +/// Supports [map] representation. +/// +/// Has a [WorkoutRouteHarmonized.fromJson] constructor +/// to create instances from JSON payload coming from iOS native code. +/// +class WorkoutRouteHarmonized { + const WorkoutRouteHarmonized( + this.count, + this.routes, + this.metadata, + ); + + final int count; + final List routes; + final Map? metadata; + + /// General map representation + /// + Map get map => { + 'count': count, + 'routes': routes.map((e) => e.map).toList(), + 'metadata': metadata, + }; + + /// General constructor from JSON payload + /// + WorkoutRouteHarmonized.fromJson(Map json) + : count = json['count'], + routes = WorkoutRouteBatch.collect(json['routes']), + metadata = json['metadata']; +} + +class WorkoutRouteBatch { + const WorkoutRouteBatch( this.locations, this.done, ); @@ -26,9 +105,20 @@ class WorkoutRoute { /// General constructor from JSON payload /// - WorkoutRoute.fromJson(Map json) - : locations = WorkoutRouteLocation.collect(json['timeSinceSeriesStart']), + WorkoutRouteBatch.fromJson(Map json) + : locations = WorkoutRouteLocation.collect(json['locations']), done = json['done']; + + /// Simplifies creating a list of objects from JSON payload. + /// + static List collect(List list) { + final samples = []; + for (final Map map in list) { + final sample = WorkoutRouteBatch.fromJson(map); + samples.add(sample); + } + return samples; + } } /// Equivalent of [WorkoutRoute.Location] @@ -61,11 +151,11 @@ class WorkoutRouteLocation { final num longitude; final num altitude; final num course; - final num courseAccuracy; - final num floor; + final num? courseAccuracy; + final num? floor; final num horizontalAccuracy; final num speed; - final num speedAccuracy; + final num? speedAccuracy; final num timestamp; final num verticalAccuracy; diff --git a/lib/model/type/series_type.dart b/lib/model/type/series_type.dart index 7672c64..aef3d19 100644 --- a/lib/model/type/series_type.dart +++ b/lib/model/type/series_type.dart @@ -6,7 +6,7 @@ /// enum SeriesType { heartbeatSeries, - route, + workoutRoute, } extension SeriesTypeIdentifier on SeriesType { @@ -14,7 +14,7 @@ extension SeriesTypeIdentifier on SeriesType { switch (this) { case SeriesType.heartbeatSeries: return 'HKDataTypeIdentifierHeartbeatSeries'; - case SeriesType.route: + case SeriesType.workoutRoute: return 'HKWorkoutRouteTypeIdentifier'; } } diff --git a/pubspec.lock b/pubspec.lock index 00e6c32..55859c8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,7 +7,7 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.8.1" + version: "2.8.2" boolean_selector: dependency: transitive description: @@ -21,7 +21,7 @@ packages: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" charcode: dependency: transitive description: @@ -80,7 +80,14 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.10" + version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" meta: dependency: transitive description: @@ -141,7 +148,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.8" typed_data: dependency: transitive description: @@ -155,7 +162,7 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.1.1" sdks: - dart: ">=2.12.0 <3.0.0" + dart: ">=2.14.0 <3.0.0" flutter: ">=1.20.0" diff --git a/pubspec.yaml b/pubspec.yaml index 7bc48b0..0369773 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: health_kit_reporter description: Helps to write or read data from Apple Health via HealthKit framework. -version: 2.0.0 +version: 2.0.1 homepage: https://github.com/VictorKachalov/health_kit_reporter environment: diff --git a/test/heartbeat_series_sample_test.dart b/test/heartbeat_series_test.dart similarity index 99% rename from test/heartbeat_series_sample_test.dart rename to test/heartbeat_series_test.dart index 3dbdf8a..f38015b 100644 --- a/test/heartbeat_series_sample_test.dart +++ b/test/heartbeat_series_test.dart @@ -2,7 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:health_kit_reporter/model/payload/heartbeat_series.dart'; void main() { - test('heartbeat_series_sample_parse_from_json', () { + test('heartbeat_series_parse_from_json', () { final json = { 'device': { 'softwareVersion': '8.0.1', diff --git a/test/workout_route_test.dart b/test/workout_route_test.dart new file mode 100644 index 0000000..a69f7c9 --- /dev/null +++ b/test/workout_route_test.dart @@ -0,0 +1,144 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:health_kit_reporter/model/payload/workout_route.dart'; + +void main() { + test('workout_route_parse_from_json', () { + final json = { + "device": { + "softwareVersion": "8.5.1", + "manufacturer": "Apple Inc.", + "model": "Watch", + "name": "Apple Watch", + "hardwareVersion": "Watch6,1" + }, + "sourceRevision": { + "productType": "Watch6,1", + "systemVersion": "8.5.1", + "source": { + "name": "Apple Watch von Victor", + "bundleIdentifier": + "com.apple.health.9482C212-CB6B-4949-A400-E448CCA82CEF" + }, + "operatingSystem": { + "majorVersion": 8, + "minorVersion": 5, + "patchVersion": 1 + }, + "version": "8.5.1" + }, + "uuid": "15431FED-4009-4764-9B4F-9FDD64827335", + "identifier": "HKWorkoutRouteTypeIdentifier", + "startTimestamp": 1650106382.259656, + "endTimestamp": 1650107789.9993167, + "harmonized": { + "count": 2, + "metadata": { + "HKMetadataKeySyncVersion": "2", + "HKMetadataKeySyncIdentifier": "8DA1E494-C047-4610-967F-267D60BD6E16" + }, + "routes": [ + { + "locations": [ + { + "floor": 1, + "course": 211.54032897949219, + "speed": 0.124705970287323, + "longitude": 13.354639734623429, + "horizontalAccuracy": 2.3436229228973389, + "verticalAccuracy": 1.3776830434799194, + "latitude": 52.517285348919636, + "courseAccuracy": 398.32894897460938, + "speedAccuracy": 0.86697620153427124, + "altitude": 36.923385620117188, + "timestamp": 1650106382.259656, + }, + { + "course": 212.49124145507812, + "speed": 0.14083768427371979, + "longitude": 13.354638966592331, + "horizontalAccuracy": 2.0688667297363281, + "verticalAccuracy": 1.2691746950149536, + "latitude": 52.517284600413831, + "courseAccuracy": 290.92919921875, + "speedAccuracy": 0.71512973308563232, + "altitude": 36.95831298828125, + "timestamp": 1650106382.9997792 + }, + ], + "done": false + }, + { + "locations": [ + { + "course": 181.96142578125, + "speed": 4.3713326454162598, + "longitude": 13.385890186794727, + "horizontalAccuracy": 3.2566030025482178, + "verticalAccuracy": 1.7064602375030518, + "latitude": 52.48740014749324, + "courseAccuracy": 6.4161453247070312, + "speedAccuracy": 0.53145873546600342, + "altitude": 46.786865234375, + "timestamp": 1650107481.9998665 + }, + { + "course": 182.047607421875, + "speed": 4.3631305694580078, + "longitude": 13.385887939659334, + "horizontalAccuracy": 3.2557823657989502, + "verticalAccuracy": 1.7118692398071289, + "latitude": 52.487360925517542, + "courseAccuracy": 6.4148092269897461, + "speedAccuracy": 0.53077465295791626, + "altitude": 46.917694091796875, + "timestamp": 1650107482.9998574 + }, + ], + "done": true + } + ] + } + }; + final sut = WorkoutRoute.fromJson(json); + expect(sut.uuid, '15431FED-4009-4764-9B4F-9FDD64827335'); + expect(sut.identifier, 'HKWorkoutRouteTypeIdentifier'); + expect(sut.startTimestamp, 1650106382.259656); + expect(sut.endTimestamp, 1650107789.9993167); + expect(sut.device?.name, 'Apple Watch'); + expect(sut.device?.manufacturer, 'Apple Inc.'); + expect(sut.device?.softwareVersion, '8.5.1'); + expect(sut.device?.model, 'Watch'); + expect(sut.device?.hardwareVersion, 'Watch6,1'); + expect(sut.sourceRevision.source.name, 'Apple Watch von Victor'); + expect(sut.sourceRevision.source.bundleIdentifier, + 'com.apple.health.9482C212-CB6B-4949-A400-E448CCA82CEF'); + expect(sut.sourceRevision.version, '8.5.1'); + expect(sut.sourceRevision.productType, 'Watch6,1'); + expect(sut.sourceRevision.systemVersion, '8.5.1'); + expect(sut.sourceRevision.operatingSystem.majorVersion, 8); + expect(sut.sourceRevision.operatingSystem.minorVersion, 5); + expect(sut.sourceRevision.operatingSystem.patchVersion, 1); + expect(sut.harmonized.count, 2); + expect(sut.harmonized.routes.length, 2); + expect(sut.harmonized.routes[0].done, false); + expect(sut.harmonized.routes[0].locations[0].course, 211.54032897949219); + expect(sut.harmonized.routes[0].locations[0].speed, 0.124705970287323); + expect(sut.harmonized.routes[0].locations[0].longitude, 13.354639734623429); + expect(sut.harmonized.routes[0].locations[0].horizontalAccuracy, + 2.3436229228973389); + expect(sut.harmonized.routes[0].locations[0].verticalAccuracy, + 1.3776830434799194); + expect(sut.harmonized.routes[0].locations[0].latitude, 52.517285348919636); + expect(sut.harmonized.routes[0].locations[0].courseAccuracy!, + 398.32894897460938); + expect(sut.harmonized.routes[0].locations[0].speedAccuracy!, + 0.86697620153427124); + expect(sut.harmonized.routes[0].locations[0].altitude, 36.923385620117188); + expect(sut.harmonized.routes[0].locations[0].timestamp, 1650106382.259656); + expect(sut.harmonized.routes[0].locations[0].floor, 1); + expect(sut.harmonized.metadata, { + "HKMetadataKeySyncVersion": "2", + "HKMetadataKeySyncIdentifier": "8DA1E494-C047-4610-967F-267D60BD6E16" + }); + }); +}