Showing
10 changed files
with
422 additions
and
41 deletions
| @@ -3,7 +3,7 @@ | @@ -3,7 +3,7 @@ | ||
| 3 | archiveVersion = 1; | 3 | archiveVersion = 1; |
| 4 | classes = { | 4 | classes = { |
| 5 | }; | 5 | }; |
| 6 | - objectVersion = 77; | 6 | + objectVersion = 54; |
| 7 | objects = { | 7 | objects = { |
| 8 | 8 | ||
| 9 | /* Begin PBXBuildFile section */ | 9 | /* Begin PBXBuildFile section */ |
| @@ -342,14 +342,10 @@ | @@ -342,14 +342,10 @@ | ||
| 342 | inputFileListPaths = ( | 342 | inputFileListPaths = ( |
| 343 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", | 343 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", |
| 344 | ); | 344 | ); |
| 345 | - inputPaths = ( | ||
| 346 | - ); | ||
| 347 | name = "[CP] Embed Pods Frameworks"; | 345 | name = "[CP] Embed Pods Frameworks"; |
| 348 | outputFileListPaths = ( | 346 | outputFileListPaths = ( |
| 349 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", | 347 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", |
| 350 | ); | 348 | ); |
| 351 | - outputPaths = ( | ||
| 352 | - ); | ||
| 353 | runOnlyForDeploymentPostprocessing = 0; | 349 | runOnlyForDeploymentPostprocessing = 0; |
| 354 | shellPath = /bin/sh; | 350 | shellPath = /bin/sh; |
| 355 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; | 351 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; |
| @@ -420,14 +416,10 @@ | @@ -420,14 +416,10 @@ | ||
| 420 | inputFileListPaths = ( | 416 | inputFileListPaths = ( |
| 421 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", | 417 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", |
| 422 | ); | 418 | ); |
| 423 | - inputPaths = ( | ||
| 424 | - ); | ||
| 425 | name = "[CP] Copy Pods Resources"; | 419 | name = "[CP] Copy Pods Resources"; |
| 426 | outputFileListPaths = ( | 420 | outputFileListPaths = ( |
| 427 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", | 421 | "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", |
| 428 | ); | 422 | ); |
| 429 | - outputPaths = ( | ||
| 430 | - ); | ||
| 431 | runOnlyForDeploymentPostprocessing = 0; | 423 | runOnlyForDeploymentPostprocessing = 0; |
| 432 | shellPath = /bin/sh; | 424 | shellPath = /bin/sh; |
| 433 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; | 425 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; |
| @@ -66,33 +66,32 @@ final class HealthKitService { | @@ -66,33 +66,32 @@ final class HealthKitService { | ||
| 66 | guard isHealthDataAvailable else { return false } | 66 | guard isHealthDataAvailable else { return false } |
| 67 | 67 | ||
| 68 | let endDate = Date() | 68 | let endDate = Date() |
| 69 | - let startDate = requestedStartDate | ||
| 70 | - ?? Calendar.current.date(byAdding: .year, value: -1, to: endDate) | ||
| 71 | - ?? Date(timeInterval: -1 * 365 * 24 * 60 * 60, since: endDate) | ||
| 72 | - | ||
| 73 | - if let sleepType = NativeHealthTypeCatalog.category(.sleepAnalysis), | ||
| 74 | - await hasAnyReadableSample( | ||
| 75 | - of: [sleepType], | ||
| 76 | - startDate: startDate, | 69 | + let sampleTypes = NativeHealthTypeCatalog.readTypes.compactMap { $0 as? HKSampleType } |
| 70 | + if await hasAnyReadableSample( | ||
| 71 | + of: sampleTypes, | ||
| 72 | + startDate: requestedStartDate, | ||
| 77 | endDate: endDate | 73 | endDate: endDate |
| 78 | ) { | 74 | ) { |
| 79 | return true | 75 | return true |
| 80 | } | 76 | } |
| 77 | + | ||
| 78 | + if NativeHealthTypeCatalog.readTypes.contains(.activitySummaryType()), | ||
| 79 | + await hasAnyReadableActivitySummary(startDate: requestedStartDate, endDate: endDate) { | ||
| 80 | + return true | ||
| 81 | + } | ||
| 81 | return false | 82 | return false |
| 82 | } | 83 | } |
| 83 | 84 | ||
| 84 | private func hasAnyReadableSample( | 85 | private func hasAnyReadableSample( |
| 85 | of sampleTypes: [HKSampleType], | 86 | of sampleTypes: [HKSampleType], |
| 86 | - startDate: Date, | 87 | + startDate: Date?, |
| 87 | endDate: Date | 88 | endDate: Date |
| 88 | ) async -> Bool { | 89 | ) async -> Bool { |
| 89 | guard !sampleTypes.isEmpty else { return false } | 90 | guard !sampleTypes.isEmpty else { return false } |
| 90 | 91 | ||
| 91 | - let predicate = HKQuery.predicateForSamples( | ||
| 92 | - withStart: startDate, | ||
| 93 | - end: endDate, | ||
| 94 | - options: [] | ||
| 95 | - ) | 92 | + let predicate = startDate.map { |
| 93 | + HKQuery.predicateForSamples(withStart: $0, end: endDate, options: []) | ||
| 94 | + } | ||
| 96 | let state = HealthReadableDataProbeState() | 95 | let state = HealthReadableDataProbeState() |
| 97 | 96 | ||
| 98 | return await withCheckedContinuation { continuation in | 97 | return await withCheckedContinuation { continuation in |
| @@ -119,6 +118,27 @@ final class HealthKitService { | @@ -119,6 +118,27 @@ final class HealthKitService { | ||
| 119 | } | 118 | } |
| 120 | } | 119 | } |
| 121 | 120 | ||
| 121 | + private func hasAnyReadableActivitySummary(startDate: Date?, endDate: Date) async -> Bool { | ||
| 122 | + let calendar = Calendar.current | ||
| 123 | + let start = startDate ?? Date(timeIntervalSince1970: 0) | ||
| 124 | + var startComponents = calendar.dateComponents([.era, .year, .month, .day], from: start) | ||
| 125 | + var endComponents = calendar.dateComponents([.era, .year, .month, .day], from: endDate) | ||
| 126 | + startComponents.calendar = calendar | ||
| 127 | + endComponents.calendar = calendar | ||
| 128 | + | ||
| 129 | + let predicate = HKQuery.predicate( | ||
| 130 | + forActivitySummariesBetweenStart: startComponents, | ||
| 131 | + end: endComponents | ||
| 132 | + ) | ||
| 133 | + | ||
| 134 | + return await withCheckedContinuation { continuation in | ||
| 135 | + let query = HKActivitySummaryQuery(predicate: predicate) { _, summaries, _ in | ||
| 136 | + continuation.resume(returning: summaries?.isEmpty == false) | ||
| 137 | + } | ||
| 138 | + healthStore.execute(query) | ||
| 139 | + } | ||
| 140 | + } | ||
| 141 | + | ||
| 122 | func startBackgroundObserversIfNeeded() { | 142 | func startBackgroundObserversIfNeeded() { |
| 123 | Task { @MainActor [weak self] in | 143 | Task { @MainActor [weak self] in |
| 124 | self?.observerController.startIfNeeded() | 144 | self?.observerController.startIfNeeded() |
| @@ -364,6 +364,8 @@ class HealthKitRawDataApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Se | @@ -364,6 +364,8 @@ class HealthKitRawDataApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Se | ||
| 364 | 364 | ||
| 365 | /// Generated protocol from Pigeon that represents a handler of messages from Flutter. | 365 | /// Generated protocol from Pigeon that represents a handler of messages from Flutter. |
| 366 | protocol HealthKitRawDataHostApi { | 366 | protocol HealthKitRawDataHostApi { |
| 367 | + /// 是否有AppleHealth数据 | ||
| 368 | + func hasHealthData(completion: @escaping (Result<Bool, Error>) -> Void) | ||
| 367 | /// 从AppleHealth 中读取原始数据 | 369 | /// 从AppleHealth 中读取原始数据 |
| 368 | func getHealthKitRawData(dataType: Int64, startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthKitRawDataPoint], Error>) -> Void) | 370 | func getHealthKitRawData(dataType: Int64, startTime: Int64, endTime: Int64, completion: @escaping (Result<[HealthKitRawDataPoint], Error>) -> Void) |
| 369 | /// 从从AppleHealth中读取睡眠数据 | 371 | /// 从从AppleHealth中读取睡眠数据 |
| @@ -382,6 +384,22 @@ class HealthKitRawDataHostApiSetup { | @@ -382,6 +384,22 @@ class HealthKitRawDataHostApiSetup { | ||
| 382 | /// Sets up an instance of `HealthKitRawDataHostApi` to handle messages through the `binaryMessenger`. | 384 | /// Sets up an instance of `HealthKitRawDataHostApi` to handle messages through the `binaryMessenger`. |
| 383 | static func setUp(binaryMessenger: FlutterBinaryMessenger, api: HealthKitRawDataHostApi?, messageChannelSuffix: String = "") { | 385 | static func setUp(binaryMessenger: FlutterBinaryMessenger, api: HealthKitRawDataHostApi?, messageChannelSuffix: String = "") { |
| 384 | let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" | 386 | let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" |
| 387 | + /// 是否有AppleHealth数据 | ||
| 388 | + let hasHealthDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.hasHealthData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | ||
| 389 | + if let api = api { | ||
| 390 | + hasHealthDataChannel.setMessageHandler { _, reply in | ||
| 391 | + api.hasHealthData { result in | ||
| 392 | + switch result { | ||
| 393 | + case .success(let res): | ||
| 394 | + reply(wrapResult(res)) | ||
| 395 | + case .failure(let error): | ||
| 396 | + reply(wrapError(error)) | ||
| 397 | + } | ||
| 398 | + } | ||
| 399 | + } | ||
| 400 | + } else { | ||
| 401 | + hasHealthDataChannel.setMessageHandler(nil) | ||
| 402 | + } | ||
| 385 | /// 从AppleHealth 中读取原始数据 | 403 | /// 从AppleHealth 中读取原始数据 |
| 386 | let getHealthKitRawDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.getHealthKitRawData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | 404 | let getHealthKitRawDataChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.getHealthKitRawData\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) |
| 387 | if let api = api { | 405 | if let api = api { |
| 1 | import Foundation | 1 | import Foundation |
| 2 | 2 | ||
| 3 | final class HealthKitRawDataHostApiImpl: HealthKitRawDataHostApi { | 3 | final class HealthKitRawDataHostApiImpl: HealthKitRawDataHostApi { |
| 4 | + | ||
| 4 | private let service: HealthKitService | 5 | private let service: HealthKitService |
| 5 | 6 | ||
| 6 | init(service: HealthKitService = .shared) { | 7 | init(service: HealthKitService = .shared) { |
| 7 | self.service = service | 8 | self.service = service |
| 8 | } | 9 | } |
| 10 | + | ||
| 11 | + func hasHealthData(completion: @escaping (Result<Bool, Error>) -> Void) { | ||
| 12 | + Task { | ||
| 13 | + let hasData = await service.hasAnyReadableData() | ||
| 14 | + completion(.success(hasData)) | ||
| 15 | + } | ||
| 16 | + } | ||
| 9 | 17 | ||
| 10 | func getHealthKitRawData( | 18 | func getHealthKitRawData( |
| 11 | dataType: Int64, | 19 | dataType: Int64, |
| @@ -57,6 +57,10 @@ class HealthRawDataCoreService { | @@ -57,6 +57,10 @@ class HealthRawDataCoreService { | ||
| 57 | return _localStore.closeDatabase(userId: _userId); | 57 | return _localStore.closeDatabase(userId: _userId); |
| 58 | } | 58 | } |
| 59 | 59 | ||
| 60 | + Future<bool> hasHealthData() { | ||
| 61 | + return _rawDataApi.hasHealthData(); | ||
| 62 | + } | ||
| 63 | + | ||
| 60 | Stream<HealthRawDataUpdatedEvent> get healthDataUpdatedStream => | 64 | Stream<HealthRawDataUpdatedEvent> get healthDataUpdatedStream => |
| 61 | _healthDataUpdatedController.stream; | 65 | _healthDataUpdatedController.stream; |
| 62 | 66 | ||
| @@ -732,6 +736,7 @@ class HealthRawDataCoreService { | @@ -732,6 +736,7 @@ class HealthRawDataCoreService { | ||
| 732 | HealthRawHrvStressPoint( | 736 | HealthRawHrvStressPoint( |
| 733 | userId: userId, | 737 | userId: userId, |
| 734 | rawEndTime: point.endTime, | 738 | rawEndTime: point.endTime, |
| 739 | + rawHrv: pointValue, | ||
| 735 | result: trendHrv, | 740 | result: trendHrv, |
| 736 | sourceStartTime: sourceRange.start, | 741 | sourceStartTime: sourceRange.start, |
| 737 | sourceEndTime: sourceRange.end, | 742 | sourceEndTime: sourceRange.end, |
| @@ -1263,6 +1268,7 @@ class HealthRawHrvStressPoint { | @@ -1263,6 +1268,7 @@ class HealthRawHrvStressPoint { | ||
| 1263 | const HealthRawHrvStressPoint({ | 1268 | const HealthRawHrvStressPoint({ |
| 1264 | required this.userId, | 1269 | required this.userId, |
| 1265 | required this.rawEndTime, | 1270 | required this.rawEndTime, |
| 1271 | + required this.rawHrv, | ||
| 1266 | required this.result, | 1272 | required this.result, |
| 1267 | required this.sourceStartTime, | 1273 | required this.sourceStartTime, |
| 1268 | required this.sourceEndTime, | 1274 | required this.sourceEndTime, |
| @@ -1277,6 +1283,7 @@ class HealthRawHrvStressPoint { | @@ -1277,6 +1283,7 @@ class HealthRawHrvStressPoint { | ||
| 1277 | 1283 | ||
| 1278 | final int userId; | 1284 | final int userId; |
| 1279 | final int rawEndTime; | 1285 | final int rawEndTime; |
| 1286 | + final double rawHrv; | ||
| 1280 | final double result; | 1287 | final double result; |
| 1281 | final int sourceStartTime; | 1288 | final int sourceStartTime; |
| 1282 | final int sourceEndTime; | 1289 | final int sourceEndTime; |
| @@ -1290,16 +1297,18 @@ class HealthRawHrvStressPoint { | @@ -1290,16 +1297,18 @@ class HealthRawHrvStressPoint { | ||
| 1290 | 1297 | ||
| 1291 | factory HealthRawHrvStressPoint.fromDb(Map<String, Object?> row) { | 1298 | factory HealthRawHrvStressPoint.fromDb(Map<String, Object?> row) { |
| 1292 | final result = (row['result'] as num).toDouble(); | 1299 | final result = (row['result'] as num).toDouble(); |
| 1300 | + final rawHrv = _positiveDouble(row['raw_hrv']) ?? result; | ||
| 1293 | final baselineHrv = _positiveDouble(row['baseline_hrv']) ?? result; | 1301 | final baselineHrv = _positiveDouble(row['baseline_hrv']) ?? result; |
| 1294 | final baselineAwakeHrv = | 1302 | final baselineAwakeHrv = |
| 1295 | _positiveDouble(row['baseline_awake_hrv']) ?? baselineHrv; | 1303 | _positiveDouble(row['baseline_awake_hrv']) ?? baselineHrv; |
| 1296 | return HealthRawHrvStressPoint( | 1304 | return HealthRawHrvStressPoint( |
| 1297 | userId: row['user_id'] as int, | 1305 | userId: row['user_id'] as int, |
| 1298 | rawEndTime: row['raw_end_time'] as int, | 1306 | rawEndTime: row['raw_end_time'] as int, |
| 1307 | + rawHrv: rawHrv, | ||
| 1299 | result: result, | 1308 | result: result, |
| 1300 | sourceStartTime: row['source_start_time'] as int, | 1309 | sourceStartTime: row['source_start_time'] as int, |
| 1301 | sourceEndTime: row['source_end_time'] as int, | 1310 | sourceEndTime: row['source_end_time'] as int, |
| 1302 | - state: HealthRawStressState.normal, | 1311 | + state: _stressStateFromDb(row['state']) ?? HealthRawStressState.normal, |
| 1303 | baselineHrv: baselineHrv, | 1312 | baselineHrv: baselineHrv, |
| 1304 | baselineAwakeHrv: baselineAwakeHrv, | 1313 | baselineAwakeHrv: baselineAwakeHrv, |
| 1305 | baselineSleepHrv: _positiveDouble(row['baseline_sleep_hrv']), | 1314 | baselineSleepHrv: _positiveDouble(row['baseline_sleep_hrv']), |
| @@ -1329,6 +1338,8 @@ class HealthRawRealtimeStressPoint { | @@ -1329,6 +1338,8 @@ class HealthRawRealtimeStressPoint { | ||
| 1329 | final HealthRawPointFlags flags; | 1338 | final HealthRawPointFlags flags; |
| 1330 | final bool uploaded; | 1339 | final bool uploaded; |
| 1331 | 1340 | ||
| 1341 | + HealthRawStressState get state => healthRawRealtimeStressState(result); | ||
| 1342 | + | ||
| 1332 | factory HealthRawRealtimeStressPoint.fromDb(Map<String, Object?> row) { | 1343 | factory HealthRawRealtimeStressPoint.fromDb(Map<String, Object?> row) { |
| 1333 | return HealthRawRealtimeStressPoint( | 1344 | return HealthRawRealtimeStressPoint( |
| 1334 | userId: row['user_id'] as int, | 1345 | userId: row['user_id'] as int, |
| @@ -1342,6 +1353,13 @@ class HealthRawRealtimeStressPoint { | @@ -1342,6 +1353,13 @@ class HealthRawRealtimeStressPoint { | ||
| 1342 | } | 1353 | } |
| 1343 | } | 1354 | } |
| 1344 | 1355 | ||
| 1356 | +HealthRawStressState healthRawRealtimeStressState(num value) { | ||
| 1357 | + if (value >= 81) return HealthRawStressState.overload; | ||
| 1358 | + if (value >= 61) return HealthRawStressState.attention; | ||
| 1359 | + if (value >= 21) return HealthRawStressState.normal; | ||
| 1360 | + return HealthRawStressState.excellent; | ||
| 1361 | +} | ||
| 1362 | + | ||
| 1345 | HealthRawPointFlags _flagsFromDb(Map<String, Object?> row) { | 1363 | HealthRawPointFlags _flagsFromDb(Map<String, Object?> row) { |
| 1346 | return HealthRawPointFlags( | 1364 | return HealthRawPointFlags( |
| 1347 | isWorkout: (row['is_workout'] as int? ?? 0) == 1, | 1365 | isWorkout: (row['is_workout'] as int? ?? 0) == 1, |
| @@ -1357,6 +1375,15 @@ double? _positiveDouble(Object? value) { | @@ -1357,6 +1375,15 @@ double? _positiveDouble(Object? value) { | ||
| 1357 | return result; | 1375 | return result; |
| 1358 | } | 1376 | } |
| 1359 | 1377 | ||
| 1378 | +HealthRawStressState? _stressStateFromDb(Object? value) { | ||
| 1379 | + final stateValue = value as int?; | ||
| 1380 | + if (stateValue == null) return null; | ||
| 1381 | + for (final state in HealthRawStressState.values) { | ||
| 1382 | + if (state.value == stateValue) return state; | ||
| 1383 | + } | ||
| 1384 | + return null; | ||
| 1385 | +} | ||
| 1386 | + | ||
| 1360 | class HealthRawStressDbRows { | 1387 | class HealthRawStressDbRows { |
| 1361 | const HealthRawStressDbRows({ | 1388 | const HealthRawStressDbRows({ |
| 1362 | required this.hrvRows, | 1389 | required this.hrvRows, |
| @@ -1542,7 +1569,7 @@ class HealthRawStressLocalStore { | @@ -1542,7 +1569,7 @@ class HealthRawStressLocalStore { | ||
| 1542 | final db = await factory.openDatabase( | 1569 | final db = await factory.openDatabase( |
| 1543 | path, | 1570 | path, |
| 1544 | options: OpenDatabaseOptions( | 1571 | options: OpenDatabaseOptions( |
| 1545 | - version: 3, | 1572 | + version: 4, |
| 1546 | onCreate: (db, version) async { | 1573 | onCreate: (db, version) async { |
| 1547 | await _createTables(db); | 1574 | await _createTables(db); |
| 1548 | }, | 1575 | }, |
| @@ -1554,6 +1581,9 @@ class HealthRawStressLocalStore { | @@ -1554,6 +1581,9 @@ class HealthRawStressLocalStore { | ||
| 1554 | if (oldVersion < 3) { | 1581 | if (oldVersion < 3) { |
| 1555 | await _addHrvBaselineColumns(db); | 1582 | await _addHrvBaselineColumns(db); |
| 1556 | } | 1583 | } |
| 1584 | + if (oldVersion < 4) { | ||
| 1585 | + await _addHrvRawAndStateColumns(db); | ||
| 1586 | + } | ||
| 1557 | }, | 1587 | }, |
| 1558 | ), | 1588 | ), |
| 1559 | ); | 1589 | ); |
| @@ -1566,9 +1596,11 @@ class HealthRawStressLocalStore { | @@ -1566,9 +1596,11 @@ class HealthRawStressLocalStore { | ||
| 1566 | CREATE TABLE IF NOT EXISTS $hrvResultsTable ( | 1596 | CREATE TABLE IF NOT EXISTS $hrvResultsTable ( |
| 1567 | raw_end_time INTEGER PRIMARY KEY, | 1597 | raw_end_time INTEGER PRIMARY KEY, |
| 1568 | user_id INTEGER NOT NULL, | 1598 | user_id INTEGER NOT NULL, |
| 1599 | + raw_hrv REAL NOT NULL DEFAULT 0, | ||
| 1569 | result REAL NOT NULL, | 1600 | result REAL NOT NULL, |
| 1570 | source_start_time INTEGER NOT NULL, | 1601 | source_start_time INTEGER NOT NULL, |
| 1571 | source_end_time INTEGER NOT NULL, | 1602 | source_end_time INTEGER NOT NULL, |
| 1603 | + state INTEGER NOT NULL DEFAULT 3, | ||
| 1572 | baseline_hrv REAL NOT NULL DEFAULT 0, | 1604 | baseline_hrv REAL NOT NULL DEFAULT 0, |
| 1573 | baseline_awake_hrv REAL NOT NULL DEFAULT 0, | 1605 | baseline_awake_hrv REAL NOT NULL DEFAULT 0, |
| 1574 | baseline_sleep_hrv REAL, | 1606 | baseline_sleep_hrv REAL, |
| @@ -1630,6 +1662,21 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable ( | @@ -1630,6 +1662,21 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable ( | ||
| 1630 | } | 1662 | } |
| 1631 | } | 1663 | } |
| 1632 | 1664 | ||
| 1665 | + Future<void> _addHrvRawAndStateColumns(DatabaseExecutor db) async { | ||
| 1666 | + for (final columnSql in const [ | ||
| 1667 | + 'raw_hrv REAL NOT NULL DEFAULT 0', | ||
| 1668 | + 'state INTEGER NOT NULL DEFAULT 3', | ||
| 1669 | + ]) { | ||
| 1670 | + try { | ||
| 1671 | + await db.execute( | ||
| 1672 | + 'ALTER TABLE $hrvResultsTable ADD COLUMN $columnSql', | ||
| 1673 | + ); | ||
| 1674 | + } on DatabaseException catch (error) { | ||
| 1675 | + if (!error.isDuplicateColumnError()) rethrow; | ||
| 1676 | + } | ||
| 1677 | + } | ||
| 1678 | + } | ||
| 1679 | + | ||
| 1633 | Future<int?> _latestSourceStartTime(int userId, String table) async { | 1680 | Future<int?> _latestSourceStartTime(int userId, String table) async { |
| 1634 | final db = await _database(userId); | 1681 | final db = await _database(userId); |
| 1635 | final rows = await db.query( | 1682 | final rows = await db.query( |
| @@ -1662,9 +1709,11 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable ( | @@ -1662,9 +1709,11 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable ( | ||
| 1662 | return <String, Object?>{ | 1709 | return <String, Object?>{ |
| 1663 | 'raw_end_time': point.rawEndTime, | 1710 | 'raw_end_time': point.rawEndTime, |
| 1664 | 'user_id': point.userId, | 1711 | 'user_id': point.userId, |
| 1712 | + 'raw_hrv': point.rawHrv, | ||
| 1665 | 'result': point.result, | 1713 | 'result': point.result, |
| 1666 | 'source_start_time': point.sourceStartTime, | 1714 | 'source_start_time': point.sourceStartTime, |
| 1667 | 'source_end_time': point.sourceEndTime, | 1715 | 'source_end_time': point.sourceEndTime, |
| 1716 | + 'state': point.state.value, | ||
| 1668 | 'baseline_hrv': point.baselineHrv, | 1717 | 'baseline_hrv': point.baselineHrv, |
| 1669 | 'baseline_awake_hrv': point.baselineAwakeHrv, | 1718 | 'baseline_awake_hrv': point.baselineAwakeHrv, |
| 1670 | 'baseline_sleep_hrv': point.baselineSleepHrv, | 1719 | 'baseline_sleep_hrv': point.baselineSleepHrv, |
| @@ -1709,9 +1758,11 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable ( | @@ -1709,9 +1758,11 @@ CREATE TABLE IF NOT EXISTS $realtimeStressResultsTable ( | ||
| 1709 | table, | 1758 | table, |
| 1710 | <String, Object?>{ | 1759 | <String, Object?>{ |
| 1711 | 'user_id': row['user_id'], | 1760 | 'user_id': row['user_id'], |
| 1761 | + if (row.containsKey('raw_hrv')) 'raw_hrv': row['raw_hrv'], | ||
| 1712 | 'result': row['result'], | 1762 | 'result': row['result'], |
| 1713 | 'source_start_time': row['source_start_time'], | 1763 | 'source_start_time': row['source_start_time'], |
| 1714 | 'source_end_time': row['source_end_time'], | 1764 | 'source_end_time': row['source_end_time'], |
| 1765 | + if (row.containsKey('state')) 'state': row['state'], | ||
| 1715 | if (row.containsKey('baseline_hrv')) | 1766 | if (row.containsKey('baseline_hrv')) |
| 1716 | 'baseline_hrv': row['baseline_hrv'], | 1767 | 'baseline_hrv': row['baseline_hrv'], |
| 1717 | if (row.containsKey('baseline_awake_hrv')) | 1768 | if (row.containsKey('baseline_awake_hrv')) |
| @@ -3,6 +3,7 @@ import 'dart:math' as math; | @@ -3,6 +3,7 @@ import 'dart:math' as math; | ||
| 3 | import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart'; | 3 | import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart'; |
| 4 | import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; | 4 | import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; |
| 5 | import 'package:doublefeel_flutter/data/models/health/activity/activity_burn_statistics_data_v2.dart'; | 5 | import 'package:doublefeel_flutter/data/models/health/activity/activity_burn_statistics_data_v2.dart'; |
| 6 | +import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart'; | ||
| 6 | import 'package:doublefeel_flutter/data/models/health/hrv/hrv_statistics_data.dart'; | 7 | import 'package:doublefeel_flutter/data/models/health/hrv/hrv_statistics_data.dart'; |
| 7 | import 'package:doublefeel_flutter/data/models/health/sleep/sleep_statistics_data.dart'; | 8 | import 'package:doublefeel_flutter/data/models/health/sleep/sleep_statistics_data.dart'; |
| 8 | import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'; | 9 | import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'; |
| @@ -253,6 +254,135 @@ class LocalHealthDataConvert { | @@ -253,6 +254,135 @@ class LocalHealthDataConvert { | ||
| 253 | ); | 254 | ); |
| 254 | } | 255 | } |
| 255 | 256 | ||
| 257 | + static V2ActivityTarget v2ActivityTarget({ | ||
| 258 | + required int userId, | ||
| 259 | + required List<HealthKitRawActivityDataPoint> activity, | ||
| 260 | + }) { | ||
| 261 | + final latestWithGoal = activity | ||
| 262 | + .where(_hasActivityGoal) | ||
| 263 | + .fold<HealthKitRawActivityDataPoint?>( | ||
| 264 | + null, | ||
| 265 | + (latest, item) => | ||
| 266 | + latest == null || item.endTime > latest.endTime ? item : latest, | ||
| 267 | + ); | ||
| 268 | + return V2ActivityTarget( | ||
| 269 | + userId: userId, | ||
| 270 | + move: latestWithGoal?.activeEnergyBurnedGoal?.round(), | ||
| 271 | + step: latestWithGoal?.appleMoveTimeGoal?.round(), | ||
| 272 | + stand: latestWithGoal?.standHoursGoal?.round(), | ||
| 273 | + exercise: latestWithGoal?.exerciseTimeGoal == null | ||
| 274 | + ? null | ||
| 275 | + : (latestWithGoal!.exerciseTimeGoal! * 60).round(), | ||
| 276 | + updateTime: latestWithGoal?.endTime, | ||
| 277 | + sleepTargetDuration: _sleepGoalMinutes.round(), | ||
| 278 | + ); | ||
| 279 | + } | ||
| 280 | + | ||
| 281 | + static V2HealthData v2HealthData({ | ||
| 282 | + required DateTime day, | ||
| 283 | + required List<HealthRawHrvStressPoint> hrvPoints, | ||
| 284 | + required List<HealthKitRawActivityDataPoint> activity, | ||
| 285 | + required List<HealthKitRawDataPoint> heartRate, | ||
| 286 | + required List<HealthKitRawDataPoint> restingHeartRate, | ||
| 287 | + required List<HealthKitRawDataPoint> sleepIntervals, | ||
| 288 | + }) { | ||
| 289 | + final activitySummary = _activityDaySummary(day, activity); | ||
| 290 | + final sleepSummary = _sleepDaySummary(day, sleepIntervals, heartRate); | ||
| 291 | + final dayStart = unixSeconds(day); | ||
| 292 | + final dayEnd = unixSeconds(day.add(const Duration(days: 1))); | ||
| 293 | + final sleepWindowStart = | ||
| 294 | + unixSeconds(day.subtract(const Duration(hours: 12))); | ||
| 295 | + final sleepWindowEnd = unixSeconds(day.add(const Duration(hours: 12))); | ||
| 296 | + final sleepTimeList = sleepIntervals | ||
| 297 | + .where( | ||
| 298 | + (e) => e.endTime > sleepWindowStart && e.startTime < sleepWindowEnd) | ||
| 299 | + .map( | ||
| 300 | + (e) => V2SleepTimeRange( | ||
| 301 | + fromTime: math.max(e.startTime, sleepWindowStart), | ||
| 302 | + toTime: math.min(e.endTime, sleepWindowEnd), | ||
| 303 | + ), | ||
| 304 | + ) | ||
| 305 | + .toList(); | ||
| 306 | + final dayHr = heartRate | ||
| 307 | + .where((e) => e.endTime >= dayStart && e.endTime < dayEnd) | ||
| 308 | + .toList(); | ||
| 309 | + final dayRestingHr = restingHeartRate | ||
| 310 | + .where((e) => e.endTime >= dayStart && e.endTime < dayEnd) | ||
| 311 | + .toList() | ||
| 312 | + ..sort((a, b) => a.endTime.compareTo(b.endTime)); | ||
| 313 | + | ||
| 314 | + return V2HealthData( | ||
| 315 | + hrvAvg: _averageOrNull(hrvPoints.map((e) => e.result).toList())?.round(), | ||
| 316 | + lastRestingHrValue: | ||
| 317 | + dayRestingHr.isEmpty ? null : dayRestingHr.last.value?.round(), | ||
| 318 | + hrAvg: _averageOrNull(dayHr.map((e) => e.value).whereType<num>().toList()) | ||
| 319 | + ?.round(), | ||
| 320 | + move: activitySummary.move.round(), | ||
| 321 | + exercise: activitySummary.exerciseSeconds.round(), | ||
| 322 | + stand: activitySummary.standHours.round(), | ||
| 323 | + steps: activitySummary.steps.round(), | ||
| 324 | + sleepDuration: sleepSummary.durationSeconds.round(), | ||
| 325 | + sleepScore: sleepSummary.score?.toDouble(), | ||
| 326 | + sleepState: sleepSummary.evaluate?.round(), | ||
| 327 | + sleepTimeList: sleepTimeList, | ||
| 328 | + ); | ||
| 329 | + } | ||
| 330 | + | ||
| 331 | + static V2HrvTrendData v2HrvTrendData( | ||
| 332 | + List<HealthRawHrvStressPoint> hrvPoints, | ||
| 333 | + ) { | ||
| 334 | + return V2HrvTrendData( | ||
| 335 | + list: [ | ||
| 336 | + for (final point in hrvPoints) | ||
| 337 | + V2HrvTrendItem( | ||
| 338 | + time: point.rawEndTime, | ||
| 339 | + rawHrv: point.rawHrv, | ||
| 340 | + trendHrv: point.result.round(), | ||
| 341 | + state: point.state.value, | ||
| 342 | + ), | ||
| 343 | + ], | ||
| 344 | + ); | ||
| 345 | + } | ||
| 346 | + | ||
| 347 | + static V2LatestHrvData v2LatestHrvData( | ||
| 348 | + List<HealthRawHrvStressPoint> hrvPoints, | ||
| 349 | + ) { | ||
| 350 | + final latest = _latestHrvByRawEndTime(hrvPoints); | ||
| 351 | + return V2LatestHrvData( | ||
| 352 | + latestHrv: latest?.result, | ||
| 353 | + latestDataTime: latest?.rawEndTime, | ||
| 354 | + state: latest?.state.value, | ||
| 355 | + ); | ||
| 356 | + } | ||
| 357 | + | ||
| 358 | + static V2RealtimeStressData v2RealtimeStressData( | ||
| 359 | + List<HealthRawRealtimeStressPoint> realtimePoints, | ||
| 360 | + ) { | ||
| 361 | + return V2RealtimeStressData( | ||
| 362 | + list: [ | ||
| 363 | + for (final point in realtimePoints) | ||
| 364 | + V2RealtimeStressItem( | ||
| 365 | + time: point.rawEndTime, | ||
| 366 | + value: point.result.round(), | ||
| 367 | + state: point.state.value, | ||
| 368 | + ), | ||
| 369 | + ], | ||
| 370 | + ); | ||
| 371 | + } | ||
| 372 | + | ||
| 373 | + static V2StressScore v2StressScore( | ||
| 374 | + List<HealthRawRealtimeStressPoint> realtimePoints, | ||
| 375 | + ) { | ||
| 376 | + final averageStress = _averageOrNull( | ||
| 377 | + realtimePoints.map((e) => e.result).toList(), | ||
| 378 | + ); | ||
| 379 | + final score = averageStress?.round(); | ||
| 380 | + return V2StressScore( | ||
| 381 | + state: score == null ? 0 : healthRawRealtimeStressState(score).value, | ||
| 382 | + comprehensiveScore: score, | ||
| 383 | + ); | ||
| 384 | + } | ||
| 385 | + | ||
| 256 | static _SleepDaySummary _sleepDaySummary( | 386 | static _SleepDaySummary _sleepDaySummary( |
| 257 | DateTime day, | 387 | DateTime day, |
| 258 | List<HealthKitRawDataPoint> sleepIntervals, | 388 | List<HealthKitRawDataPoint> sleepIntervals, |
| @@ -311,6 +441,23 @@ class LocalHealthDataConvert { | @@ -311,6 +441,23 @@ class LocalHealthDataConvert { | ||
| 311 | ); | 441 | ); |
| 312 | } | 442 | } |
| 313 | 443 | ||
| 444 | + static bool _hasActivityGoal(HealthKitRawActivityDataPoint item) { | ||
| 445 | + return item.activeEnergyBurnedGoal != null || | ||
| 446 | + item.appleMoveTimeGoal != null || | ||
| 447 | + item.standHoursGoal != null || | ||
| 448 | + item.exerciseTimeGoal != null; | ||
| 449 | + } | ||
| 450 | + | ||
| 451 | + static HealthRawHrvStressPoint? _latestHrvByRawEndTime( | ||
| 452 | + List<HealthRawHrvStressPoint> points, | ||
| 453 | + ) { | ||
| 454 | + return points.fold<HealthRawHrvStressPoint?>( | ||
| 455 | + null, | ||
| 456 | + (latest, item) => | ||
| 457 | + latest == null || item.rawEndTime > latest.rawEndTime ? item : latest, | ||
| 458 | + ); | ||
| 459 | + } | ||
| 460 | + | ||
| 314 | static _ActivityDaySummary? _latestActivitySummaryWithGoal( | 461 | static _ActivityDaySummary? _latestActivitySummaryWithGoal( |
| 315 | List<_ActivityDaySummary> daily, | 462 | List<_ActivityDaySummary> daily, |
| 316 | ) { | 463 | ) { |
| @@ -144,51 +144,159 @@ class LocalHealthDataSource implements HealthDataSource { | @@ -144,51 +144,159 @@ class LocalHealthDataSource implements HealthDataSource { | ||
| 144 | 144 | ||
| 145 | @override | 145 | @override |
| 146 | Future<AppResult<V2HealthEverUploaded>> getHealthDataEverUploaded() async { | 146 | Future<AppResult<V2HealthEverUploaded>> getHealthDataEverUploaded() async { |
| 147 | - var data = V2HealthEverUploaded(flag: true); | ||
| 148 | - return AppSuccess(data); | 147 | + try { |
| 148 | + final hasHealthData = await coreService.hasHealthData(); | ||
| 149 | + return AppSuccess(V2HealthEverUploaded(flag: hasHealthData)); | ||
| 150 | + } catch (error) { | ||
| 151 | + return AppFailure(AppUnknownError(error)); | ||
| 152 | + } | ||
| 149 | } | 153 | } |
| 150 | 154 | ||
| 151 | @override | 155 | @override |
| 152 | Future<AppResult<V2ActivityTarget>> getV2ActivityTarget( | 156 | Future<AppResult<V2ActivityTarget>> getV2ActivityTarget( |
| 153 | int? queryUserId) async { | 157 | int? queryUserId) async { |
| 154 | - var data = V2ActivityTarget( | ||
| 155 | - sleepTargetDuration: 480, | ||
| 156 | - ); | ||
| 157 | - return AppSuccess(data); | 158 | + try { |
| 159 | + final now = DateTime.now(); | ||
| 160 | + final queryStart = LocalHealthDataConvert.unixSeconds( | ||
| 161 | + now.subtract( | ||
| 162 | + const Duration(days: HealthRawDataCoreService.defaultLookbackDays)), | ||
| 163 | + ); | ||
| 164 | + final queryEnd = LocalHealthDataConvert.unixSeconds(now); | ||
| 165 | + final activity = await coreService.queryRawActivitySummaries( | ||
| 166 | + startTime: queryStart, | ||
| 167 | + endTime: queryEnd, | ||
| 168 | + ); | ||
| 169 | + return AppSuccess( | ||
| 170 | + LocalHealthDataConvert.v2ActivityTarget( | ||
| 171 | + userId: 0, | ||
| 172 | + activity: activity, | ||
| 173 | + ), | ||
| 174 | + ); | ||
| 175 | + } catch (error) { | ||
| 176 | + return AppFailure(AppUnknownError(error)); | ||
| 177 | + } | ||
| 158 | } | 178 | } |
| 159 | 179 | ||
| 160 | @override | 180 | @override |
| 161 | Future<AppResult<V2HealthData>> getV2HealthData( | 181 | Future<AppResult<V2HealthData>> getV2HealthData( |
| 162 | int? queryUserId, int intDate) async { | 182 | int? queryUserId, int intDate) async { |
| 163 | - var data = V2HealthData(hrvAvg: 10); | ||
| 164 | - return AppSuccess(data); | 183 | + try { |
| 184 | + final day = LocalHealthDataConvert.dateFromKey(intDate); | ||
| 185 | + final dayStart = LocalHealthDataConvert.unixSeconds(day); | ||
| 186 | + final dayEnd = LocalHealthDataConvert.unixSeconds( | ||
| 187 | + day.add(const Duration(days: 1)), | ||
| 188 | + ) - | ||
| 189 | + 1; | ||
| 190 | + final sleepStart = LocalHealthDataConvert.unixSeconds( | ||
| 191 | + day.subtract(const Duration(hours: 12)), | ||
| 192 | + ); | ||
| 193 | + final sleepEnd = LocalHealthDataConvert.unixSeconds( | ||
| 194 | + day.add(const Duration(hours: 12)), | ||
| 195 | + ); | ||
| 196 | + final hrvPoints = await coreService.queryHrvStressPoints( | ||
| 197 | + startTime: dayStart, | ||
| 198 | + endTime: dayEnd, | ||
| 199 | + ); | ||
| 200 | + final activity = await coreService.queryRawActivitySummaries( | ||
| 201 | + startTime: dayStart, | ||
| 202 | + endTime: dayEnd, | ||
| 203 | + ); | ||
| 204 | + final heartRate = await coreService.queryRawDataPoints( | ||
| 205 | + dataType: HealthDataUploadType.heartRate.type, | ||
| 206 | + startTime: sleepStart, | ||
| 207 | + endTime: dayEnd, | ||
| 208 | + ); | ||
| 209 | + final restingHeartRate = await coreService.queryRawDataPoints( | ||
| 210 | + dataType: HealthDataUploadType.restingHeartRate.type, | ||
| 211 | + startTime: dayStart, | ||
| 212 | + endTime: dayEnd, | ||
| 213 | + ); | ||
| 214 | + final sleepIntervals = await coreService.queryRawSleepIntervals( | ||
| 215 | + startTime: sleepStart, | ||
| 216 | + endTime: sleepEnd, | ||
| 217 | + ); | ||
| 218 | + return AppSuccess( | ||
| 219 | + LocalHealthDataConvert.v2HealthData( | ||
| 220 | + day: day, | ||
| 221 | + hrvPoints: hrvPoints, | ||
| 222 | + activity: activity, | ||
| 223 | + heartRate: heartRate, | ||
| 224 | + restingHeartRate: restingHeartRate, | ||
| 225 | + sleepIntervals: sleepIntervals, | ||
| 226 | + ), | ||
| 227 | + ); | ||
| 228 | + } catch (error) { | ||
| 229 | + return AppFailure(AppUnknownError(error)); | ||
| 230 | + } | ||
| 165 | } | 231 | } |
| 166 | 232 | ||
| 167 | @override | 233 | @override |
| 168 | Future<AppResult<V2HrvTrendData>> getV2HrvTrend( | 234 | Future<AppResult<V2HrvTrendData>> getV2HrvTrend( |
| 169 | int? queryUserId, int intDate) async { | 235 | int? queryUserId, int intDate) async { |
| 170 | - var data = V2HrvTrendData(); | ||
| 171 | - return AppSuccess(data); | 236 | + try { |
| 237 | + final (startTime, endTime) = _dayRange(intDate); | ||
| 238 | + final hrvPoints = await coreService.queryHrvStressPoints( | ||
| 239 | + startTime: startTime, | ||
| 240 | + endTime: endTime, | ||
| 241 | + ); | ||
| 242 | + return AppSuccess(LocalHealthDataConvert.v2HrvTrendData(hrvPoints)); | ||
| 243 | + } catch (error) { | ||
| 244 | + return AppFailure(AppUnknownError(error)); | ||
| 245 | + } | ||
| 172 | } | 246 | } |
| 173 | 247 | ||
| 174 | @override | 248 | @override |
| 175 | Future<AppResult<V2LatestHrvData>> getV2LatestHrv( | 249 | Future<AppResult<V2LatestHrvData>> getV2LatestHrv( |
| 176 | int? queryUserId, int intDate) async { | 250 | int? queryUserId, int intDate) async { |
| 177 | - var data = V2LatestHrvData(); | ||
| 178 | - return AppSuccess(data); | 251 | + try { |
| 252 | + final (startTime, endTime) = _dayRange(intDate); | ||
| 253 | + final hrvPoints = await coreService.queryHrvStressPoints( | ||
| 254 | + startTime: startTime, | ||
| 255 | + endTime: endTime, | ||
| 256 | + ); | ||
| 257 | + return AppSuccess(LocalHealthDataConvert.v2LatestHrvData(hrvPoints)); | ||
| 258 | + } catch (error) { | ||
| 259 | + return AppFailure(AppUnknownError(error)); | ||
| 260 | + } | ||
| 179 | } | 261 | } |
| 180 | 262 | ||
| 181 | @override | 263 | @override |
| 182 | Future<AppResult<V2RealtimeStressData>> getV2RealtimeStress( | 264 | Future<AppResult<V2RealtimeStressData>> getV2RealtimeStress( |
| 183 | int? queryUserId, int intDate) async { | 265 | int? queryUserId, int intDate) async { |
| 184 | - var data = V2RealtimeStressData(); | ||
| 185 | - return AppSuccess(data); | 266 | + try { |
| 267 | + final (startTime, endTime) = _dayRange(intDate); | ||
| 268 | + final realtimePoints = await coreService.queryRealtimeStressPoints( | ||
| 269 | + startTime: startTime, | ||
| 270 | + endTime: endTime, | ||
| 271 | + ); | ||
| 272 | + return AppSuccess( | ||
| 273 | + LocalHealthDataConvert.v2RealtimeStressData(realtimePoints), | ||
| 274 | + ); | ||
| 275 | + } catch (error) { | ||
| 276 | + return AppFailure(AppUnknownError(error)); | ||
| 277 | + } | ||
| 186 | } | 278 | } |
| 187 | 279 | ||
| 188 | @override | 280 | @override |
| 189 | Future<AppResult<V2StressScore>> getV2StressScore( | 281 | Future<AppResult<V2StressScore>> getV2StressScore( |
| 190 | int? queryUserId, int intDate) async { | 282 | int? queryUserId, int intDate) async { |
| 191 | - var data = V2StressScore(state: 1, comprehensiveScore: 18); | ||
| 192 | - return AppSuccess(data); | 283 | + try { |
| 284 | + final (startTime, endTime) = _dayRange(intDate); | ||
| 285 | + final realtimePoints = await coreService.queryRealtimeStressPoints( | ||
| 286 | + startTime: startTime, | ||
| 287 | + endTime: endTime, | ||
| 288 | + ); | ||
| 289 | + return AppSuccess(LocalHealthDataConvert.v2StressScore(realtimePoints)); | ||
| 290 | + } catch (error) { | ||
| 291 | + return AppFailure(AppUnknownError(error)); | ||
| 292 | + } | ||
| 293 | + } | ||
| 294 | + | ||
| 295 | + (int startTime, int endTime) _dayRange(int intDate) { | ||
| 296 | + final day = LocalHealthDataConvert.dateFromKey(intDate); | ||
| 297 | + return ( | ||
| 298 | + LocalHealthDataConvert.unixSeconds(day), | ||
| 299 | + LocalHealthDataConvert.unixSeconds(day.add(const Duration(days: 1))) - 1, | ||
| 300 | + ); | ||
| 193 | } | 301 | } |
| 194 | } | 302 | } |
| @@ -380,6 +380,35 @@ class HealthKitRawDataHostApi { | @@ -380,6 +380,35 @@ class HealthKitRawDataHostApi { | ||
| 380 | 380 | ||
| 381 | final String pigeonVar_messageChannelSuffix; | 381 | final String pigeonVar_messageChannelSuffix; |
| 382 | 382 | ||
| 383 | + /// 是否有AppleHealth数据 | ||
| 384 | + Future<bool> hasHealthData() async { | ||
| 385 | + final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.hasHealthData$pigeonVar_messageChannelSuffix'; | ||
| 386 | + final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( | ||
| 387 | + pigeonVar_channelName, | ||
| 388 | + pigeonChannelCodec, | ||
| 389 | + binaryMessenger: pigeonVar_binaryMessenger, | ||
| 390 | + ); | ||
| 391 | + final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null); | ||
| 392 | + final List<Object?>? pigeonVar_replyList = | ||
| 393 | + await pigeonVar_sendFuture as List<Object?>?; | ||
| 394 | + if (pigeonVar_replyList == null) { | ||
| 395 | + throw _createConnectionError(pigeonVar_channelName); | ||
| 396 | + } else if (pigeonVar_replyList.length > 1) { | ||
| 397 | + throw PlatformException( | ||
| 398 | + code: pigeonVar_replyList[0]! as String, | ||
| 399 | + message: pigeonVar_replyList[1] as String?, | ||
| 400 | + details: pigeonVar_replyList[2], | ||
| 401 | + ); | ||
| 402 | + } else if (pigeonVar_replyList[0] == null) { | ||
| 403 | + throw PlatformException( | ||
| 404 | + code: 'null-error', | ||
| 405 | + message: 'Host platform returned null value for non-null return value.', | ||
| 406 | + ); | ||
| 407 | + } else { | ||
| 408 | + return (pigeonVar_replyList[0] as bool?)!; | ||
| 409 | + } | ||
| 410 | + } | ||
| 411 | + | ||
| 383 | /// 从AppleHealth 中读取原始数据 | 412 | /// 从AppleHealth 中读取原始数据 |
| 384 | Future<List<HealthKitRawDataPoint>> getHealthKitRawData(int dataType, int startTime, int endTime) async { | 413 | Future<List<HealthKitRawDataPoint>> getHealthKitRawData(int dataType, int startTime, int endTime) async { |
| 385 | final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.getHealthKitRawData$pigeonVar_messageChannelSuffix'; | 414 | final String pigeonVar_channelName = 'dev.flutter.pigeon.doublefeel_flutter.HealthKitRawDataHostApi.getHealthKitRawData$pigeonVar_messageChannelSuffix'; |
| @@ -89,6 +89,10 @@ class HealthKitRawActivityDataPoint { | @@ -89,6 +89,10 @@ class HealthKitRawActivityDataPoint { | ||
| 89 | ) | 89 | ) |
| 90 | @HostApi() | 90 | @HostApi() |
| 91 | abstract class HealthKitRawDataHostApi { | 91 | abstract class HealthKitRawDataHostApi { |
| 92 | + /// 是否有AppleHealth数据 | ||
| 93 | + @async | ||
| 94 | + bool hasHealthData(); | ||
| 95 | + | ||
| 92 | /// 从AppleHealth 中读取原始数据 | 96 | /// 从AppleHealth 中读取原始数据 |
| 93 | @async | 97 | @async |
| 94 | List<HealthKitRawDataPoint> getHealthKitRawData( | 98 | List<HealthKitRawDataPoint> getHealthKitRawData( |
| @@ -283,6 +283,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { | @@ -283,6 +283,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { | ||
| 283 | hrvByTime[point.rawEndTime] = HealthRawHrvStressPoint( | 283 | hrvByTime[point.rawEndTime] = HealthRawHrvStressPoint( |
| 284 | userId: point.userId, | 284 | userId: point.userId, |
| 285 | rawEndTime: point.rawEndTime, | 285 | rawEndTime: point.rawEndTime, |
| 286 | + rawHrv: point.rawHrv, | ||
| 286 | result: point.result, | 287 | result: point.result, |
| 287 | sourceStartTime: point.sourceStartTime, | 288 | sourceStartTime: point.sourceStartTime, |
| 288 | sourceEndTime: point.sourceEndTime, | 289 | sourceEndTime: point.sourceEndTime, |
| @@ -374,6 +375,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { | @@ -374,6 +375,7 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { | ||
| 374 | return HealthRawHrvStressPoint( | 375 | return HealthRawHrvStressPoint( |
| 375 | userId: point.userId, | 376 | userId: point.userId, |
| 376 | rawEndTime: point.rawEndTime, | 377 | rawEndTime: point.rawEndTime, |
| 378 | + rawHrv: point.rawHrv, | ||
| 377 | result: point.result, | 379 | result: point.result, |
| 378 | sourceStartTime: point.sourceStartTime, | 380 | sourceStartTime: point.sourceStartTime, |
| 379 | sourceEndTime: point.sourceEndTime, | 381 | sourceEndTime: point.sourceEndTime, |
| @@ -413,9 +415,11 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { | @@ -413,9 +415,11 @@ class _MemoryHealthRawStressLocalStore extends HealthRawStressLocalStore { | ||
| 413 | return <String, Object?>{ | 415 | return <String, Object?>{ |
| 414 | 'raw_end_time': point.rawEndTime, | 416 | 'raw_end_time': point.rawEndTime, |
| 415 | 'user_id': point.userId, | 417 | 'user_id': point.userId, |
| 418 | + 'raw_hrv': point.rawHrv, | ||
| 416 | 'result': point.result, | 419 | 'result': point.result, |
| 417 | 'source_start_time': point.sourceStartTime, | 420 | 'source_start_time': point.sourceStartTime, |
| 418 | 'source_end_time': point.sourceEndTime, | 421 | 'source_end_time': point.sourceEndTime, |
| 422 | + 'state': point.state.value, | ||
| 419 | 'baseline_hrv': point.baselineHrv, | 423 | 'baseline_hrv': point.baselineHrv, |
| 420 | 'baseline_awake_hrv': point.baselineAwakeHrv, | 424 | 'baseline_awake_hrv': point.baselineAwakeHrv, |
| 421 | 'baseline_sleep_hrv': point.baselineSleepHrv, | 425 | 'baseline_sleep_hrv': point.baselineSleepHrv, |
-
Please register or login to post a comment