Commit cb34ffd65ace7a4a9d690b540328546a6f93ddb3
1 parent
55dba8dc
feat(ui):分离业务逻辑直接对apple health接口的依赖;增加appstore地区的判断
Showing
31 changed files
with
1178 additions
and
85 deletions
| @@ -339,7 +339,7 @@ interface PlatformHostApi { | @@ -339,7 +339,7 @@ interface PlatformHostApi { | ||
| 339 | */ | 339 | */ |
| 340 | fun getFullUserAgent(): String | 340 | fun getFullUserAgent(): String |
| 341 | /** 是否是中国大陆地区 */ | 341 | /** 是否是中国大陆地区 */ |
| 342 | - fun isChinaRegion(): Boolean | 342 | + fun isChinaRegion(callback: (Result<Boolean>) -> Unit) |
| 343 | /** 申请通知权限 */ | 343 | /** 申请通知权限 */ |
| 344 | fun requestNotificationAuth(callback: (Result<Boolean>) -> Unit) | 344 | fun requestNotificationAuth(callback: (Result<Boolean>) -> Unit) |
| 345 | /** 是否是测试环境 */ | 345 | /** 是否是测试环境 */ |
| @@ -428,12 +428,15 @@ interface PlatformHostApi { | @@ -428,12 +428,15 @@ interface PlatformHostApi { | ||
| 428 | val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isChinaRegion$separatedMessageChannelSuffix", codec) | 428 | val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isChinaRegion$separatedMessageChannelSuffix", codec) |
| 429 | if (api != null) { | 429 | if (api != null) { |
| 430 | channel.setMessageHandler { _, reply -> | 430 | channel.setMessageHandler { _, reply -> |
| 431 | - val wrapped: List<Any?> = try { | ||
| 432 | - listOf(api.isChinaRegion()) | ||
| 433 | - } catch (exception: Throwable) { | ||
| 434 | - PlatformApiPigeonUtils.wrapError(exception) | 431 | + api.isChinaRegion{ result: Result<Boolean> -> |
| 432 | + val error = result.exceptionOrNull() | ||
| 433 | + if (error != null) { | ||
| 434 | + reply.reply(PlatformApiPigeonUtils.wrapError(error)) | ||
| 435 | + } else { | ||
| 436 | + val data = result.getOrNull() | ||
| 437 | + reply.reply(PlatformApiPigeonUtils.wrapResult(data)) | ||
| 438 | + } | ||
| 435 | } | 439 | } |
| 436 | - reply.reply(wrapped) | ||
| 437 | } | 440 | } |
| 438 | } else { | 441 | } else { |
| 439 | channel.setMessageHandler(null) | 442 | channel.setMessageHandler(null) |
No preview for this file type
21.4 KB
| @@ -369,7 +369,7 @@ protocol PlatformHostApi { | @@ -369,7 +369,7 @@ protocol PlatformHostApi { | ||
| 369 | /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` | 369 | /// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)` |
| 370 | func getFullUserAgent() throws -> String | 370 | func getFullUserAgent() throws -> String |
| 371 | /// 是否是中国大陆地区 | 371 | /// 是否是中国大陆地区 |
| 372 | - func isChinaRegion() throws -> Bool | 372 | + func isChinaRegion(completion: @escaping (Result<Bool, Error>) -> Void) |
| 373 | /// 申请通知权限 | 373 | /// 申请通知权限 |
| 374 | func requestNotificationAuth(completion: @escaping (Result<Bool, Error>) -> Void) | 374 | func requestNotificationAuth(completion: @escaping (Result<Bool, Error>) -> Void) |
| 375 | /// 是否是测试环境 | 375 | /// 是否是测试环境 |
| @@ -444,11 +444,13 @@ class PlatformHostApiSetup { | @@ -444,11 +444,13 @@ class PlatformHostApiSetup { | ||
| 444 | let isChinaRegionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isChinaRegion\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) | 444 | let isChinaRegionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isChinaRegion\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec) |
| 445 | if let api = api { | 445 | if let api = api { |
| 446 | isChinaRegionChannel.setMessageHandler { _, reply in | 446 | isChinaRegionChannel.setMessageHandler { _, reply in |
| 447 | - do { | ||
| 448 | - let result = try api.isChinaRegion() | ||
| 449 | - reply(wrapResult(result)) | ||
| 450 | - } catch { | ||
| 451 | - reply(wrapError(error)) | 447 | + api.isChinaRegion { result in |
| 448 | + switch result { | ||
| 449 | + case .success(let res): | ||
| 450 | + reply(wrapResult(res)) | ||
| 451 | + case .failure(let error): | ||
| 452 | + reply(wrapError(error)) | ||
| 453 | + } | ||
| 452 | } | 454 | } |
| 453 | } | 455 | } |
| 454 | } else { | 456 | } else { |
| @@ -55,19 +55,12 @@ final class PlatformHostApiImpl: PlatformHostApi { | @@ -55,19 +55,12 @@ final class PlatformHostApiImpl: PlatformHostApi { | ||
| 55 | #endif | 55 | #endif |
| 56 | } | 56 | } |
| 57 | /// 检查appstore 是否是中国大陆区 | 57 | /// 检查appstore 是否是中国大陆区 |
| 58 | - func isChinaRegion() throws -> Bool { | ||
| 59 | - var localeCountryCode: String? | ||
| 60 | - if let code = SKPaymentQueue.default().storefront?.countryCode{ | ||
| 61 | - localeCountryCode = code | ||
| 62 | - } | ||
| 63 | - if localeCountryCode == nil, #available(iOS 16.0, *) { | ||
| 64 | - localeCountryCode = Locale.current.region?.identifier | ||
| 65 | - } | ||
| 66 | - if localeCountryCode == nil, | ||
| 67 | - let code = (Locale.current as NSLocale).object(forKey: .countryCode) as? String{ | ||
| 68 | - localeCountryCode = code | 58 | + func isChinaRegion(completion: @escaping (Result<Bool, any Error>) -> Void) { |
| 59 | + Task{ | ||
| 60 | + let storefront = await Storefront.current | ||
| 61 | + let isChinaMainLand = storefront?.countryCode == "CHN" | ||
| 62 | + completion(.success(isChinaMainLand)) | ||
| 69 | } | 63 | } |
| 70 | - return localeCountryCode?.uppercased() == "CN" | ||
| 71 | } | 64 | } |
| 72 | 65 | ||
| 73 | func requestNotificationAuth(completion: @escaping (Result<Bool, any Error>) -> Void) { | 66 | func requestNotificationAuth(completion: @escaping (Result<Bool, any Error>) -> Void) { |
| 1 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | 1 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; |
| 2 | -import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart'; | 2 | +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 3 | import 'package:get/get.dart'; | 3 | import 'package:get/get.dart'; |
| 4 | 4 | ||
| 5 | import '../../core/config/app_environment_config.dart'; | 5 | import '../../core/config/app_environment_config.dart'; |
| 1 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; | 1 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; |
| 2 | -import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart'; | 2 | +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 3 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | 3 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; |
| 4 | import 'package:doublefeel_flutter/data/datasource/health/health_datasource.dart'; | 4 | import 'package:doublefeel_flutter/data/datasource/health/health_datasource.dart'; |
| 5 | import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart'; | 5 | import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart'; |
| 1 | import 'package:get/get.dart'; | 1 | import 'package:get/get.dart'; |
| 2 | 2 | ||
| 3 | -import '../../../../core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart'; | 3 | +import '../../../../core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 4 | import '../../../../data/local/user_preferences_storage.dart'; | 4 | import '../../../../data/local/user_preferences_storage.dart'; |
| 5 | import '../controllers/developer_options_controller.dart'; | 5 | import '../controllers/developer_options_controller.dart'; |
| 6 | 6 |
| @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; | @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; | ||
| 2 | import 'package:get/get.dart'; | 2 | import 'package:get/get.dart'; |
| 3 | 3 | ||
| 4 | import '../../../../core/logging/app_logger.dart'; | 4 | import '../../../../core/logging/app_logger.dart'; |
| 5 | -import '../../../../core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart'; | 5 | +import '../../../../core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 6 | import '../../../../data/local/user_preferences_storage.dart'; | 6 | import '../../../../data/local/user_preferences_storage.dart'; |
| 7 | import '../../../../pigeon/platform_api.g.dart'; | 7 | import '../../../../pigeon/platform_api.g.dart'; |
| 8 | import '../../../routes/app_pages.dart'; | 8 | import '../../../routes/app_pages.dart'; |
| @@ -3,7 +3,7 @@ import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | @@ -3,7 +3,7 @@ import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | ||
| 3 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; | 3 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; |
| 4 | import 'package:doublefeel_flutter/core/network/api/pay_api.dart'; | 4 | import 'package:doublefeel_flutter/core/network/api/pay_api.dart'; |
| 5 | import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; | 5 | import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; |
| 6 | -import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart'; | 6 | +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 7 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | 7 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; |
| 8 | import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart'; | 8 | import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart'; |
| 9 | import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart'; | 9 | import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart'; |
| @@ -5,7 +5,7 @@ import 'package:doublefeel_flutter/core/network/api/pay_api.dart'; | @@ -5,7 +5,7 @@ import 'package:doublefeel_flutter/core/network/api/pay_api.dart'; | ||
| 5 | import 'package:doublefeel_flutter/core/network/api/theme_api.dart'; | 5 | import 'package:doublefeel_flutter/core/network/api/theme_api.dart'; |
| 6 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | 6 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; |
| 7 | import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; | 7 | import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; |
| 8 | -import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart'; | 8 | +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 9 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | 9 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; |
| 10 | import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart'; | 10 | import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart'; |
| 11 | import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart'; | 11 | import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart'; |
| @@ -4,7 +4,7 @@ import 'package:doublefeel_flutter/core/constants/flutter_bridge_method_name.dar | @@ -4,7 +4,7 @@ import 'package:doublefeel_flutter/core/constants/flutter_bridge_method_name.dar | ||
| 4 | import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; | 4 | import 'package:doublefeel_flutter/core/constants/intent_keys.dart'; |
| 5 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | 5 | import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; |
| 6 | import 'package:doublefeel_flutter/core/result/app_result.dart'; | 6 | import 'package:doublefeel_flutter/core/result/app_result.dart'; |
| 7 | -import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart'; | 7 | +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 8 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; | 8 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; |
| 9 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | 9 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; |
| 10 | import 'package:doublefeel_flutter/data/local/local_storage.dart'; | 10 | import 'package:doublefeel_flutter/data/local/local_storage.dart'; |
| @@ -15,7 +15,7 @@ import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | @@ -15,7 +15,7 @@ import 'package:doublefeel_flutter/core/network/api/friend_api.dart'; | ||
| 15 | import 'package:doublefeel_flutter/core/network/api/pay_api.dart'; | 15 | import 'package:doublefeel_flutter/core/network/api/pay_api.dart'; |
| 16 | import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; | 16 | import 'package:doublefeel_flutter/core/network/api/vip_api.dart'; |
| 17 | import 'package:doublefeel_flutter/core/result/app_result.dart'; | 17 | import 'package:doublefeel_flutter/core/result/app_result.dart'; |
| 18 | -import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart'; | 18 | +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 19 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; | 19 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; |
| 20 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | 20 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; |
| 21 | import 'package:doublefeel_flutter/core/util/app_toast.dart'; | 21 | import 'package:doublefeel_flutter/core/util/app_toast.dart'; |
| @@ -28,7 +28,6 @@ import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; | @@ -28,7 +28,6 @@ import 'package:doublefeel_flutter/data/models/local/user_preferences.dart'; | ||
| 28 | import 'package:doublefeel_flutter/data/models/pay/pay_models.dart'; | 28 | import 'package:doublefeel_flutter/data/models/pay/pay_models.dart'; |
| 29 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; | 29 | import 'package:doublefeel_flutter/l10n/l10n_extensions.dart'; |
| 30 | import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart'; | 30 | import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart'; |
| 31 | -import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'; | ||
| 32 | import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; | 31 | import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; |
| 33 | import 'package:doublefeel_flutter/pigeon/wear_engine_api.g.dart'; | 32 | import 'package:doublefeel_flutter/pigeon/wear_engine_api.g.dart'; |
| 34 | import 'package:flutter/material.dart'; | 33 | import 'package:flutter/material.dart'; |
| @@ -75,7 +74,6 @@ class TodayController extends GetMaterialController { | @@ -75,7 +74,6 @@ class TodayController extends GetMaterialController { | ||
| 75 | final FriendApi _friendApi; | 74 | final FriendApi _friendApi; |
| 76 | final UserStateService _userStateService; | 75 | final UserStateService _userStateService; |
| 77 | final HealthKitHostApi _hostApi = HealthKitHostApi(); | 76 | final HealthKitHostApi _hostApi = HealthKitHostApi(); |
| 78 | - final HealthKitRawDataHostApi _rawDataApi = HealthKitRawDataHostApi(); | ||
| 79 | final HealthRawDataCoreService _healthRawDataCoreService = | 77 | final HealthRawDataCoreService _healthRawDataCoreService = |
| 80 | Get.find<HealthRawDataCoreService>(); | 78 | Get.find<HealthRawDataCoreService>(); |
| 81 | 79 | ||
| @@ -500,7 +498,7 @@ class TodayController extends GetMaterialController { | @@ -500,7 +498,7 @@ class TodayController extends GetMaterialController { | ||
| 500 | 498 | ||
| 501 | void _performHealthDataUpload() { | 499 | void _performHealthDataUpload() { |
| 502 | unawaited( | 500 | unawaited( |
| 503 | - _rawDataApi | 501 | + _healthRawDataCoreService |
| 504 | .performHealthDataUpload() | 502 | .performHealthDataUpload() |
| 505 | .catchError((Object error, StackTrace stackTrace) { | 503 | .catchError((Object error, StackTrace stackTrace) { |
| 506 | AppLogger.e('Apple Health raw upload failed', error, stackTrace); | 504 | AppLogger.e('Apple Health raw upload failed', error, stackTrace); |
| 1 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; | 1 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; |
| 2 | -import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart'; | 2 | +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 3 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | 3 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; |
| 4 | import 'package:doublefeel_flutter/data/datasource/health/health_datasource.dart'; | 4 | import 'package:doublefeel_flutter/data/datasource/health/health_datasource.dart'; |
| 5 | import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart'; | 5 | import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart'; |
| 1 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; | 1 | import 'package:doublefeel_flutter/core/network/api/health_api.dart'; |
| 2 | -import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart'; | 2 | +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 3 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | 3 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; |
| 4 | import 'package:doublefeel_flutter/data/datasource/health/health_datasource.dart'; | 4 | import 'package:doublefeel_flutter/data/datasource/health/health_datasource.dart'; |
| 5 | import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart'; | 5 | import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart'; |
| 1 | -import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart'; | 1 | +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 2 | import 'package:flutter/scheduler.dart'; | 2 | import 'package:flutter/scheduler.dart'; |
| 3 | import 'package:get/get.dart'; | 3 | import 'package:get/get.dart'; |
| 4 | 4 | ||
| @@ -62,7 +62,7 @@ class SplashController extends GetxController { | @@ -62,7 +62,7 @@ class SplashController extends GetxController { | ||
| 62 | // final me = (userResult as AppSuccess).data; | 62 | // final me = (userResult as AppSuccess).data; |
| 63 | // print(me.nickname); | 63 | // print(me.nickname); |
| 64 | // } | 64 | // } |
| 65 | - if (userResult case AppSuccess(data: final me)) { | 65 | + if (userResult is AppSuccess) { |
| 66 | // UserInfoResponse? partner; | 66 | // UserInfoResponse? partner; |
| 67 | // if ((me.pairId ?? 0) > 0) { | 67 | // if ((me.pairId ?? 0) > 0) { |
| 68 | // final partnerResult = await _userApi.getPartnerUserInfo(); | 68 | // final partnerResult = await _userApi.getPartnerUserInfo(); |
| 1 | import 'package:doublefeel_flutter/core/logging/app_logger.dart'; | 1 | import 'package:doublefeel_flutter/core/logging/app_logger.dart'; |
| 2 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; | 2 | import 'package:doublefeel_flutter/core/network/api/user_api.dart'; |
| 3 | -import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart'; | 3 | +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 4 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; | 4 | import 'package:doublefeel_flutter/core/services/thinking_data_service.dart'; |
| 5 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; | 5 | import 'package:doublefeel_flutter/core/services/user_state_service.dart'; |
| 6 | import 'package:doublefeel_flutter/data/local/user_account_storage.dart'; | 6 | import 'package:doublefeel_flutter/data/local/user_account_storage.dart'; |
| 7 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; | 7 | import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart'; |
| 8 | import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart'; | 8 | import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart'; |
| 9 | -import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'; | ||
| 10 | import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; | 9 | import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; |
| 11 | import 'package:get/get.dart'; | 10 | import 'package:get/get.dart'; |
| 12 | import 'package:permission_handler/permission_handler.dart'; | 11 | import 'package:permission_handler/permission_handler.dart'; |
| @@ -127,7 +126,9 @@ class UserOnboardingController extends GetxController { | @@ -127,7 +126,9 @@ class UserOnboardingController extends GetxController { | ||
| 127 | } | 126 | } |
| 128 | try { | 127 | try { |
| 129 | report(); | 128 | report(); |
| 130 | - } on Exception catch (e) {} | 129 | + } on Exception catch (error, stackTrace) { |
| 130 | + AppLogger.e('Onboarding report failed', error, stackTrace); | ||
| 131 | + } | ||
| 131 | currentPageIndex.value++; | 132 | currentPageIndex.value++; |
| 132 | if (currentPageIndex.value == 7) { | 133 | if (currentPageIndex.value == 7) { |
| 133 | ta.track('enter_doublefeel_health_grant_page'); | 134 | ta.track('enter_doublefeel_health_grant_page'); |
| @@ -231,8 +232,9 @@ class UserOnboardingController extends GetxController { | @@ -231,8 +232,9 @@ class UserOnboardingController extends GetxController { | ||
| 231 | /// 计算 Apple Health 新数据。 | 232 | /// 计算 Apple Health 新数据。 |
| 232 | Future<void> _performDataUpload() async { | 233 | Future<void> _performDataUpload() async { |
| 233 | try { | 234 | try { |
| 234 | - HealthKitRawDataHostApi().performHealthDataUpload(); | ||
| 235 | - await Get.find<HealthRawDataCoreService>().startCoreCaculate(); | 235 | + final service = Get.find<HealthRawDataCoreService>(); |
| 236 | + await service.performHealthDataUpload(); | ||
| 237 | + await service.startCoreCaculate(); | ||
| 236 | } catch (error, stackTrace) { | 238 | } catch (error, stackTrace) { |
| 237 | AppLogger.e('Apple Health calculate failed', error, stackTrace); | 239 | AppLogger.e('Apple Health calculate failed', error, stackTrace); |
| 238 | } | 240 | } |
| @@ -264,7 +266,9 @@ class UserOnboardingController extends GetxController { | @@ -264,7 +266,9 @@ class UserOnboardingController extends GetxController { | ||
| 264 | if (result.status == 1) _performDataUpload(); | 266 | if (result.status == 1) _performDataUpload(); |
| 265 | return true; | 267 | return true; |
| 266 | } | 268 | } |
| 267 | - } catch (e) {} | 269 | + } catch (error, stackTrace) { |
| 270 | + AppLogger.e('Health authorization failed', error, stackTrace); | ||
| 271 | + } | ||
| 268 | } | 272 | } |
| 269 | } | 273 | } |
| 270 | return false; | 274 | return false; |
| @@ -286,7 +290,7 @@ class UserOnboardingController extends GetxController { | @@ -286,7 +290,7 @@ class UserOnboardingController extends GetxController { | ||
| 286 | } | 290 | } |
| 287 | 291 | ||
| 288 | // 未决定 → 弹出系统授权弹窗 | 292 | // 未决定 → 弹出系统授权弹窗 |
| 289 | - final result = await _platformHostApi.requestNotificationAuth(); | 293 | + await _platformHostApi.requestNotificationAuth(); |
| 290 | } catch (error) { | 294 | } catch (error) { |
| 291 | AppLogger.e('Notification authorization failed: $error'); | 295 | AppLogger.e('Notification authorization failed: $error'); |
| 292 | } | 296 | } |
| @@ -5,7 +5,7 @@ import 'package:sqflite/sqflite.dart'; | @@ -5,7 +5,7 @@ import 'package:sqflite/sqflite.dart'; | ||
| 5 | 5 | ||
| 6 | import '../../../data/models/enums/app_enums.dart'; | 6 | import '../../../data/models/enums/app_enums.dart'; |
| 7 | import '../../../pigeon/health_kit_raw_data_api.g.dart'; | 7 | import '../../../pigeon/health_kit_raw_data_api.g.dart'; |
| 8 | -import 'platform_ios/health_raw_data_core_service.dart'; | 8 | +import 'platform_ios/apple_health_raw_data_core_service.dart'; |
| 9 | import 'health_raw_models.dart'; | 9 | import 'health_raw_models.dart'; |
| 10 | import 'health_raw_stress_calculator.dart'; | 10 | import 'health_raw_stress_calculator.dart'; |
| 11 | 11 |
| 1 | +import 'dart:async'; | ||
| 2 | + | ||
| 3 | +import 'package:flutter/foundation.dart'; | ||
| 4 | + | ||
| 5 | +import '../../../pigeon/health_kit_raw_data_api.g.dart'; | ||
| 6 | +import '../../config/app_environment_config.dart'; | ||
| 7 | +import '../../network/api/health_api.dart'; | ||
| 8 | +import 'health_raw_models.dart'; | ||
| 9 | +import 'platform_ios/apple_health_raw_data_core_service.dart'; | ||
| 10 | +import 'platform_ohos/ohos_health_raw_data_core_service.dart'; | ||
| 11 | + | ||
| 12 | +class HealthRawDataCoreService { | ||
| 13 | + static const int defaultLookbackDays = | ||
| 14 | + AppleHealthRawDataCoreService.defaultLookbackDays; | ||
| 15 | + static const int defaultReadChunkDays = | ||
| 16 | + AppleHealthRawDataCoreService.defaultReadChunkDays; | ||
| 17 | + | ||
| 18 | + HealthRawDataCoreService({ | ||
| 19 | + AppleHealthRawDataCoreService? appleService, | ||
| 20 | + OHOSHealthRawDataCoreService? ohosService, | ||
| 21 | + TargetPlatform? targetPlatform, | ||
| 22 | + AppEnvironmentConfig? environmentConfig, | ||
| 23 | + int Function()? userIdProvider, | ||
| 24 | + bool uploadResultsAfterCalculation = true, | ||
| 25 | + HealthApi? serverHealthApi, | ||
| 26 | + }) : _appleService = appleService ?? | ||
| 27 | + AppleHealthRawDataCoreService( | ||
| 28 | + environmentConfig: environmentConfig, | ||
| 29 | + userIdProvider: userIdProvider, | ||
| 30 | + uploadResultsAfterCalculation: uploadResultsAfterCalculation, | ||
| 31 | + serverHealthApi: serverHealthApi, | ||
| 32 | + ), | ||
| 33 | + _ohosService = ohosService ?? | ||
| 34 | + OHOSHealthRawDataCoreService( | ||
| 35 | + environmentConfig: environmentConfig, | ||
| 36 | + userIdProvider: userIdProvider, | ||
| 37 | + uploadResultsAfterCalculation: uploadResultsAfterCalculation, | ||
| 38 | + serverHealthApi: serverHealthApi, | ||
| 39 | + ), | ||
| 40 | + _targetPlatform = targetPlatform; | ||
| 41 | + | ||
| 42 | + final AppleHealthRawDataCoreService _appleService; | ||
| 43 | + final OHOSHealthRawDataCoreService _ohosService; | ||
| 44 | + final TargetPlatform? _targetPlatform; | ||
| 45 | + | ||
| 46 | + dynamic get _currentService { | ||
| 47 | + final platform = _targetPlatform ?? defaultTargetPlatform; | ||
| 48 | + return platform == TargetPlatform.iOS ? _appleService : _ohosService; | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + Future<void> openDatabase() { | ||
| 52 | + return _currentService.openDatabase(); | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + Future<void> closeDatabase() { | ||
| 56 | + return _currentService.closeDatabase(); | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + Future<bool> hasHealthData() { | ||
| 60 | + return _currentService.hasHealthData(); | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | + Future<bool> performHealthDataUpload() { | ||
| 64 | + return _currentService.performHealthDataUpload(); | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + Future<bool> syncDatabaseToNativeIfNeeded() { | ||
| 68 | + return _currentService.syncDatabaseToNativeIfNeeded(); | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + Stream<HealthRawDataUpdatedEvent> get healthDataUpdatedStream => | ||
| 72 | + _currentService.healthDataUpdatedStream; | ||
| 73 | + | ||
| 74 | + Future<void> onHealthDataUpdated({List<int>? dataTypes}) { | ||
| 75 | + return _currentService.onHealthDataUpdated(dataTypes: dataTypes); | ||
| 76 | + } | ||
| 77 | + | ||
| 78 | + Future<String> databaseFilePath() { | ||
| 79 | + return _currentService.databaseFilePath(); | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + Future<Uint8List> readDatabaseFileBytes() { | ||
| 83 | + return _currentService.readDatabaseFileBytes(); | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + Future<String> readUploadApiLogText() { | ||
| 87 | + return _currentService.readUploadApiLogText(); | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + Future<void> clearLocalDatabaseAndUploadLog() { | ||
| 91 | + return _currentService.clearLocalDatabaseAndUploadLog(); | ||
| 92 | + } | ||
| 93 | + | ||
| 94 | + Future<void> clearUploadApiLog() { | ||
| 95 | + return _currentService.clearUploadApiLog(); | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | + Future<bool> shareNativeAppleHealthObserverRecord() { | ||
| 99 | + return _currentService.shareNativeAppleHealthObserverRecord(); | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + Future<bool> shareFlutterObserverRecord() { | ||
| 103 | + return _currentService.shareFlutterObserverRecord(); | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | + Future<bool> shareUploadTaskRecord() { | ||
| 107 | + return _currentService.shareUploadTaskRecord(); | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + Future<String> readLocalNotificationDebugLogText() { | ||
| 111 | + return _currentService.readLocalNotificationDebugLogText(); | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + Future<bool> shareLocalNotificationDebugRecord() { | ||
| 115 | + return _currentService.shareLocalNotificationDebugRecord(); | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + Future<HealthRawStressCalculationResult> startCoreCaculate({ | ||
| 119 | + int? endTime, | ||
| 120 | + int readChunkDays = defaultReadChunkDays, | ||
| 121 | + }) { | ||
| 122 | + return _currentService.startCoreCaculate( | ||
| 123 | + endTime: endTime, | ||
| 124 | + readChunkDays: readChunkDays, | ||
| 125 | + ); | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + Future<HealthRawStressCalculationResult> syncAndStore({ | ||
| 129 | + int? startTime, | ||
| 130 | + int? endTime, | ||
| 131 | + int readChunkDays = defaultReadChunkDays, | ||
| 132 | + }) { | ||
| 133 | + return _currentService.syncAndStore( | ||
| 134 | + startTime: startTime, | ||
| 135 | + endTime: endTime, | ||
| 136 | + readChunkDays: readChunkDays, | ||
| 137 | + ); | ||
| 138 | + } | ||
| 139 | + | ||
| 140 | + Stream<HealthKitRawDataPoint> streamRawData({ | ||
| 141 | + required int dataType, | ||
| 142 | + required int startTime, | ||
| 143 | + required int endTime, | ||
| 144 | + int readChunkDays = defaultReadChunkDays, | ||
| 145 | + }) { | ||
| 146 | + return _currentService.streamRawData( | ||
| 147 | + dataType: dataType, | ||
| 148 | + startTime: startTime, | ||
| 149 | + endTime: endTime, | ||
| 150 | + readChunkDays: readChunkDays, | ||
| 151 | + ); | ||
| 152 | + } | ||
| 153 | + | ||
| 154 | + Stream<HealthKitRawDataPoint> streamRawSleepData({ | ||
| 155 | + required int startTime, | ||
| 156 | + required int endTime, | ||
| 157 | + int readChunkDays = defaultReadChunkDays, | ||
| 158 | + }) { | ||
| 159 | + return _currentService.streamRawSleepData( | ||
| 160 | + startTime: startTime, | ||
| 161 | + endTime: endTime, | ||
| 162 | + readChunkDays: readChunkDays, | ||
| 163 | + ); | ||
| 164 | + } | ||
| 165 | + | ||
| 166 | + Stream<HealthKitRawWorkoutDataPoint> streamRawWorkoutData({ | ||
| 167 | + required int startTime, | ||
| 168 | + required int endTime, | ||
| 169 | + int readChunkDays = defaultReadChunkDays, | ||
| 170 | + }) { | ||
| 171 | + return _currentService.streamRawWorkoutData( | ||
| 172 | + startTime: startTime, | ||
| 173 | + endTime: endTime, | ||
| 174 | + readChunkDays: readChunkDays, | ||
| 175 | + ); | ||
| 176 | + } | ||
| 177 | + | ||
| 178 | + Future<List<HealthRawHrvStressPoint>> queryHrvStressPoints({ | ||
| 179 | + required int startTime, | ||
| 180 | + required int endTime, | ||
| 181 | + }) { | ||
| 182 | + return _currentService.queryHrvStressPoints( | ||
| 183 | + startTime: startTime, | ||
| 184 | + endTime: endTime, | ||
| 185 | + ); | ||
| 186 | + } | ||
| 187 | + | ||
| 188 | + Future<List<HealthRawRealtimeStressPoint>> queryRealtimeStressPoints({ | ||
| 189 | + required int startTime, | ||
| 190 | + required int endTime, | ||
| 191 | + }) { | ||
| 192 | + return _currentService.queryRealtimeStressPoints( | ||
| 193 | + startTime: startTime, | ||
| 194 | + endTime: endTime, | ||
| 195 | + ); | ||
| 196 | + } | ||
| 197 | + | ||
| 198 | + Future<int?> queryEarliestHrRawEndTime() { | ||
| 199 | + return _currentService.queryEarliestHrRawEndTime(); | ||
| 200 | + } | ||
| 201 | + | ||
| 202 | + Future<List<HealthRawDailyStressPoint>> queryDailyStressPoints({ | ||
| 203 | + required int startDate, | ||
| 204 | + required int endDate, | ||
| 205 | + }) { | ||
| 206 | + return _currentService.queryDailyStressPoints( | ||
| 207 | + startDate: startDate, | ||
| 208 | + endDate: endDate, | ||
| 209 | + ); | ||
| 210 | + } | ||
| 211 | + | ||
| 212 | + Future<List<HealthRawSleepResult>> querySleepResults({ | ||
| 213 | + required int startTime, | ||
| 214 | + required int endTime, | ||
| 215 | + }) { | ||
| 216 | + return _currentService.querySleepResults( | ||
| 217 | + startTime: startTime, | ||
| 218 | + endTime: endTime, | ||
| 219 | + ); | ||
| 220 | + } | ||
| 221 | + | ||
| 222 | + Future<List<HealthKitRawDataPoint>> queryRawDataPoints({ | ||
| 223 | + required int dataType, | ||
| 224 | + required int startTime, | ||
| 225 | + required int endTime, | ||
| 226 | + int readChunkDays = defaultReadChunkDays, | ||
| 227 | + }) { | ||
| 228 | + return _currentService.queryRawDataPoints( | ||
| 229 | + dataType: dataType, | ||
| 230 | + startTime: startTime, | ||
| 231 | + endTime: endTime, | ||
| 232 | + readChunkDays: readChunkDays, | ||
| 233 | + ); | ||
| 234 | + } | ||
| 235 | + | ||
| 236 | + Future<List<HealthKitRawDataPoint>> queryRawSleepIntervals({ | ||
| 237 | + required int startTime, | ||
| 238 | + required int endTime, | ||
| 239 | + int readChunkDays = defaultReadChunkDays, | ||
| 240 | + }) { | ||
| 241 | + return _currentService.queryRawSleepIntervals( | ||
| 242 | + startTime: startTime, | ||
| 243 | + endTime: endTime, | ||
| 244 | + readChunkDays: readChunkDays, | ||
| 245 | + ); | ||
| 246 | + } | ||
| 247 | + | ||
| 248 | + Future<List<HealthKitRawActivityDataPoint>> queryRawActivitySummaries({ | ||
| 249 | + required int startTime, | ||
| 250 | + required int endTime, | ||
| 251 | + }) { | ||
| 252 | + return _currentService.queryRawActivitySummaries( | ||
| 253 | + startTime: startTime, | ||
| 254 | + endTime: endTime, | ||
| 255 | + ); | ||
| 256 | + } | ||
| 257 | + | ||
| 258 | + Future<void> markHrvStressUploaded({ | ||
| 259 | + required Iterable<int> rawEndTimes, | ||
| 260 | + }) { | ||
| 261 | + return _currentService.markHrvStressUploaded(rawEndTimes: rawEndTimes); | ||
| 262 | + } | ||
| 263 | + | ||
| 264 | + Future<void> markRealtimeStressUploaded({ | ||
| 265 | + required Iterable<int> rawEndTimes, | ||
| 266 | + }) { | ||
| 267 | + return _currentService.markRealtimeStressUploaded( | ||
| 268 | + rawEndTimes: rawEndTimes, | ||
| 269 | + ); | ||
| 270 | + } | ||
| 271 | + | ||
| 272 | + static HealthRawStressCalculationResult calculate({ | ||
| 273 | + required int userId, | ||
| 274 | + required List<HealthKitRawDataPoint> hrvPoints, | ||
| 275 | + required List<HealthKitRawDataPoint> heartRatePoints, | ||
| 276 | + required List<HealthKitRawDataPoint> restingHeartRatePoints, | ||
| 277 | + required List<HealthKitRawDataPoint> sleepIntervals, | ||
| 278 | + List<HealthKitRawWorkoutDataPoint> workoutIntervals = | ||
| 279 | + const <HealthKitRawWorkoutDataPoint>[], | ||
| 280 | + required int startTime, | ||
| 281 | + required int endTime, | ||
| 282 | + }) { | ||
| 283 | + return AppleHealthRawDataCoreService.calculate( | ||
| 284 | + userId: userId, | ||
| 285 | + hrvPoints: hrvPoints, | ||
| 286 | + heartRatePoints: heartRatePoints, | ||
| 287 | + restingHeartRatePoints: restingHeartRatePoints, | ||
| 288 | + sleepIntervals: sleepIntervals, | ||
| 289 | + workoutIntervals: workoutIntervals, | ||
| 290 | + startTime: startTime, | ||
| 291 | + endTime: endTime, | ||
| 292 | + ); | ||
| 293 | + } | ||
| 294 | +} |
| @@ -17,16 +17,16 @@ import '../../../logging/app_logger.dart'; | @@ -17,16 +17,16 @@ import '../../../logging/app_logger.dart'; | ||
| 17 | import '../../../network/api/health_api.dart'; | 17 | import '../../../network/api/health_api.dart'; |
| 18 | import '../../../result/app_result.dart'; | 18 | import '../../../result/app_result.dart'; |
| 19 | import '../health_raw_models.dart'; | 19 | import '../health_raw_models.dart'; |
| 20 | -import 'health_raw_local_notification_debug_store.dart'; | ||
| 21 | -import 'health_raw_local_notification.dart'; | 20 | +import 'apple_health_raw_local_notification_debug_store.dart'; |
| 21 | +import 'apple_health_raw_local_notification.dart'; | ||
| 22 | import '../health_raw_stress_calculator.dart'; | 22 | import '../health_raw_stress_calculator.dart'; |
| 23 | import '../health_sleep_calculator.dart'; | 23 | import '../health_sleep_calculator.dart'; |
| 24 | 24 | ||
| 25 | -class HealthRawDataCoreService { | 25 | +class AppleHealthRawDataCoreService { |
| 26 | static const int defaultLookbackDays = 183; | 26 | static const int defaultLookbackDays = 183; |
| 27 | static const int defaultReadChunkDays = 7; | 27 | static const int defaultReadChunkDays = 7; |
| 28 | 28 | ||
| 29 | - HealthRawDataCoreService({ | 29 | + AppleHealthRawDataCoreService({ |
| 30 | HealthKitHostApi? healthApi, | 30 | HealthKitHostApi? healthApi, |
| 31 | HealthKitRawDataHostApi? rawDataApi, | 31 | HealthKitRawDataHostApi? rawDataApi, |
| 32 | HealthRawStressLocalStore? localStore, | 32 | HealthRawStressLocalStore? localStore, |
| @@ -73,7 +73,7 @@ class HealthRawDataCoreService { | @@ -73,7 +73,7 @@ class HealthRawDataCoreService { | ||
| 73 | int get _userId { | 73 | int get _userId { |
| 74 | final userId = _userIdProvider?.call() ?? 0; | 74 | final userId = _userIdProvider?.call() ?? 0; |
| 75 | if (userId <= 0) { | 75 | if (userId <= 0) { |
| 76 | - throw StateError('HealthRawDataCoreService requires a valid userId'); | 76 | + throw StateError('AppleHealthRawDataCoreService requires a valid userId'); |
| 77 | } | 77 | } |
| 78 | return userId; | 78 | return userId; |
| 79 | } | 79 | } |
| @@ -90,6 +90,20 @@ class HealthRawDataCoreService { | @@ -90,6 +90,20 @@ class HealthRawDataCoreService { | ||
| 90 | return _rawDataApi.hasHealthData(); | 90 | return _rawDataApi.hasHealthData(); |
| 91 | } | 91 | } |
| 92 | 92 | ||
| 93 | + Future<bool> performHealthDataUpload() { | ||
| 94 | + return _rawDataApi.performHealthDataUpload(); | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | + Future<bool> syncDatabaseToNativeIfNeeded() async { | ||
| 98 | + final databasePath = await databaseFilePath(); | ||
| 99 | + return _rawDataApi.syncDatabase( | ||
| 100 | + hrDataPath: databasePath, | ||
| 101 | + hrvDataPath: databasePath, | ||
| 102 | + sleepDataPath: databasePath, | ||
| 103 | + avgRealtimeStressDataPath: databasePath, | ||
| 104 | + ); | ||
| 105 | + } | ||
| 106 | + | ||
| 93 | Stream<HealthRawDataUpdatedEvent> get healthDataUpdatedStream => | 107 | Stream<HealthRawDataUpdatedEvent> get healthDataUpdatedStream => |
| 94 | _healthDataUpdatedController.stream; | 108 | _healthDataUpdatedController.stream; |
| 95 | 109 | ||
| @@ -148,7 +162,7 @@ class HealthRawDataCoreService { | @@ -148,7 +162,7 @@ class HealthRawDataCoreService { | ||
| 148 | final path = await _localStore.dbPath(userId); | 162 | final path = await _localStore.dbPath(userId); |
| 149 | final file = File(path); | 163 | final file = File(path); |
| 150 | if (!await file.exists()) { | 164 | if (!await file.exists()) { |
| 151 | - throw StateError('HealthRawDataCoreService database file not found'); | 165 | + throw StateError('AppleHealthRawDataCoreService database file not found'); |
| 152 | } | 166 | } |
| 153 | final bytes = await file.readAsBytes(); | 167 | final bytes = await file.readAsBytes(); |
| 154 | return bytes; | 168 | return bytes; |
| @@ -1664,8 +1678,10 @@ class HealthRawStressLocalStore { | @@ -1664,8 +1678,10 @@ class HealthRawStressLocalStore { | ||
| 1664 | HealthRawStressLocalStore({ | 1678 | HealthRawStressLocalStore({ |
| 1665 | Directory? rootDirectory, | 1679 | Directory? rootDirectory, |
| 1666 | DatabaseFactory? databaseFactory, | 1680 | DatabaseFactory? databaseFactory, |
| 1681 | + String databaseNamePrefix = '', | ||
| 1667 | }) : _rootDirectory = rootDirectory, | 1682 | }) : _rootDirectory = rootDirectory, |
| 1668 | - _databaseFactory = databaseFactory; | 1683 | + _databaseFactory = databaseFactory, |
| 1684 | + _databaseNamePrefix = databaseNamePrefix; | ||
| 1669 | 1685 | ||
| 1670 | static const hrvResultsTable = 'hrv_results'; | 1686 | static const hrvResultsTable = 'hrv_results'; |
| 1671 | static const realtimeStressResultsTable = 'realtime_stress_results'; | 1687 | static const realtimeStressResultsTable = 'realtime_stress_results'; |
| @@ -1674,6 +1690,7 @@ class HealthRawStressLocalStore { | @@ -1674,6 +1690,7 @@ class HealthRawStressLocalStore { | ||
| 1674 | 1690 | ||
| 1675 | final Directory? _rootDirectory; | 1691 | final Directory? _rootDirectory; |
| 1676 | final DatabaseFactory? _databaseFactory; | 1692 | final DatabaseFactory? _databaseFactory; |
| 1693 | + final String _databaseNamePrefix; | ||
| 1677 | final Map<int, Database> _opened = <int, Database>{}; | 1694 | final Map<int, Database> _opened = <int, Database>{}; |
| 1678 | 1695 | ||
| 1679 | Future<void> upsertResult(HealthRawStressCalculationResult result) async { | 1696 | Future<void> upsertResult(HealthRawStressCalculationResult result) async { |
| @@ -1996,7 +2013,7 @@ class HealthRawStressLocalStore { | @@ -1996,7 +2013,7 @@ class HealthRawStressLocalStore { | ||
| 1996 | 2013 | ||
| 1997 | Future<String> dbPath(int userId) async { | 2014 | Future<String> dbPath(int userId) async { |
| 1998 | final dir = _rootDirectory ?? await getApplicationDocumentsDirectory(); | 2015 | final dir = _rootDirectory ?? await getApplicationDocumentsDirectory(); |
| 1999 | - return '${dir.path}/hrv_result_$userId.sqlite'; | 2016 | + return '${dir.path}/${_databaseNamePrefix}hrv_result_$userId.sqlite'; |
| 2000 | } | 2017 | } |
| 2001 | 2018 | ||
| 2002 | Future<void> openDatabase(int userId) async { | 2019 | Future<void> openDatabase(int userId) async { |
| 1 | +import 'dart:async'; | ||
| 2 | +import 'dart:developer' as developer; | ||
| 3 | +import 'dart:io'; | ||
| 4 | +import 'dart:isolate'; | ||
| 5 | +import 'dart:math' as math; | ||
| 6 | + | ||
| 7 | +import 'package:flutter/foundation.dart'; | ||
| 8 | + | ||
| 9 | +import '../../../../data/models/enums/app_enums.dart'; | ||
| 10 | +import '../../../../pigeon/health_kit_raw_data_api.g.dart'; | ||
| 11 | +import '../../../config/app_environment_config.dart'; | ||
| 12 | +import '../../../logging/app_logger.dart'; | ||
| 13 | +import '../../../network/api/health_api.dart'; | ||
| 14 | +import '../health_raw_data_source.dart'; | ||
| 15 | +import '../health_raw_models.dart'; | ||
| 16 | +import '../health_raw_stress_calculator.dart'; | ||
| 17 | +import '../health_sleep_calculator.dart'; | ||
| 18 | +import '../platform_ios/apple_health_raw_data_core_service.dart'; | ||
| 19 | + | ||
| 20 | +class OHOSHealthRawDataCoreService { | ||
| 21 | + static const int defaultLookbackDays = 183; | ||
| 22 | + static const int defaultReadChunkDays = 7; | ||
| 23 | + | ||
| 24 | + OHOSHealthRawDataCoreService({ | ||
| 25 | + HealthRawDataSource? rawDataSource, | ||
| 26 | + HealthRawStressLocalStore? localStore, | ||
| 27 | + AppEnvironmentConfig? environmentConfig, | ||
| 28 | + int Function()? userIdProvider, | ||
| 29 | + bool uploadResultsAfterCalculation = true, | ||
| 30 | + HealthApi? serverHealthApi, | ||
| 31 | + }) : _rawDataSource = rawDataSource ?? OhosHealthRawDataSource(), | ||
| 32 | + _localStore = localStore ?? | ||
| 33 | + HealthRawStressLocalStore(databaseNamePrefix: 'ohos_'), | ||
| 34 | + _environmentConfig = environmentConfig, | ||
| 35 | + _userIdProvider = userIdProvider, | ||
| 36 | + _uploadResultsAfterCalculation = uploadResultsAfterCalculation, | ||
| 37 | + _serverHealthApi = serverHealthApi; | ||
| 38 | + | ||
| 39 | + final HealthRawDataSource _rawDataSource; | ||
| 40 | + final HealthRawStressLocalStore _localStore; | ||
| 41 | + final AppEnvironmentConfig? _environmentConfig; | ||
| 42 | + final int Function()? _userIdProvider; | ||
| 43 | + final bool _uploadResultsAfterCalculation; | ||
| 44 | + final HealthApi? _serverHealthApi; | ||
| 45 | + final StreamController<HealthRawDataUpdatedEvent> | ||
| 46 | + _healthDataUpdatedController = | ||
| 47 | + StreamController<HealthRawDataUpdatedEvent>.broadcast(); | ||
| 48 | + Future<HealthRawStressCalculationResult>? _coreCalculation; | ||
| 49 | + | ||
| 50 | + bool get _isDebug => _environmentConfig?.isDebug ?? false; | ||
| 51 | + | ||
| 52 | + int get _userId { | ||
| 53 | + final userId = _userIdProvider?.call() ?? 0; | ||
| 54 | + if (userId <= 0) { | ||
| 55 | + throw StateError('OHOSHealthRawDataCoreService requires a valid userId'); | ||
| 56 | + } | ||
| 57 | + return userId; | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + Future<void> openDatabase() { | ||
| 61 | + return _localStore.openDatabase(_userId); | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + Future<void> closeDatabase() { | ||
| 65 | + return _localStore.closeDatabase(userId: _userId); | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + Future<bool> hasHealthData() { | ||
| 69 | + return _rawDataSource.hasHealthData(); | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + Future<bool> performHealthDataUpload() async { | ||
| 73 | + // TODO: Trigger OHOS raw-data upload when the OHOS upload API is ready. | ||
| 74 | + return false; | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + Future<bool> syncDatabaseToNativeIfNeeded() async { | ||
| 78 | + // OHOS must not call the Apple Pigeon database sync path. | ||
| 79 | + return false; | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + Stream<HealthRawDataUpdatedEvent> get healthDataUpdatedStream => | ||
| 83 | + _healthDataUpdatedController.stream; | ||
| 84 | + | ||
| 85 | + Future<void> onHealthDataUpdated({List<int>? dataTypes}) async { | ||
| 86 | + await startCoreCaculate(); | ||
| 87 | + _healthDataUpdatedController.add( | ||
| 88 | + HealthRawDataUpdatedEvent(dataTypes: dataTypes ?? const <int>[]), | ||
| 89 | + ); | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + Future<String> databaseFilePath() { | ||
| 93 | + return _localStore.dbPath(_userId); | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + Future<Uint8List> readDatabaseFileBytes() async { | ||
| 97 | + final userId = _userId; | ||
| 98 | + await _localStore.prepareDatabaseFileForShare(userId); | ||
| 99 | + final path = await _localStore.dbPath(userId); | ||
| 100 | + final file = File(path); | ||
| 101 | + if (!await file.exists()) { | ||
| 102 | + throw StateError('OHOSHealthRawDataCoreService database file not found'); | ||
| 103 | + } | ||
| 104 | + return file.readAsBytes(); | ||
| 105 | + } | ||
| 106 | + | ||
| 107 | + Future<String> readUploadApiLogText() async { | ||
| 108 | + return _isDebug ? 'OHOS upload API log is not implemented yet.' : ''; | ||
| 109 | + } | ||
| 110 | + | ||
| 111 | + Future<void> clearLocalDatabaseAndUploadLog() { | ||
| 112 | + return _localStore.clearTables(_userId); | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + Future<void> clearUploadApiLog() async { | ||
| 116 | + // TODO: Clear OHOS upload log after the OHOS upload/debug store exists. | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | + Future<bool> shareNativeAppleHealthObserverRecord() async { | ||
| 120 | + return false; | ||
| 121 | + } | ||
| 122 | + | ||
| 123 | + Future<bool> shareFlutterObserverRecord() async { | ||
| 124 | + return false; | ||
| 125 | + } | ||
| 126 | + | ||
| 127 | + Future<bool> shareUploadTaskRecord() async { | ||
| 128 | + return false; | ||
| 129 | + } | ||
| 130 | + | ||
| 131 | + Future<String> readLocalNotificationDebugLogText() async { | ||
| 132 | + return _isDebug | ||
| 133 | + ? 'OHOS notification debug log is not implemented yet.' | ||
| 134 | + : ''; | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + Future<bool> shareLocalNotificationDebugRecord() async { | ||
| 138 | + return false; | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + Future<HealthRawStressCalculationResult> startCoreCaculate({ | ||
| 142 | + int? endTime, | ||
| 143 | + int readChunkDays = defaultReadChunkDays, | ||
| 144 | + }) async { | ||
| 145 | + final running = _coreCalculation; | ||
| 146 | + if (running != null) return running; | ||
| 147 | + final task = _readCalculateAndStore( | ||
| 148 | + endTime: endTime, | ||
| 149 | + readChunkDays: readChunkDays, | ||
| 150 | + forceStartTime: null, | ||
| 151 | + ); | ||
| 152 | + _coreCalculation = task; | ||
| 153 | + try { | ||
| 154 | + return await task; | ||
| 155 | + } catch (error, stackTrace) { | ||
| 156 | + _logError('startCoreCaculate failed', error, stackTrace); | ||
| 157 | + rethrow; | ||
| 158 | + } finally { | ||
| 159 | + if (identical(_coreCalculation, task)) { | ||
| 160 | + _coreCalculation = null; | ||
| 161 | + } | ||
| 162 | + } | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + Future<HealthRawStressCalculationResult> syncAndStore({ | ||
| 166 | + int? startTime, | ||
| 167 | + int? endTime, | ||
| 168 | + int readChunkDays = defaultReadChunkDays, | ||
| 169 | + }) async { | ||
| 170 | + try { | ||
| 171 | + return await _readCalculateAndStore( | ||
| 172 | + endTime: endTime, | ||
| 173 | + readChunkDays: readChunkDays, | ||
| 174 | + forceStartTime: startTime, | ||
| 175 | + ); | ||
| 176 | + } catch (error, stackTrace) { | ||
| 177 | + _logError('syncAndStore failed', error, stackTrace); | ||
| 178 | + rethrow; | ||
| 179 | + } | ||
| 180 | + } | ||
| 181 | + | ||
| 182 | + Future<HealthRawStressCalculationResult> _readCalculateAndStore({ | ||
| 183 | + required int? endTime, | ||
| 184 | + required int readChunkDays, | ||
| 185 | + required int? forceStartTime, | ||
| 186 | + }) async { | ||
| 187 | + if (readChunkDays <= 0) { | ||
| 188 | + throw ArgumentError.value(readChunkDays, 'readChunkDays'); | ||
| 189 | + } | ||
| 190 | + | ||
| 191 | + final userId = _userId; | ||
| 192 | + await _localStore.ensureReadable(userId); | ||
| 193 | + final effectiveEndTime = | ||
| 194 | + endTime ?? DateTime.now().millisecondsSinceEpoch ~/ 1000; | ||
| 195 | + final earliestStartTime = DateTime.now() | ||
| 196 | + .subtract(const Duration(days: defaultLookbackDays)) | ||
| 197 | + .millisecondsSinceEpoch ~/ | ||
| 198 | + 1000; | ||
| 199 | + final requestedStartTime = | ||
| 200 | + math.max(forceStartTime ?? earliestStartTime, earliestStartTime); | ||
| 201 | + if (effectiveEndTime < requestedStartTime) { | ||
| 202 | + throw ArgumentError.value(endTime, 'endTime'); | ||
| 203 | + } | ||
| 204 | + | ||
| 205 | + final hrvContextStart = await _localStore.latestHrvSourceStartTime(userId); | ||
| 206 | + final realtimeContextStart = | ||
| 207 | + await _localStore.latestRealtimeSourceStartTime(userId); | ||
| 208 | + final latestHrvRawEndTime = await _localStore.latestHrvRawEndTime(userId); | ||
| 209 | + final latestRealtimeRawEndTime = | ||
| 210 | + await _localStore.latestRealtimeRawEndTime(userId); | ||
| 211 | + final latestSleepResultTime = await _localStore.latestSleepResultTime( | ||
| 212 | + userId, | ||
| 213 | + ); | ||
| 214 | + final hrvStartTime = math.max( | ||
| 215 | + hrvContextStart ?? requestedStartTime, | ||
| 216 | + earliestStartTime, | ||
| 217 | + ); | ||
| 218 | + final realtimeStartTime = math.max( | ||
| 219 | + realtimeContextStart ?? requestedStartTime, | ||
| 220 | + earliestStartTime, | ||
| 221 | + ); | ||
| 222 | + final heartRateStartTime = math.max( | ||
| 223 | + _minNullable(hrvStartTime, realtimeStartTime) ?? requestedStartTime, | ||
| 224 | + earliestStartTime, | ||
| 225 | + ); | ||
| 226 | + final sleepStartTime = math.max( | ||
| 227 | + latestSleepResultTime == null | ||
| 228 | + ? requestedStartTime | ||
| 229 | + : latestSleepResultTime - Duration.secondsPerDay, | ||
| 230 | + earliestStartTime, | ||
| 231 | + ); | ||
| 232 | + | ||
| 233 | + final hrvPoints = await _fetchRawDataInChunks( | ||
| 234 | + HealthDataUploadType.hrv.type, | ||
| 235 | + hrvStartTime, | ||
| 236 | + effectiveEndTime, | ||
| 237 | + readChunkDays: readChunkDays, | ||
| 238 | + ); | ||
| 239 | + final heartRatePoints = await _fetchRawDataInChunks( | ||
| 240 | + HealthDataUploadType.heartRate.type, | ||
| 241 | + heartRateStartTime, | ||
| 242 | + effectiveEndTime, | ||
| 243 | + readChunkDays: readChunkDays, | ||
| 244 | + ); | ||
| 245 | + final restingHeartRatePoints = await _fetchRawDataInChunks( | ||
| 246 | + HealthDataUploadType.restingHeartRate.type, | ||
| 247 | + heartRateStartTime, | ||
| 248 | + effectiveEndTime, | ||
| 249 | + readChunkDays: readChunkDays, | ||
| 250 | + ); | ||
| 251 | + final sleepIntervals = await _fetchSleepIntervalsInChunks( | ||
| 252 | + sleepStartTime, | ||
| 253 | + effectiveEndTime, | ||
| 254 | + readChunkDays: readChunkDays, | ||
| 255 | + ); | ||
| 256 | + final workoutIntervals = await _fetchWorkoutIntervalsInChunks( | ||
| 257 | + heartRateStartTime, | ||
| 258 | + effectiveEndTime, | ||
| 259 | + readChunkDays: readChunkDays, | ||
| 260 | + ); | ||
| 261 | + | ||
| 262 | + final result = await Isolate.run( | ||
| 263 | + () => HealthRawStressCalculator(userId: userId).calculate( | ||
| 264 | + hrvPoints: hrvPoints, | ||
| 265 | + heartRatePoints: heartRatePoints, | ||
| 266 | + restingHeartRatePoints: restingHeartRatePoints, | ||
| 267 | + sleepIntervals: sleepIntervals, | ||
| 268 | + workoutIntervals: workoutIntervals, | ||
| 269 | + startTime: math.min(hrvStartTime, heartRateStartTime), | ||
| 270 | + endTime: effectiveEndTime, | ||
| 271 | + ), | ||
| 272 | + debugName: 'OHOSHealthRawStressCalculator', | ||
| 273 | + ); | ||
| 274 | + final newResult = result.copyWith( | ||
| 275 | + hrvStressPoints: _filterNewHrvStressPoints( | ||
| 276 | + result.hrvStressPoints, | ||
| 277 | + latestHrvRawEndTime, | ||
| 278 | + ), | ||
| 279 | + realtimeStressPoints: _filterNewRealtimeStressPoints( | ||
| 280 | + result.realtimeStressPoints, | ||
| 281 | + latestRealtimeRawEndTime, | ||
| 282 | + ), | ||
| 283 | + ); | ||
| 284 | + await _localStore.upsertResult(newResult); | ||
| 285 | + | ||
| 286 | + final dailyStressPoints = await _calculateAndStoreDailyStressPoints( | ||
| 287 | + userId: userId, | ||
| 288 | + realtimePoints: newResult.realtimeStressPoints, | ||
| 289 | + nowSeconds: effectiveEndTime, | ||
| 290 | + ); | ||
| 291 | + final sleepResults = await _calculateAndStoreSleepResults( | ||
| 292 | + userId: userId, | ||
| 293 | + sleepIntervals: sleepIntervals, | ||
| 294 | + latestSleepResultTime: latestSleepResultTime, | ||
| 295 | + ); | ||
| 296 | + if (_uploadResultsAfterCalculation) { | ||
| 297 | + await _uploadResults(); | ||
| 298 | + } | ||
| 299 | + return newResult.copyWith( | ||
| 300 | + dailyStressPoints: dailyStressPoints, | ||
| 301 | + sleepResults: sleepResults, | ||
| 302 | + ); | ||
| 303 | + } | ||
| 304 | + | ||
| 305 | + Stream<HealthKitRawDataPoint> streamRawData({ | ||
| 306 | + required int dataType, | ||
| 307 | + required int startTime, | ||
| 308 | + required int endTime, | ||
| 309 | + int readChunkDays = defaultReadChunkDays, | ||
| 310 | + }) async* { | ||
| 311 | + final chunkSeconds = readChunkDays * Duration.secondsPerDay; | ||
| 312 | + var cursor = startTime; | ||
| 313 | + while (cursor <= endTime) { | ||
| 314 | + final chunkEnd = math.min(cursor + chunkSeconds - 1, endTime); | ||
| 315 | + final points = await _rawDataSource.getRawData( | ||
| 316 | + dataType, | ||
| 317 | + cursor, | ||
| 318 | + chunkEnd, | ||
| 319 | + ); | ||
| 320 | + for (final point in points) { | ||
| 321 | + yield point; | ||
| 322 | + } | ||
| 323 | + cursor = chunkEnd + 1; | ||
| 324 | + } | ||
| 325 | + } | ||
| 326 | + | ||
| 327 | + Stream<HealthKitRawDataPoint> streamRawSleepData({ | ||
| 328 | + required int startTime, | ||
| 329 | + required int endTime, | ||
| 330 | + int readChunkDays = defaultReadChunkDays, | ||
| 331 | + }) async* { | ||
| 332 | + final chunkSeconds = readChunkDays * Duration.secondsPerDay; | ||
| 333 | + var cursor = startTime; | ||
| 334 | + while (cursor <= endTime) { | ||
| 335 | + final chunkEnd = math.min(cursor + chunkSeconds - 1, endTime); | ||
| 336 | + final groups = await _rawDataSource.getRawSleepData(cursor, chunkEnd); | ||
| 337 | + final points = groups | ||
| 338 | + .expand((group) => group.sleepDataPoints) | ||
| 339 | + .where( | ||
| 340 | + (point) => point.endTime >= cursor && point.startTime <= chunkEnd) | ||
| 341 | + .toList(); | ||
| 342 | + for (final point in points) { | ||
| 343 | + yield point; | ||
| 344 | + } | ||
| 345 | + cursor = chunkEnd + 1; | ||
| 346 | + } | ||
| 347 | + } | ||
| 348 | + | ||
| 349 | + Stream<HealthKitRawWorkoutDataPoint> streamRawWorkoutData({ | ||
| 350 | + required int startTime, | ||
| 351 | + required int endTime, | ||
| 352 | + int readChunkDays = defaultReadChunkDays, | ||
| 353 | + }) async* { | ||
| 354 | + final chunkSeconds = readChunkDays * Duration.secondsPerDay; | ||
| 355 | + var cursor = startTime; | ||
| 356 | + while (cursor <= endTime) { | ||
| 357 | + final chunkEnd = math.min(cursor + chunkSeconds - 1, endTime); | ||
| 358 | + final points = await _rawDataSource.getRawWorkoutData(cursor, chunkEnd); | ||
| 359 | + for (final point in points.where( | ||
| 360 | + (point) => point.endTime >= cursor && point.startTime <= chunkEnd)) { | ||
| 361 | + yield point; | ||
| 362 | + } | ||
| 363 | + cursor = chunkEnd + 1; | ||
| 364 | + } | ||
| 365 | + } | ||
| 366 | + | ||
| 367 | + Future<List<HealthRawHrvStressPoint>> queryHrvStressPoints({ | ||
| 368 | + required int startTime, | ||
| 369 | + required int endTime, | ||
| 370 | + }) { | ||
| 371 | + return _localStore.queryHrvStressPoints( | ||
| 372 | + userId: _userId, | ||
| 373 | + startTime: startTime, | ||
| 374 | + endTime: endTime, | ||
| 375 | + ); | ||
| 376 | + } | ||
| 377 | + | ||
| 378 | + Future<List<HealthRawRealtimeStressPoint>> queryRealtimeStressPoints({ | ||
| 379 | + required int startTime, | ||
| 380 | + required int endTime, | ||
| 381 | + }) { | ||
| 382 | + return _localStore.queryRealtimeStressPoints( | ||
| 383 | + userId: _userId, | ||
| 384 | + startTime: startTime, | ||
| 385 | + endTime: endTime, | ||
| 386 | + ); | ||
| 387 | + } | ||
| 388 | + | ||
| 389 | + Future<int?> queryEarliestHrRawEndTime() { | ||
| 390 | + return _localStore.earliestRealtimeRawEndTime(_userId); | ||
| 391 | + } | ||
| 392 | + | ||
| 393 | + Future<List<HealthRawDailyStressPoint>> queryDailyStressPoints({ | ||
| 394 | + required int startDate, | ||
| 395 | + required int endDate, | ||
| 396 | + }) { | ||
| 397 | + return _localStore.queryDailyStressPoints( | ||
| 398 | + userId: _userId, | ||
| 399 | + startDate: startDate, | ||
| 400 | + endDate: endDate, | ||
| 401 | + ); | ||
| 402 | + } | ||
| 403 | + | ||
| 404 | + Future<List<HealthRawSleepResult>> querySleepResults({ | ||
| 405 | + required int startTime, | ||
| 406 | + required int endTime, | ||
| 407 | + }) { | ||
| 408 | + return _localStore.querySleepResults( | ||
| 409 | + userId: _userId, | ||
| 410 | + startTime: startTime, | ||
| 411 | + endTime: endTime, | ||
| 412 | + ); | ||
| 413 | + } | ||
| 414 | + | ||
| 415 | + Future<List<HealthKitRawDataPoint>> queryRawDataPoints({ | ||
| 416 | + required int dataType, | ||
| 417 | + required int startTime, | ||
| 418 | + required int endTime, | ||
| 419 | + int readChunkDays = defaultReadChunkDays, | ||
| 420 | + }) { | ||
| 421 | + return _fetchRawDataInChunks( | ||
| 422 | + dataType, | ||
| 423 | + startTime, | ||
| 424 | + endTime, | ||
| 425 | + readChunkDays: readChunkDays, | ||
| 426 | + ); | ||
| 427 | + } | ||
| 428 | + | ||
| 429 | + Future<List<HealthKitRawDataPoint>> queryRawSleepIntervals({ | ||
| 430 | + required int startTime, | ||
| 431 | + required int endTime, | ||
| 432 | + int readChunkDays = defaultReadChunkDays, | ||
| 433 | + }) { | ||
| 434 | + return _fetchSleepIntervalsInChunks( | ||
| 435 | + startTime, | ||
| 436 | + endTime, | ||
| 437 | + readChunkDays: readChunkDays, | ||
| 438 | + ); | ||
| 439 | + } | ||
| 440 | + | ||
| 441 | + Future<List<HealthKitRawActivityDataPoint>> queryRawActivitySummaries({ | ||
| 442 | + required int startTime, | ||
| 443 | + required int endTime, | ||
| 444 | + }) async { | ||
| 445 | + final points = await _rawDataSource.getRawActivityData(startTime, endTime); | ||
| 446 | + return points | ||
| 447 | + .where((e) => e.endTime >= startTime && e.endTime <= endTime) | ||
| 448 | + .toList() | ||
| 449 | + ..sort((a, b) => a.endTime.compareTo(b.endTime)); | ||
| 450 | + } | ||
| 451 | + | ||
| 452 | + Future<void> markHrvStressUploaded({ | ||
| 453 | + required Iterable<int> rawEndTimes, | ||
| 454 | + }) { | ||
| 455 | + return _localStore.markHrvStressUploaded( | ||
| 456 | + userId: _userId, | ||
| 457 | + rawEndTimes: rawEndTimes, | ||
| 458 | + ); | ||
| 459 | + } | ||
| 460 | + | ||
| 461 | + Future<void> markRealtimeStressUploaded({ | ||
| 462 | + required Iterable<int> rawEndTimes, | ||
| 463 | + }) { | ||
| 464 | + return _localStore.markRealtimeStressUploaded( | ||
| 465 | + userId: _userId, | ||
| 466 | + rawEndTimes: rawEndTimes, | ||
| 467 | + ); | ||
| 468 | + } | ||
| 469 | + | ||
| 470 | + Future<void> _uploadResults() async { | ||
| 471 | + // TODO: Upload OHOS result rows through OHOS/backend APIs and mark uploaded | ||
| 472 | + // rows in _localStore after the contract is available. | ||
| 473 | + if (_serverHealthApi == null) return; | ||
| 474 | + } | ||
| 475 | + | ||
| 476 | + Future<List<HealthRawDailyStressPoint>> _calculateAndStoreDailyStressPoints({ | ||
| 477 | + required int userId, | ||
| 478 | + required List<HealthRawRealtimeStressPoint> realtimePoints, | ||
| 479 | + required int nowSeconds, | ||
| 480 | + }) async { | ||
| 481 | + final todayDate = _dateKeyFromUnixSeconds(nowSeconds); | ||
| 482 | + final affectedDates = <int>{ | ||
| 483 | + todayDate, | ||
| 484 | + for (final point in realtimePoints) | ||
| 485 | + _dateKeyFromUnixSeconds(point.rawEndTime), | ||
| 486 | + }.toList() | ||
| 487 | + ..sort(); | ||
| 488 | + final existingDates = await _localStore.existingDailyStressDates( | ||
| 489 | + userId: userId, | ||
| 490 | + dates: affectedDates, | ||
| 491 | + ); | ||
| 492 | + final dailyStressPoints = <HealthRawDailyStressPoint>[]; | ||
| 493 | + final emptyDates = <int>[]; | ||
| 494 | + for (final date in affectedDates) { | ||
| 495 | + if (date != todayDate && existingDates.contains(date)) continue; | ||
| 496 | + final (startTime, endTime) = _dayRangeFromDateKey(date); | ||
| 497 | + final dayRealtimePoints = await _localStore.queryRealtimeStressPoints( | ||
| 498 | + userId: userId, | ||
| 499 | + startTime: startTime, | ||
| 500 | + endTime: endTime, | ||
| 501 | + ); | ||
| 502 | + final point = HealthRawDailyStressCalculator.calculate( | ||
| 503 | + userId: userId, | ||
| 504 | + date: date, | ||
| 505 | + realtimePoints: dayRealtimePoints, | ||
| 506 | + dataTime: date == todayDate ? nowSeconds : startTime, | ||
| 507 | + ); | ||
| 508 | + if (point == null) { | ||
| 509 | + emptyDates.add(date); | ||
| 510 | + continue; | ||
| 511 | + } | ||
| 512 | + dailyStressPoints.add(point); | ||
| 513 | + } | ||
| 514 | + await _localStore.upsertDailyStressPoints( | ||
| 515 | + userId: userId, | ||
| 516 | + points: dailyStressPoints, | ||
| 517 | + ); | ||
| 518 | + await _localStore.deleteDailyStressDates( | ||
| 519 | + userId: userId, | ||
| 520 | + dates: emptyDates, | ||
| 521 | + ); | ||
| 522 | + return dailyStressPoints; | ||
| 523 | + } | ||
| 524 | + | ||
| 525 | + Future<List<HealthRawSleepResult>> _calculateAndStoreSleepResults({ | ||
| 526 | + required int userId, | ||
| 527 | + required List<HealthKitRawDataPoint> sleepIntervals, | ||
| 528 | + required int? latestSleepResultTime, | ||
| 529 | + }) async { | ||
| 530 | + if (sleepIntervals.isEmpty) return const <HealthRawSleepResult>[]; | ||
| 531 | + final days = { | ||
| 532 | + for (final interval in sleepIntervals) | ||
| 533 | + DateTime.fromMillisecondsSinceEpoch(interval.endTime * 1000) | ||
| 534 | + }.map((date) => DateTime(date.year, date.month, date.day)).toList() | ||
| 535 | + ..sort((a, b) => a.compareTo(b)); | ||
| 536 | + final results = <HealthRawSleepResult>[]; | ||
| 537 | + for (final day in days) { | ||
| 538 | + final calculation = HealthSleepCalculator.calculateDay( | ||
| 539 | + day: day, | ||
| 540 | + sleepIntervals: sleepIntervals, | ||
| 541 | + ); | ||
| 542 | + final merged = calculation.mergeSleepTimeRange; | ||
| 543 | + final score = calculation.score; | ||
| 544 | + final state = calculation.state; | ||
| 545 | + if (merged == null || score == null || state == null) continue; | ||
| 546 | + if (!calculation.hasValidSleep) continue; | ||
| 547 | + if (latestSleepResultTime != null && | ||
| 548 | + merged.endTime <= latestSleepResultTime) { | ||
| 549 | + continue; | ||
| 550 | + } | ||
| 551 | + results.add( | ||
| 552 | + HealthRawSleepResult( | ||
| 553 | + userId: userId, | ||
| 554 | + date: merged.endTime, | ||
| 555 | + startDate: merged.startTime, | ||
| 556 | + sleepScore: score, | ||
| 557 | + sleepState: state.value, | ||
| 558 | + inBedMinutes: calculation.summary.timeInBedMinutes, | ||
| 559 | + awakMinutes: calculation.summary.awakeMinutes, | ||
| 560 | + sleepMinutes: calculation.summary.sleepMinutes, | ||
| 561 | + uploaded: false, | ||
| 562 | + ), | ||
| 563 | + ); | ||
| 564 | + } | ||
| 565 | + await _localStore.upsertSleepResults(userId: userId, results: results); | ||
| 566 | + return results; | ||
| 567 | + } | ||
| 568 | + | ||
| 569 | + List<HealthRawHrvStressPoint> _filterNewHrvStressPoints( | ||
| 570 | + List<HealthRawHrvStressPoint> points, | ||
| 571 | + int? latestRawEndTime, | ||
| 572 | + ) { | ||
| 573 | + if (latestRawEndTime == null) return points; | ||
| 574 | + return points | ||
| 575 | + .where((point) => point.rawEndTime > latestRawEndTime) | ||
| 576 | + .toList(); | ||
| 577 | + } | ||
| 578 | + | ||
| 579 | + List<HealthRawRealtimeStressPoint> _filterNewRealtimeStressPoints( | ||
| 580 | + List<HealthRawRealtimeStressPoint> points, | ||
| 581 | + int? latestRawEndTime, | ||
| 582 | + ) { | ||
| 583 | + if (latestRawEndTime == null) return points; | ||
| 584 | + return points | ||
| 585 | + .where((point) => point.rawEndTime > latestRawEndTime) | ||
| 586 | + .toList(); | ||
| 587 | + } | ||
| 588 | + | ||
| 589 | + Future<List<HealthKitRawDataPoint>> _fetchRawDataInChunks( | ||
| 590 | + int dataType, | ||
| 591 | + int startTime, | ||
| 592 | + int endTime, { | ||
| 593 | + required int readChunkDays, | ||
| 594 | + }) async { | ||
| 595 | + final points = <HealthKitRawDataPoint>[]; | ||
| 596 | + await for (final point in streamRawData( | ||
| 597 | + dataType: dataType, | ||
| 598 | + startTime: startTime, | ||
| 599 | + endTime: endTime, | ||
| 600 | + readChunkDays: readChunkDays, | ||
| 601 | + )) { | ||
| 602 | + points.add(point); | ||
| 603 | + } | ||
| 604 | + points.sort((a, b) => a.endTime.compareTo(b.endTime)); | ||
| 605 | + return points; | ||
| 606 | + } | ||
| 607 | + | ||
| 608 | + Future<List<HealthKitRawDataPoint>> _fetchSleepIntervalsInChunks( | ||
| 609 | + int startTime, | ||
| 610 | + int endTime, { | ||
| 611 | + required int readChunkDays, | ||
| 612 | + }) async { | ||
| 613 | + final points = <HealthKitRawDataPoint>[]; | ||
| 614 | + await for (final point in streamRawSleepData( | ||
| 615 | + startTime: startTime, | ||
| 616 | + endTime: endTime, | ||
| 617 | + readChunkDays: readChunkDays, | ||
| 618 | + )) { | ||
| 619 | + points.add(point); | ||
| 620 | + } | ||
| 621 | + points.sort((a, b) => a.endTime.compareTo(b.endTime)); | ||
| 622 | + return points; | ||
| 623 | + } | ||
| 624 | + | ||
| 625 | + Future<List<HealthKitRawWorkoutDataPoint>> _fetchWorkoutIntervalsInChunks( | ||
| 626 | + int startTime, | ||
| 627 | + int endTime, { | ||
| 628 | + required int readChunkDays, | ||
| 629 | + }) async { | ||
| 630 | + final points = <HealthKitRawWorkoutDataPoint>[]; | ||
| 631 | + await for (final point in streamRawWorkoutData( | ||
| 632 | + startTime: startTime, | ||
| 633 | + endTime: endTime, | ||
| 634 | + readChunkDays: readChunkDays, | ||
| 635 | + )) { | ||
| 636 | + points.add(point); | ||
| 637 | + } | ||
| 638 | + points.sort((a, b) => a.endTime.compareTo(b.endTime)); | ||
| 639 | + return points; | ||
| 640 | + } | ||
| 641 | + | ||
| 642 | + static int? _minNullable(int? a, int? b) { | ||
| 643 | + if (a == null) return b; | ||
| 644 | + if (b == null) return a; | ||
| 645 | + return math.min(a, b); | ||
| 646 | + } | ||
| 647 | + | ||
| 648 | + static int _dateKeyFromUnixSeconds(int seconds) { | ||
| 649 | + final date = DateTime.fromMillisecondsSinceEpoch(seconds * 1000); | ||
| 650 | + return date.year * 10000 + date.month * 100 + date.day; | ||
| 651 | + } | ||
| 652 | + | ||
| 653 | + static (int startTime, int endTime) _dayRangeFromDateKey(int dateKey) { | ||
| 654 | + final year = dateKey ~/ 10000; | ||
| 655 | + final month = (dateKey ~/ 100) % 100; | ||
| 656 | + final day = dateKey % 100; | ||
| 657 | + final start = DateTime(year, month, day); | ||
| 658 | + return ( | ||
| 659 | + start.millisecondsSinceEpoch ~/ 1000, | ||
| 660 | + start.add(const Duration(days: 1)).millisecondsSinceEpoch ~/ 1000 - 1, | ||
| 661 | + ); | ||
| 662 | + } | ||
| 663 | + | ||
| 664 | + static void _logError( | ||
| 665 | + String message, | ||
| 666 | + Object error, | ||
| 667 | + StackTrace stackTrace, | ||
| 668 | + ) { | ||
| 669 | + final tagged = 'OHOSHealthRawDataCoreService $message: $error'; | ||
| 670 | + developer.log( | ||
| 671 | + tagged, | ||
| 672 | + name: 'OHOSHealthRawDataCoreService', | ||
| 673 | + error: error, | ||
| 674 | + stackTrace: stackTrace, | ||
| 675 | + ); | ||
| 676 | + debugPrint(tagged); | ||
| 677 | + try { | ||
| 678 | + AppLogger.e(tagged, error, stackTrace); | ||
| 679 | + } catch (_) { | ||
| 680 | + // Logger may be unavailable in isolated unit tests. | ||
| 681 | + } | ||
| 682 | + } | ||
| 683 | +} |
| 1 | -import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart'; | 1 | +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 2 | import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; | 2 | import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; |
| 3 | 3 | ||
| 4 | import '../logging/app_logger.dart'; | 4 | import '../logging/app_logger.dart'; |
| 1 | import 'package:doublefeel_flutter/core/error/app_error.dart'; | 1 | import 'package:doublefeel_flutter/core/error/app_error.dart'; |
| 2 | import 'package:doublefeel_flutter/core/result/app_result.dart'; | 2 | import 'package:doublefeel_flutter/core/result/app_result.dart'; |
| 3 | -import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart'; | 3 | +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 4 | import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart'; | 4 | import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart'; |
| 5 | import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; | 5 | import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; |
| 6 | import 'package:doublefeel_flutter/data/models/health/activity/activity_burn_statistics_data_v2.dart'; | 6 | import 'package:doublefeel_flutter/data/models/health/activity/activity_burn_statistics_data_v2.dart'; |
| 1 | import 'dart:convert'; | 1 | import 'dart:convert'; |
| 2 | 2 | ||
| 3 | import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; | 3 | import 'package:doublefeel_flutter/core/config/app_environment_config.dart'; |
| 4 | -import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart'; | ||
| 5 | -import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'; | 4 | +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 6 | import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; | 5 | import 'package:doublefeel_flutter/pigeon/platform_api.g.dart'; |
| 7 | import 'package:get/get.dart'; | 6 | import 'package:get/get.dart'; |
| 8 | import 'package:shared_preferences/shared_preferences.dart'; | 7 | import 'package:shared_preferences/shared_preferences.dart'; |
| @@ -73,14 +72,7 @@ class UserPreferencesStorage { | @@ -73,14 +72,7 @@ class UserPreferencesStorage { | ||
| 73 | 72 | ||
| 74 | Future<void> _syncHealthRawDatabaseToNative() async { | 73 | Future<void> _syncHealthRawDatabaseToNative() async { |
| 75 | if (!Get.isRegistered<HealthRawDataCoreService>()) return; | 74 | if (!Get.isRegistered<HealthRawDataCoreService>()) return; |
| 76 | - final databasePath = | ||
| 77 | - await Get.find<HealthRawDataCoreService>().databaseFilePath(); | ||
| 78 | - await HealthKitRawDataHostApi().syncDatabase( | ||
| 79 | - hrDataPath: databasePath, | ||
| 80 | - hrvDataPath: databasePath, | ||
| 81 | - sleepDataPath: databasePath, | ||
| 82 | - avgRealtimeStressDataPath: databasePath, | ||
| 83 | - ); | 75 | + await Get.find<HealthRawDataCoreService>().syncDatabaseToNativeIfNeeded(); |
| 84 | } | 76 | } |
| 85 | 77 | ||
| 86 | Future<void> _persist(UserPreferences value) async { | 78 | Future<void> _persist(UserPreferences value) async { |
| @@ -8,7 +8,7 @@ import 'package:intl/date_symbol_data_local.dart'; | @@ -8,7 +8,7 @@ import 'package:intl/date_symbol_data_local.dart'; | ||
| 8 | 8 | ||
| 9 | import 'app/bootstrap/app_bootstrap.dart'; | 9 | import 'app/bootstrap/app_bootstrap.dart'; |
| 10 | import 'app/double_feel_app.dart'; | 10 | import 'app/double_feel_app.dart'; |
| 11 | -import 'core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart'; | 11 | +import 'core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 12 | import 'core/theme/app_theme.dart'; | 12 | import 'core/theme/app_theme.dart'; |
| 13 | import 'data/local/user_preferences_storage.dart'; | 13 | import 'data/local/user_preferences_storage.dart'; |
| 14 | 14 |
| @@ -117,6 +117,7 @@ abstract class PlatformHostApi { | @@ -117,6 +117,7 @@ abstract class PlatformHostApi { | ||
| 117 | String getFullUserAgent(); | 117 | String getFullUserAgent(); |
| 118 | 118 | ||
| 119 | /// 是否是中国大陆地区 | 119 | /// 是否是中国大陆地区 |
| 120 | + @async | ||
| 120 | bool isChinaRegion(); | 121 | bool isChinaRegion(); |
| 121 | 122 | ||
| 122 | /// 申请通知权限 | 123 | /// 申请通知权限 |
| 1 | +import 'dart:io'; | ||
| 2 | + | ||
| 3 | +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; | ||
| 4 | +import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/apple_health_raw_data_core_service.dart'; | ||
| 5 | +import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ohos/ohos_health_raw_data_core_service.dart'; | ||
| 6 | +import 'package:flutter/foundation.dart'; | ||
| 7 | +import 'package:flutter_test/flutter_test.dart'; | ||
| 8 | + | ||
| 9 | +void main() { | ||
| 10 | + test('facade routes iOS calls to Apple implementation', () async { | ||
| 11 | + final service = HealthRawDataCoreService( | ||
| 12 | + appleService: _FakeAppleHealthRawDataCoreService(label: 'apple'), | ||
| 13 | + ohosService: _FakeOhosHealthRawDataCoreService(label: 'ohos'), | ||
| 14 | + targetPlatform: TargetPlatform.iOS, | ||
| 15 | + ); | ||
| 16 | + | ||
| 17 | + expect(await service.databaseFilePath(), 'apple'); | ||
| 18 | + expect(await service.performHealthDataUpload(), isTrue); | ||
| 19 | + }); | ||
| 20 | + | ||
| 21 | + test('facade routes non-iOS calls to OHOS implementation', () async { | ||
| 22 | + final service = HealthRawDataCoreService( | ||
| 23 | + appleService: _FakeAppleHealthRawDataCoreService(label: 'apple'), | ||
| 24 | + ohosService: _FakeOhosHealthRawDataCoreService(label: 'ohos'), | ||
| 25 | + targetPlatform: TargetPlatform.android, | ||
| 26 | + ); | ||
| 27 | + | ||
| 28 | + expect(await service.databaseFilePath(), 'ohos'); | ||
| 29 | + expect(await service.performHealthDataUpload(), isFalse); | ||
| 30 | + }); | ||
| 31 | + | ||
| 32 | + test('OHOS result database is isolated from iOS result database', () async { | ||
| 33 | + final tempDir = await Directory.systemTemp.createTemp('health_raw_test_'); | ||
| 34 | + addTearDown(() => tempDir.delete(recursive: true)); | ||
| 35 | + const userId = 42; | ||
| 36 | + | ||
| 37 | + final apple = AppleHealthRawDataCoreService( | ||
| 38 | + localStore: HealthRawStressLocalStore(rootDirectory: tempDir), | ||
| 39 | + userIdProvider: () => userId, | ||
| 40 | + uploadResultsAfterCalculation: false, | ||
| 41 | + ); | ||
| 42 | + final ohos = OHOSHealthRawDataCoreService( | ||
| 43 | + localStore: HealthRawStressLocalStore( | ||
| 44 | + rootDirectory: tempDir, | ||
| 45 | + databaseNamePrefix: 'ohos_', | ||
| 46 | + ), | ||
| 47 | + userIdProvider: () => userId, | ||
| 48 | + uploadResultsAfterCalculation: false, | ||
| 49 | + ); | ||
| 50 | + | ||
| 51 | + expect( | ||
| 52 | + await apple.databaseFilePath(), | ||
| 53 | + '${tempDir.path}/hrv_result_$userId.sqlite', | ||
| 54 | + ); | ||
| 55 | + expect( | ||
| 56 | + await ohos.databaseFilePath(), | ||
| 57 | + '${tempDir.path}/ohos_hrv_result_$userId.sqlite', | ||
| 58 | + ); | ||
| 59 | + }); | ||
| 60 | +} | ||
| 61 | + | ||
| 62 | +class _FakeAppleHealthRawDataCoreService extends AppleHealthRawDataCoreService { | ||
| 63 | + _FakeAppleHealthRawDataCoreService({required this.label}) | ||
| 64 | + : super( | ||
| 65 | + userIdProvider: () => 1, | ||
| 66 | + uploadResultsAfterCalculation: false, | ||
| 67 | + ); | ||
| 68 | + | ||
| 69 | + final String label; | ||
| 70 | + | ||
| 71 | + @override | ||
| 72 | + Future<String> databaseFilePath() async { | ||
| 73 | + return label; | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + @override | ||
| 77 | + Future<bool> performHealthDataUpload() async { | ||
| 78 | + return true; | ||
| 79 | + } | ||
| 80 | +} | ||
| 81 | + | ||
| 82 | +class _FakeOhosHealthRawDataCoreService extends OHOSHealthRawDataCoreService { | ||
| 83 | + _FakeOhosHealthRawDataCoreService({required this.label}) | ||
| 84 | + : super( | ||
| 85 | + userIdProvider: () => 1, | ||
| 86 | + uploadResultsAfterCalculation: false, | ||
| 87 | + ); | ||
| 88 | + | ||
| 89 | + final String label; | ||
| 90 | + | ||
| 91 | + @override | ||
| 92 | + Future<String> databaseFilePath() async { | ||
| 93 | + return label; | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + @override | ||
| 97 | + Future<bool> performHealthDataUpload() async { | ||
| 98 | + return false; | ||
| 99 | + } | ||
| 100 | +} |
| 1 | import 'dart:async'; | 1 | import 'dart:async'; |
| 2 | 2 | ||
| 3 | import 'package:doublefeel_flutter/core/result/app_result.dart'; | 3 | import 'package:doublefeel_flutter/core/result/app_result.dart'; |
| 4 | -import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart'; | 4 | +import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart'; |
| 5 | import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart'; | 5 | import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart'; |
| 6 | -import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_local_notification.dart'; | 6 | +import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/apple_health_raw_data_core_service.dart'; |
| 7 | +import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/apple_health_raw_local_notification.dart'; | ||
| 7 | import 'package:doublefeel_flutter/data/datasource/health/health_local_data_convert.dart'; | 8 | import 'package:doublefeel_flutter/data/datasource/health/health_local_data_convert.dart'; |
| 8 | import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart'; | 9 | import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart'; |
| 9 | import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; | 10 | import 'package:doublefeel_flutter/data/models/enums/app_enums.dart'; |
| 10 | import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart'; | 11 | import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart'; |
| 11 | import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'; | 12 | import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart'; |
| 13 | +import 'package:flutter/foundation.dart'; | ||
| 12 | import 'package:flutter_test/flutter_test.dart'; | 14 | import 'package:flutter_test/flutter_test.dart'; |
| 13 | 15 | ||
| 14 | Future<void> _pumpAsyncUploads() async { | 16 | Future<void> _pumpAsyncUploads() async { |
| @@ -50,7 +52,7 @@ void main() { | @@ -50,7 +52,7 @@ void main() { | ||
| 50 | ]); | 52 | ]); |
| 51 | 53 | ||
| 52 | final store = _MemoryHealthRawStressLocalStore(); | 54 | final store = _MemoryHealthRawStressLocalStore(); |
| 53 | - final service = HealthRawDataCoreService( | 55 | + final service = AppleHealthRawDataCoreService( |
| 54 | healthApi: _FakeHealthKitHostApi(), | 56 | healthApi: _FakeHealthKitHostApi(), |
| 55 | rawDataApi: api, | 57 | rawDataApi: api, |
| 56 | localStore: store, | 58 | localStore: store, |
| @@ -185,7 +187,7 @@ void main() { | @@ -185,7 +187,7 @@ void main() { | ||
| 185 | uploaded: true, | 187 | uploaded: true, |
| 186 | ), | 188 | ), |
| 187 | ); | 189 | ); |
| 188 | - final service = HealthRawDataCoreService( | 190 | + final service = AppleHealthRawDataCoreService( |
| 189 | healthApi: _FakeHealthKitHostApi(), | 191 | healthApi: _FakeHealthKitHostApi(), |
| 190 | rawDataApi: api, | 192 | rawDataApi: api, |
| 191 | localStore: store, | 193 | localStore: store, |
| @@ -331,7 +333,7 @@ void main() { | @@ -331,7 +333,7 @@ void main() { | ||
| 331 | uploaded: true, | 333 | uploaded: true, |
| 332 | ), | 334 | ), |
| 333 | ); | 335 | ); |
| 334 | - final service = HealthRawDataCoreService( | 336 | + final service = AppleHealthRawDataCoreService( |
| 335 | healthApi: _FakeHealthKitHostApi(), | 337 | healthApi: _FakeHealthKitHostApi(), |
| 336 | rawDataApi: api, | 338 | rawDataApi: api, |
| 337 | localStore: store, | 339 | localStore: store, |
| @@ -392,7 +394,7 @@ void main() { | @@ -392,7 +394,7 @@ void main() { | ||
| 392 | test('startCoreCaculate skips raw reads when health auth is missing', | 394 | test('startCoreCaculate skips raw reads when health auth is missing', |
| 393 | () async { | 395 | () async { |
| 394 | final api = _FakeHealthKitRawDataHostApi(); | 396 | final api = _FakeHealthKitRawDataHostApi(); |
| 395 | - final service = HealthRawDataCoreService( | 397 | + final service = AppleHealthRawDataCoreService( |
| 396 | healthApi: _FakeHealthKitHostApi(status: 2), | 398 | healthApi: _FakeHealthKitHostApi(status: 2), |
| 397 | rawDataApi: api, | 399 | rawDataApi: api, |
| 398 | localStore: _MemoryHealthRawStressLocalStore(), | 400 | localStore: _MemoryHealthRawStressLocalStore(), |
| @@ -417,7 +419,7 @@ void main() { | @@ -417,7 +419,7 @@ void main() { | ||
| 417 | _point(now - 120, 35), | 419 | _point(now - 120, 35), |
| 418 | ]); | 420 | ]); |
| 419 | 421 | ||
| 420 | - final service = HealthRawDataCoreService( | 422 | + final service = AppleHealthRawDataCoreService( |
| 421 | healthApi: _FakeHealthKitHostApi(), | 423 | healthApi: _FakeHealthKitHostApi(), |
| 422 | rawDataApi: api, | 424 | rawDataApi: api, |
| 423 | localStore: _MemoryHealthRawStressLocalStore(), | 425 | localStore: _MemoryHealthRawStressLocalStore(), |
| @@ -454,7 +456,7 @@ void main() { | @@ -454,7 +456,7 @@ void main() { | ||
| 454 | ), | 456 | ), |
| 455 | ]); | 457 | ]); |
| 456 | final store = _MemoryHealthRawStressLocalStore(); | 458 | final store = _MemoryHealthRawStressLocalStore(); |
| 457 | - final service = HealthRawDataCoreService( | 459 | + final service = AppleHealthRawDataCoreService( |
| 458 | healthApi: _FakeHealthKitHostApi(), | 460 | healthApi: _FakeHealthKitHostApi(), |
| 459 | rawDataApi: api, | 461 | rawDataApi: api, |
| 460 | localStore: store, | 462 | localStore: store, |
| @@ -508,7 +510,7 @@ void main() { | @@ -508,7 +510,7 @@ void main() { | ||
| 508 | ), | 510 | ), |
| 509 | ]); | 511 | ]); |
| 510 | final store = _MemoryHealthRawStressLocalStore(); | 512 | final store = _MemoryHealthRawStressLocalStore(); |
| 511 | - final service = HealthRawDataCoreService( | 513 | + final service = AppleHealthRawDataCoreService( |
| 512 | healthApi: _FakeHealthKitHostApi(), | 514 | healthApi: _FakeHealthKitHostApi(), |
| 513 | rawDataApi: api, | 515 | rawDataApi: api, |
| 514 | localStore: store, | 516 | localStore: store, |
| @@ -587,7 +589,7 @@ void main() { | @@ -587,7 +589,7 @@ void main() { | ||
| 587 | ], | 589 | ], |
| 588 | ); | 590 | ); |
| 589 | final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher(); | 591 | final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher(); |
| 590 | - final service = HealthRawDataCoreService( | 592 | + final service = AppleHealthRawDataCoreService( |
| 591 | healthApi: _FakeHealthKitHostApi(), | 593 | healthApi: _FakeHealthKitHostApi(), |
| 592 | rawDataApi: api, | 594 | rawDataApi: api, |
| 593 | localStore: store, | 595 | localStore: store, |
| @@ -644,7 +646,7 @@ void main() { | @@ -644,7 +646,7 @@ void main() { | ||
| 644 | ], | 646 | ], |
| 645 | ); | 647 | ); |
| 646 | final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher(); | 648 | final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher(); |
| 647 | - final service = HealthRawDataCoreService( | 649 | + final service = AppleHealthRawDataCoreService( |
| 648 | healthApi: _FakeHealthKitHostApi(), | 650 | healthApi: _FakeHealthKitHostApi(), |
| 649 | rawDataApi: api, | 651 | rawDataApi: api, |
| 650 | localStore: store, | 652 | localStore: store, |
| @@ -699,7 +701,7 @@ void main() { | @@ -699,7 +701,7 @@ void main() { | ||
| 699 | ), | 701 | ), |
| 700 | ]); | 702 | ]); |
| 701 | final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher(); | 703 | final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher(); |
| 702 | - final service = HealthRawDataCoreService( | 704 | + final service = AppleHealthRawDataCoreService( |
| 703 | healthApi: _FakeHealthKitHostApi(), | 705 | healthApi: _FakeHealthKitHostApi(), |
| 704 | rawDataApi: api, | 706 | rawDataApi: api, |
| 705 | localStore: _MemoryHealthRawStressLocalStore(), | 707 | localStore: _MemoryHealthRawStressLocalStore(), |
| @@ -768,7 +770,7 @@ void main() { | @@ -768,7 +770,7 @@ void main() { | ||
| 768 | ), | 770 | ), |
| 769 | ], | 771 | ], |
| 770 | ); | 772 | ); |
| 771 | - final service = HealthRawDataCoreService( | 773 | + final service = AppleHealthRawDataCoreService( |
| 772 | healthApi: _FakeHealthKitHostApi(), | 774 | healthApi: _FakeHealthKitHostApi(), |
| 773 | rawDataApi: api, | 775 | rawDataApi: api, |
| 774 | localStore: store, | 776 | localStore: store, |
| @@ -809,7 +811,7 @@ void main() { | @@ -809,7 +811,7 @@ void main() { | ||
| 809 | dailyStressPoints: const <HealthRawDailyStressPoint>[], | 811 | dailyStressPoints: const <HealthRawDailyStressPoint>[], |
| 810 | ), | 812 | ), |
| 811 | ); | 813 | ); |
| 812 | - final service = HealthRawDataCoreService( | 814 | + final service = AppleHealthRawDataCoreService( |
| 813 | healthApi: _FakeHealthKitHostApi(), | 815 | healthApi: _FakeHealthKitHostApi(), |
| 814 | rawDataApi: api, | 816 | rawDataApi: api, |
| 815 | localStore: store, | 817 | localStore: store, |
| @@ -845,7 +847,7 @@ void main() { | @@ -845,7 +847,7 @@ void main() { | ||
| 845 | const base = 1800000000; | 847 | const base = 1800000000; |
| 846 | final api = _FakeHealthKitRawDataHostApi(); | 848 | final api = _FakeHealthKitRawDataHostApi(); |
| 847 | final store = _MemoryHealthRawStressLocalStore(); | 849 | final store = _MemoryHealthRawStressLocalStore(); |
| 848 | - final service = HealthRawDataCoreService( | 850 | + final service = AppleHealthRawDataCoreService( |
| 849 | healthApi: _FakeHealthKitHostApi(), | 851 | healthApi: _FakeHealthKitHostApi(), |
| 850 | rawDataApi: api, | 852 | rawDataApi: api, |
| 851 | localStore: store, | 853 | localStore: store, |
| @@ -864,7 +866,11 @@ void main() { | @@ -864,7 +866,11 @@ void main() { | ||
| 864 | ), | 866 | ), |
| 865 | ); | 867 | ); |
| 866 | 868 | ||
| 867 | - final result = await LocalHealthDataSource(coreService: service) | 869 | + final facade = HealthRawDataCoreService( |
| 870 | + appleService: service, | ||
| 871 | + targetPlatform: TargetPlatform.iOS, | ||
| 872 | + ); | ||
| 873 | + final result = await LocalHealthDataSource(coreService: facade) | ||
| 868 | .getHealthDataEverUploaded(); | 874 | .getHealthDataEverUploaded(); |
| 869 | 875 | ||
| 870 | expect(result, isA<AppSuccess>()); | 876 | expect(result, isA<AppSuccess>()); |
| 1 | import 'dart:io'; | 1 | import 'dart:io'; |
| 2 | 2 | ||
| 3 | import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart'; | 3 | import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart'; |
| 4 | -import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_local_notification.dart'; | 4 | +import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/apple_health_raw_local_notification.dart'; |
| 5 | import 'package:doublefeel_flutter/l10n/gen/app_localizations_zh.dart'; | 5 | import 'package:doublefeel_flutter/l10n/gen/app_localizations_zh.dart'; |
| 6 | import 'package:flutter_test/flutter_test.dart'; | 6 | import 'package:flutter_test/flutter_test.dart'; |
| 7 | 7 |
-
Please register or login to post a comment