Commit cb34ffd65ace7a4a9d690b540328546a6f93ddb3

Authored by 权海
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
@@ -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