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 {
*/
fun getFullUserAgent(): String
/** 是否是中国大陆地区 */
fun isChinaRegion(): Boolean
fun isChinaRegion(callback: (Result<Boolean>) -> Unit)
/** 申请通知权限 */
fun requestNotificationAuth(callback: (Result<Boolean>) -> Unit)
/** 是否是测试环境 */
... ... @@ -428,12 +428,15 @@ interface PlatformHostApi {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isChinaRegion$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.isChinaRegion())
} catch (exception: Throwable) {
PlatformApiPigeonUtils.wrapError(exception)
api.isChinaRegion{ result: Result<Boolean> ->
val error = result.exceptionOrNull()
if (error != null) {
reply.reply(PlatformApiPigeonUtils.wrapError(error))
} else {
val data = result.getOrNull()
reply.reply(PlatformApiPigeonUtils.wrapResult(data))
}
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
... ...
No preview for this file type
... ... @@ -369,7 +369,7 @@ protocol PlatformHostApi {
/// `{systemWebViewUA} {appName}/{versionCode}({versionName})({manufacturer}##{brand}##{model}; {OS}{osVersion}; {height}x{width})(huawei)`
func getFullUserAgent() throws -> String
/// 是否是中国大陆地区
func isChinaRegion() throws -> Bool
func isChinaRegion(completion: @escaping (Result<Bool, Error>) -> Void)
/// 申请通知权限
func requestNotificationAuth(completion: @escaping (Result<Bool, Error>) -> Void)
/// 是否是测试环境
... ... @@ -444,11 +444,13 @@ class PlatformHostApiSetup {
let isChinaRegionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.doublefeel_flutter.PlatformHostApi.isChinaRegion\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
isChinaRegionChannel.setMessageHandler { _, reply in
do {
let result = try api.isChinaRegion()
reply(wrapResult(result))
} catch {
reply(wrapError(error))
api.isChinaRegion { result in
switch result {
case .success(let res):
reply(wrapResult(res))
case .failure(let error):
reply(wrapError(error))
}
}
}
} else {
... ...
... ... @@ -55,19 +55,12 @@ final class PlatformHostApiImpl: PlatformHostApi {
#endif
}
/// 检查appstore 是否是中国大陆区
func isChinaRegion() throws -> Bool {
var localeCountryCode: String?
if let code = SKPaymentQueue.default().storefront?.countryCode{
localeCountryCode = code
}
if localeCountryCode == nil, #available(iOS 16.0, *) {
localeCountryCode = Locale.current.region?.identifier
}
if localeCountryCode == nil,
let code = (Locale.current as NSLocale).object(forKey: .countryCode) as? String{
localeCountryCode = code
func isChinaRegion(completion: @escaping (Result<Bool, any Error>) -> Void) {
Task{
let storefront = await Storefront.current
let isChinaMainLand = storefront?.countryCode == "CHN"
completion(.success(isChinaMainLand))
}
return localeCountryCode?.uppercased() == "CN"
}
func requestNotificationAuth(completion: @escaping (Result<Bool, any Error>) -> Void) {
... ...
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:get/get.dart';
import '../../core/config/app_environment_config.dart';
... ...
import 'package:doublefeel_flutter/core/network/api/health_api.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_datasource.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart';
... ...
import 'package:get/get.dart';
import '../../../../core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart';
import '../../../../core/services/raw_data_service/health_raw_data_core_service.dart';
import '../../../../data/local/user_preferences_storage.dart';
import '../controllers/developer_options_controller.dart';
... ...
... ... @@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../../core/logging/app_logger.dart';
import '../../../../core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart';
import '../../../../core/services/raw_data_service/health_raw_data_core_service.dart';
import '../../../../data/local/user_preferences_storage.dart';
import '../../../../pigeon/platform_api.g.dart';
import '../../../routes/app_pages.dart';
... ...
... ... @@ -3,7 +3,7 @@ import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/network/api/health_api.dart';
import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart';
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';
import 'package:doublefeel_flutter/core/network/api/theme_api.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart';
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
import 'package:doublefeel_flutter/core/constants/intent_keys.dart';
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/local/local_storage.dart';
... ...
... ... @@ -15,7 +15,7 @@ import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/core/util/app_toast.dart';
... ... @@ -28,7 +28,6 @@ import 'package:doublefeel_flutter/data/models/local/user_preferences.dart';
import 'package:doublefeel_flutter/data/models/pay/pay_models.dart';
import 'package:doublefeel_flutter/l10n/l10n_extensions.dart';
import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart';
import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart';
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import 'package:doublefeel_flutter/pigeon/wear_engine_api.g.dart';
import 'package:flutter/material.dart';
... ... @@ -75,7 +74,6 @@ class TodayController extends GetMaterialController {
final FriendApi _friendApi;
final UserStateService _userStateService;
final HealthKitHostApi _hostApi = HealthKitHostApi();
final HealthKitRawDataHostApi _rawDataApi = HealthKitRawDataHostApi();
final HealthRawDataCoreService _healthRawDataCoreService =
Get.find<HealthRawDataCoreService>();
... ... @@ -500,7 +498,7 @@ class TodayController extends GetMaterialController {
void _performHealthDataUpload() {
unawaited(
_rawDataApi
_healthRawDataCoreService
.performHealthDataUpload()
.catchError((Object error, StackTrace stackTrace) {
AppLogger.e('Apple Health raw upload failed', error, stackTrace);
... ...
import 'package:doublefeel_flutter/core/network/api/health_api.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_datasource.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart';
... ...
import 'package:doublefeel_flutter/core/network/api/health_api.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_datasource.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart';
... ...
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:flutter/scheduler.dart';
import 'package:get/get.dart';
... ... @@ -62,7 +62,7 @@ class SplashController extends GetxController {
// final me = (userResult as AppSuccess).data;
// print(me.nickname);
// }
if (userResult case AppSuccess(data: final me)) {
if (userResult is AppSuccess) {
// UserInfoResponse? partner;
// if ((me.pairId ?? 0) > 0) {
// final partnerResult = await _userApi.getPartnerUserInfo();
... ...
import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/local/user_account_storage.dart';
import 'package:doublefeel_flutter/data/local/user_preferences_storage.dart';
import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart';
import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart';
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
... ... @@ -127,7 +126,9 @@ class UserOnboardingController extends GetxController {
}
try {
report();
} on Exception catch (e) {}
} on Exception catch (error, stackTrace) {
AppLogger.e('Onboarding report failed', error, stackTrace);
}
currentPageIndex.value++;
if (currentPageIndex.value == 7) {
ta.track('enter_doublefeel_health_grant_page');
... ... @@ -231,8 +232,9 @@ class UserOnboardingController extends GetxController {
/// 计算 Apple Health 新数据。
Future<void> _performDataUpload() async {
try {
HealthKitRawDataHostApi().performHealthDataUpload();
await Get.find<HealthRawDataCoreService>().startCoreCaculate();
final service = Get.find<HealthRawDataCoreService>();
await service.performHealthDataUpload();
await service.startCoreCaculate();
} catch (error, stackTrace) {
AppLogger.e('Apple Health calculate failed', error, stackTrace);
}
... ... @@ -264,7 +266,9 @@ class UserOnboardingController extends GetxController {
if (result.status == 1) _performDataUpload();
return true;
}
} catch (e) {}
} catch (error, stackTrace) {
AppLogger.e('Health authorization failed', error, stackTrace);
}
}
}
return false;
... ... @@ -286,7 +290,7 @@ class UserOnboardingController extends GetxController {
}
// 未决定 → 弹出系统授权弹窗
final result = await _platformHostApi.requestNotificationAuth();
await _platformHostApi.requestNotificationAuth();
} catch (error) {
AppLogger.e('Notification authorization failed: $error');
}
... ...
... ... @@ -5,7 +5,7 @@ import 'package:sqflite/sqflite.dart';
import '../../../data/models/enums/app_enums.dart';
import '../../../pigeon/health_kit_raw_data_api.g.dart';
import 'platform_ios/health_raw_data_core_service.dart';
import 'platform_ios/apple_health_raw_data_core_service.dart';
import 'health_raw_models.dart';
import 'health_raw_stress_calculator.dart';
... ...
import 'dart:async';
import 'package:flutter/foundation.dart';
import '../../../pigeon/health_kit_raw_data_api.g.dart';
import '../../config/app_environment_config.dart';
import '../../network/api/health_api.dart';
import 'health_raw_models.dart';
import 'platform_ios/apple_health_raw_data_core_service.dart';
import 'platform_ohos/ohos_health_raw_data_core_service.dart';
class HealthRawDataCoreService {
static const int defaultLookbackDays =
AppleHealthRawDataCoreService.defaultLookbackDays;
static const int defaultReadChunkDays =
AppleHealthRawDataCoreService.defaultReadChunkDays;
HealthRawDataCoreService({
AppleHealthRawDataCoreService? appleService,
OHOSHealthRawDataCoreService? ohosService,
TargetPlatform? targetPlatform,
AppEnvironmentConfig? environmentConfig,
int Function()? userIdProvider,
bool uploadResultsAfterCalculation = true,
HealthApi? serverHealthApi,
}) : _appleService = appleService ??
AppleHealthRawDataCoreService(
environmentConfig: environmentConfig,
userIdProvider: userIdProvider,
uploadResultsAfterCalculation: uploadResultsAfterCalculation,
serverHealthApi: serverHealthApi,
),
_ohosService = ohosService ??
OHOSHealthRawDataCoreService(
environmentConfig: environmentConfig,
userIdProvider: userIdProvider,
uploadResultsAfterCalculation: uploadResultsAfterCalculation,
serverHealthApi: serverHealthApi,
),
_targetPlatform = targetPlatform;
final AppleHealthRawDataCoreService _appleService;
final OHOSHealthRawDataCoreService _ohosService;
final TargetPlatform? _targetPlatform;
dynamic get _currentService {
final platform = _targetPlatform ?? defaultTargetPlatform;
return platform == TargetPlatform.iOS ? _appleService : _ohosService;
}
Future<void> openDatabase() {
return _currentService.openDatabase();
}
Future<void> closeDatabase() {
return _currentService.closeDatabase();
}
Future<bool> hasHealthData() {
return _currentService.hasHealthData();
}
Future<bool> performHealthDataUpload() {
return _currentService.performHealthDataUpload();
}
Future<bool> syncDatabaseToNativeIfNeeded() {
return _currentService.syncDatabaseToNativeIfNeeded();
}
Stream<HealthRawDataUpdatedEvent> get healthDataUpdatedStream =>
_currentService.healthDataUpdatedStream;
Future<void> onHealthDataUpdated({List<int>? dataTypes}) {
return _currentService.onHealthDataUpdated(dataTypes: dataTypes);
}
Future<String> databaseFilePath() {
return _currentService.databaseFilePath();
}
Future<Uint8List> readDatabaseFileBytes() {
return _currentService.readDatabaseFileBytes();
}
Future<String> readUploadApiLogText() {
return _currentService.readUploadApiLogText();
}
Future<void> clearLocalDatabaseAndUploadLog() {
return _currentService.clearLocalDatabaseAndUploadLog();
}
Future<void> clearUploadApiLog() {
return _currentService.clearUploadApiLog();
}
Future<bool> shareNativeAppleHealthObserverRecord() {
return _currentService.shareNativeAppleHealthObserverRecord();
}
Future<bool> shareFlutterObserverRecord() {
return _currentService.shareFlutterObserverRecord();
}
Future<bool> shareUploadTaskRecord() {
return _currentService.shareUploadTaskRecord();
}
Future<String> readLocalNotificationDebugLogText() {
return _currentService.readLocalNotificationDebugLogText();
}
Future<bool> shareLocalNotificationDebugRecord() {
return _currentService.shareLocalNotificationDebugRecord();
}
Future<HealthRawStressCalculationResult> startCoreCaculate({
int? endTime,
int readChunkDays = defaultReadChunkDays,
}) {
return _currentService.startCoreCaculate(
endTime: endTime,
readChunkDays: readChunkDays,
);
}
Future<HealthRawStressCalculationResult> syncAndStore({
int? startTime,
int? endTime,
int readChunkDays = defaultReadChunkDays,
}) {
return _currentService.syncAndStore(
startTime: startTime,
endTime: endTime,
readChunkDays: readChunkDays,
);
}
Stream<HealthKitRawDataPoint> streamRawData({
required int dataType,
required int startTime,
required int endTime,
int readChunkDays = defaultReadChunkDays,
}) {
return _currentService.streamRawData(
dataType: dataType,
startTime: startTime,
endTime: endTime,
readChunkDays: readChunkDays,
);
}
Stream<HealthKitRawDataPoint> streamRawSleepData({
required int startTime,
required int endTime,
int readChunkDays = defaultReadChunkDays,
}) {
return _currentService.streamRawSleepData(
startTime: startTime,
endTime: endTime,
readChunkDays: readChunkDays,
);
}
Stream<HealthKitRawWorkoutDataPoint> streamRawWorkoutData({
required int startTime,
required int endTime,
int readChunkDays = defaultReadChunkDays,
}) {
return _currentService.streamRawWorkoutData(
startTime: startTime,
endTime: endTime,
readChunkDays: readChunkDays,
);
}
Future<List<HealthRawHrvStressPoint>> queryHrvStressPoints({
required int startTime,
required int endTime,
}) {
return _currentService.queryHrvStressPoints(
startTime: startTime,
endTime: endTime,
);
}
Future<List<HealthRawRealtimeStressPoint>> queryRealtimeStressPoints({
required int startTime,
required int endTime,
}) {
return _currentService.queryRealtimeStressPoints(
startTime: startTime,
endTime: endTime,
);
}
Future<int?> queryEarliestHrRawEndTime() {
return _currentService.queryEarliestHrRawEndTime();
}
Future<List<HealthRawDailyStressPoint>> queryDailyStressPoints({
required int startDate,
required int endDate,
}) {
return _currentService.queryDailyStressPoints(
startDate: startDate,
endDate: endDate,
);
}
Future<List<HealthRawSleepResult>> querySleepResults({
required int startTime,
required int endTime,
}) {
return _currentService.querySleepResults(
startTime: startTime,
endTime: endTime,
);
}
Future<List<HealthKitRawDataPoint>> queryRawDataPoints({
required int dataType,
required int startTime,
required int endTime,
int readChunkDays = defaultReadChunkDays,
}) {
return _currentService.queryRawDataPoints(
dataType: dataType,
startTime: startTime,
endTime: endTime,
readChunkDays: readChunkDays,
);
}
Future<List<HealthKitRawDataPoint>> queryRawSleepIntervals({
required int startTime,
required int endTime,
int readChunkDays = defaultReadChunkDays,
}) {
return _currentService.queryRawSleepIntervals(
startTime: startTime,
endTime: endTime,
readChunkDays: readChunkDays,
);
}
Future<List<HealthKitRawActivityDataPoint>> queryRawActivitySummaries({
required int startTime,
required int endTime,
}) {
return _currentService.queryRawActivitySummaries(
startTime: startTime,
endTime: endTime,
);
}
Future<void> markHrvStressUploaded({
required Iterable<int> rawEndTimes,
}) {
return _currentService.markHrvStressUploaded(rawEndTimes: rawEndTimes);
}
Future<void> markRealtimeStressUploaded({
required Iterable<int> rawEndTimes,
}) {
return _currentService.markRealtimeStressUploaded(
rawEndTimes: rawEndTimes,
);
}
static HealthRawStressCalculationResult calculate({
required int userId,
required List<HealthKitRawDataPoint> hrvPoints,
required List<HealthKitRawDataPoint> heartRatePoints,
required List<HealthKitRawDataPoint> restingHeartRatePoints,
required List<HealthKitRawDataPoint> sleepIntervals,
List<HealthKitRawWorkoutDataPoint> workoutIntervals =
const <HealthKitRawWorkoutDataPoint>[],
required int startTime,
required int endTime,
}) {
return AppleHealthRawDataCoreService.calculate(
userId: userId,
hrvPoints: hrvPoints,
heartRatePoints: heartRatePoints,
restingHeartRatePoints: restingHeartRatePoints,
sleepIntervals: sleepIntervals,
workoutIntervals: workoutIntervals,
startTime: startTime,
endTime: endTime,
);
}
}
... ...
... ... @@ -17,16 +17,16 @@ import '../../../logging/app_logger.dart';
import '../../../network/api/health_api.dart';
import '../../../result/app_result.dart';
import '../health_raw_models.dart';
import 'health_raw_local_notification_debug_store.dart';
import 'health_raw_local_notification.dart';
import 'apple_health_raw_local_notification_debug_store.dart';
import 'apple_health_raw_local_notification.dart';
import '../health_raw_stress_calculator.dart';
import '../health_sleep_calculator.dart';
class HealthRawDataCoreService {
class AppleHealthRawDataCoreService {
static const int defaultLookbackDays = 183;
static const int defaultReadChunkDays = 7;
HealthRawDataCoreService({
AppleHealthRawDataCoreService({
HealthKitHostApi? healthApi,
HealthKitRawDataHostApi? rawDataApi,
HealthRawStressLocalStore? localStore,
... ... @@ -73,7 +73,7 @@ class HealthRawDataCoreService {
int get _userId {
final userId = _userIdProvider?.call() ?? 0;
if (userId <= 0) {
throw StateError('HealthRawDataCoreService requires a valid userId');
throw StateError('AppleHealthRawDataCoreService requires a valid userId');
}
return userId;
}
... ... @@ -90,6 +90,20 @@ class HealthRawDataCoreService {
return _rawDataApi.hasHealthData();
}
Future<bool> performHealthDataUpload() {
return _rawDataApi.performHealthDataUpload();
}
Future<bool> syncDatabaseToNativeIfNeeded() async {
final databasePath = await databaseFilePath();
return _rawDataApi.syncDatabase(
hrDataPath: databasePath,
hrvDataPath: databasePath,
sleepDataPath: databasePath,
avgRealtimeStressDataPath: databasePath,
);
}
Stream<HealthRawDataUpdatedEvent> get healthDataUpdatedStream =>
_healthDataUpdatedController.stream;
... ... @@ -148,7 +162,7 @@ class HealthRawDataCoreService {
final path = await _localStore.dbPath(userId);
final file = File(path);
if (!await file.exists()) {
throw StateError('HealthRawDataCoreService database file not found');
throw StateError('AppleHealthRawDataCoreService database file not found');
}
final bytes = await file.readAsBytes();
return bytes;
... ... @@ -1664,8 +1678,10 @@ class HealthRawStressLocalStore {
HealthRawStressLocalStore({
Directory? rootDirectory,
DatabaseFactory? databaseFactory,
String databaseNamePrefix = '',
}) : _rootDirectory = rootDirectory,
_databaseFactory = databaseFactory;
_databaseFactory = databaseFactory,
_databaseNamePrefix = databaseNamePrefix;
static const hrvResultsTable = 'hrv_results';
static const realtimeStressResultsTable = 'realtime_stress_results';
... ... @@ -1674,6 +1690,7 @@ class HealthRawStressLocalStore {
final Directory? _rootDirectory;
final DatabaseFactory? _databaseFactory;
final String _databaseNamePrefix;
final Map<int, Database> _opened = <int, Database>{};
Future<void> upsertResult(HealthRawStressCalculationResult result) async {
... ... @@ -1996,7 +2013,7 @@ class HealthRawStressLocalStore {
Future<String> dbPath(int userId) async {
final dir = _rootDirectory ?? await getApplicationDocumentsDirectory();
return '${dir.path}/hrv_result_$userId.sqlite';
return '${dir.path}/${_databaseNamePrefix}hrv_result_$userId.sqlite';
}
Future<void> openDatabase(int userId) async {
... ...
import 'dart:async';
import 'dart:developer' as developer;
import 'dart:io';
import 'dart:isolate';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import '../../../../data/models/enums/app_enums.dart';
import '../../../../pigeon/health_kit_raw_data_api.g.dart';
import '../../../config/app_environment_config.dart';
import '../../../logging/app_logger.dart';
import '../../../network/api/health_api.dart';
import '../health_raw_data_source.dart';
import '../health_raw_models.dart';
import '../health_raw_stress_calculator.dart';
import '../health_sleep_calculator.dart';
import '../platform_ios/apple_health_raw_data_core_service.dart';
class OHOSHealthRawDataCoreService {
static const int defaultLookbackDays = 183;
static const int defaultReadChunkDays = 7;
OHOSHealthRawDataCoreService({
HealthRawDataSource? rawDataSource,
HealthRawStressLocalStore? localStore,
AppEnvironmentConfig? environmentConfig,
int Function()? userIdProvider,
bool uploadResultsAfterCalculation = true,
HealthApi? serverHealthApi,
}) : _rawDataSource = rawDataSource ?? OhosHealthRawDataSource(),
_localStore = localStore ??
HealthRawStressLocalStore(databaseNamePrefix: 'ohos_'),
_environmentConfig = environmentConfig,
_userIdProvider = userIdProvider,
_uploadResultsAfterCalculation = uploadResultsAfterCalculation,
_serverHealthApi = serverHealthApi;
final HealthRawDataSource _rawDataSource;
final HealthRawStressLocalStore _localStore;
final AppEnvironmentConfig? _environmentConfig;
final int Function()? _userIdProvider;
final bool _uploadResultsAfterCalculation;
final HealthApi? _serverHealthApi;
final StreamController<HealthRawDataUpdatedEvent>
_healthDataUpdatedController =
StreamController<HealthRawDataUpdatedEvent>.broadcast();
Future<HealthRawStressCalculationResult>? _coreCalculation;
bool get _isDebug => _environmentConfig?.isDebug ?? false;
int get _userId {
final userId = _userIdProvider?.call() ?? 0;
if (userId <= 0) {
throw StateError('OHOSHealthRawDataCoreService requires a valid userId');
}
return userId;
}
Future<void> openDatabase() {
return _localStore.openDatabase(_userId);
}
Future<void> closeDatabase() {
return _localStore.closeDatabase(userId: _userId);
}
Future<bool> hasHealthData() {
return _rawDataSource.hasHealthData();
}
Future<bool> performHealthDataUpload() async {
// TODO: Trigger OHOS raw-data upload when the OHOS upload API is ready.
return false;
}
Future<bool> syncDatabaseToNativeIfNeeded() async {
// OHOS must not call the Apple Pigeon database sync path.
return false;
}
Stream<HealthRawDataUpdatedEvent> get healthDataUpdatedStream =>
_healthDataUpdatedController.stream;
Future<void> onHealthDataUpdated({List<int>? dataTypes}) async {
await startCoreCaculate();
_healthDataUpdatedController.add(
HealthRawDataUpdatedEvent(dataTypes: dataTypes ?? const <int>[]),
);
}
Future<String> databaseFilePath() {
return _localStore.dbPath(_userId);
}
Future<Uint8List> readDatabaseFileBytes() async {
final userId = _userId;
await _localStore.prepareDatabaseFileForShare(userId);
final path = await _localStore.dbPath(userId);
final file = File(path);
if (!await file.exists()) {
throw StateError('OHOSHealthRawDataCoreService database file not found');
}
return file.readAsBytes();
}
Future<String> readUploadApiLogText() async {
return _isDebug ? 'OHOS upload API log is not implemented yet.' : '';
}
Future<void> clearLocalDatabaseAndUploadLog() {
return _localStore.clearTables(_userId);
}
Future<void> clearUploadApiLog() async {
// TODO: Clear OHOS upload log after the OHOS upload/debug store exists.
}
Future<bool> shareNativeAppleHealthObserverRecord() async {
return false;
}
Future<bool> shareFlutterObserverRecord() async {
return false;
}
Future<bool> shareUploadTaskRecord() async {
return false;
}
Future<String> readLocalNotificationDebugLogText() async {
return _isDebug
? 'OHOS notification debug log is not implemented yet.'
: '';
}
Future<bool> shareLocalNotificationDebugRecord() async {
return false;
}
Future<HealthRawStressCalculationResult> startCoreCaculate({
int? endTime,
int readChunkDays = defaultReadChunkDays,
}) async {
final running = _coreCalculation;
if (running != null) return running;
final task = _readCalculateAndStore(
endTime: endTime,
readChunkDays: readChunkDays,
forceStartTime: null,
);
_coreCalculation = task;
try {
return await task;
} catch (error, stackTrace) {
_logError('startCoreCaculate failed', error, stackTrace);
rethrow;
} finally {
if (identical(_coreCalculation, task)) {
_coreCalculation = null;
}
}
}
Future<HealthRawStressCalculationResult> syncAndStore({
int? startTime,
int? endTime,
int readChunkDays = defaultReadChunkDays,
}) async {
try {
return await _readCalculateAndStore(
endTime: endTime,
readChunkDays: readChunkDays,
forceStartTime: startTime,
);
} catch (error, stackTrace) {
_logError('syncAndStore failed', error, stackTrace);
rethrow;
}
}
Future<HealthRawStressCalculationResult> _readCalculateAndStore({
required int? endTime,
required int readChunkDays,
required int? forceStartTime,
}) async {
if (readChunkDays <= 0) {
throw ArgumentError.value(readChunkDays, 'readChunkDays');
}
final userId = _userId;
await _localStore.ensureReadable(userId);
final effectiveEndTime =
endTime ?? DateTime.now().millisecondsSinceEpoch ~/ 1000;
final earliestStartTime = DateTime.now()
.subtract(const Duration(days: defaultLookbackDays))
.millisecondsSinceEpoch ~/
1000;
final requestedStartTime =
math.max(forceStartTime ?? earliestStartTime, earliestStartTime);
if (effectiveEndTime < requestedStartTime) {
throw ArgumentError.value(endTime, 'endTime');
}
final hrvContextStart = await _localStore.latestHrvSourceStartTime(userId);
final realtimeContextStart =
await _localStore.latestRealtimeSourceStartTime(userId);
final latestHrvRawEndTime = await _localStore.latestHrvRawEndTime(userId);
final latestRealtimeRawEndTime =
await _localStore.latestRealtimeRawEndTime(userId);
final latestSleepResultTime = await _localStore.latestSleepResultTime(
userId,
);
final hrvStartTime = math.max(
hrvContextStart ?? requestedStartTime,
earliestStartTime,
);
final realtimeStartTime = math.max(
realtimeContextStart ?? requestedStartTime,
earliestStartTime,
);
final heartRateStartTime = math.max(
_minNullable(hrvStartTime, realtimeStartTime) ?? requestedStartTime,
earliestStartTime,
);
final sleepStartTime = math.max(
latestSleepResultTime == null
? requestedStartTime
: latestSleepResultTime - Duration.secondsPerDay,
earliestStartTime,
);
final hrvPoints = await _fetchRawDataInChunks(
HealthDataUploadType.hrv.type,
hrvStartTime,
effectiveEndTime,
readChunkDays: readChunkDays,
);
final heartRatePoints = await _fetchRawDataInChunks(
HealthDataUploadType.heartRate.type,
heartRateStartTime,
effectiveEndTime,
readChunkDays: readChunkDays,
);
final restingHeartRatePoints = await _fetchRawDataInChunks(
HealthDataUploadType.restingHeartRate.type,
heartRateStartTime,
effectiveEndTime,
readChunkDays: readChunkDays,
);
final sleepIntervals = await _fetchSleepIntervalsInChunks(
sleepStartTime,
effectiveEndTime,
readChunkDays: readChunkDays,
);
final workoutIntervals = await _fetchWorkoutIntervalsInChunks(
heartRateStartTime,
effectiveEndTime,
readChunkDays: readChunkDays,
);
final result = await Isolate.run(
() => HealthRawStressCalculator(userId: userId).calculate(
hrvPoints: hrvPoints,
heartRatePoints: heartRatePoints,
restingHeartRatePoints: restingHeartRatePoints,
sleepIntervals: sleepIntervals,
workoutIntervals: workoutIntervals,
startTime: math.min(hrvStartTime, heartRateStartTime),
endTime: effectiveEndTime,
),
debugName: 'OHOSHealthRawStressCalculator',
);
final newResult = result.copyWith(
hrvStressPoints: _filterNewHrvStressPoints(
result.hrvStressPoints,
latestHrvRawEndTime,
),
realtimeStressPoints: _filterNewRealtimeStressPoints(
result.realtimeStressPoints,
latestRealtimeRawEndTime,
),
);
await _localStore.upsertResult(newResult);
final dailyStressPoints = await _calculateAndStoreDailyStressPoints(
userId: userId,
realtimePoints: newResult.realtimeStressPoints,
nowSeconds: effectiveEndTime,
);
final sleepResults = await _calculateAndStoreSleepResults(
userId: userId,
sleepIntervals: sleepIntervals,
latestSleepResultTime: latestSleepResultTime,
);
if (_uploadResultsAfterCalculation) {
await _uploadResults();
}
return newResult.copyWith(
dailyStressPoints: dailyStressPoints,
sleepResults: sleepResults,
);
}
Stream<HealthKitRawDataPoint> streamRawData({
required int dataType,
required int startTime,
required int endTime,
int readChunkDays = defaultReadChunkDays,
}) async* {
final chunkSeconds = readChunkDays * Duration.secondsPerDay;
var cursor = startTime;
while (cursor <= endTime) {
final chunkEnd = math.min(cursor + chunkSeconds - 1, endTime);
final points = await _rawDataSource.getRawData(
dataType,
cursor,
chunkEnd,
);
for (final point in points) {
yield point;
}
cursor = chunkEnd + 1;
}
}
Stream<HealthKitRawDataPoint> streamRawSleepData({
required int startTime,
required int endTime,
int readChunkDays = defaultReadChunkDays,
}) async* {
final chunkSeconds = readChunkDays * Duration.secondsPerDay;
var cursor = startTime;
while (cursor <= endTime) {
final chunkEnd = math.min(cursor + chunkSeconds - 1, endTime);
final groups = await _rawDataSource.getRawSleepData(cursor, chunkEnd);
final points = groups
.expand((group) => group.sleepDataPoints)
.where(
(point) => point.endTime >= cursor && point.startTime <= chunkEnd)
.toList();
for (final point in points) {
yield point;
}
cursor = chunkEnd + 1;
}
}
Stream<HealthKitRawWorkoutDataPoint> streamRawWorkoutData({
required int startTime,
required int endTime,
int readChunkDays = defaultReadChunkDays,
}) async* {
final chunkSeconds = readChunkDays * Duration.secondsPerDay;
var cursor = startTime;
while (cursor <= endTime) {
final chunkEnd = math.min(cursor + chunkSeconds - 1, endTime);
final points = await _rawDataSource.getRawWorkoutData(cursor, chunkEnd);
for (final point in points.where(
(point) => point.endTime >= cursor && point.startTime <= chunkEnd)) {
yield point;
}
cursor = chunkEnd + 1;
}
}
Future<List<HealthRawHrvStressPoint>> queryHrvStressPoints({
required int startTime,
required int endTime,
}) {
return _localStore.queryHrvStressPoints(
userId: _userId,
startTime: startTime,
endTime: endTime,
);
}
Future<List<HealthRawRealtimeStressPoint>> queryRealtimeStressPoints({
required int startTime,
required int endTime,
}) {
return _localStore.queryRealtimeStressPoints(
userId: _userId,
startTime: startTime,
endTime: endTime,
);
}
Future<int?> queryEarliestHrRawEndTime() {
return _localStore.earliestRealtimeRawEndTime(_userId);
}
Future<List<HealthRawDailyStressPoint>> queryDailyStressPoints({
required int startDate,
required int endDate,
}) {
return _localStore.queryDailyStressPoints(
userId: _userId,
startDate: startDate,
endDate: endDate,
);
}
Future<List<HealthRawSleepResult>> querySleepResults({
required int startTime,
required int endTime,
}) {
return _localStore.querySleepResults(
userId: _userId,
startTime: startTime,
endTime: endTime,
);
}
Future<List<HealthKitRawDataPoint>> queryRawDataPoints({
required int dataType,
required int startTime,
required int endTime,
int readChunkDays = defaultReadChunkDays,
}) {
return _fetchRawDataInChunks(
dataType,
startTime,
endTime,
readChunkDays: readChunkDays,
);
}
Future<List<HealthKitRawDataPoint>> queryRawSleepIntervals({
required int startTime,
required int endTime,
int readChunkDays = defaultReadChunkDays,
}) {
return _fetchSleepIntervalsInChunks(
startTime,
endTime,
readChunkDays: readChunkDays,
);
}
Future<List<HealthKitRawActivityDataPoint>> queryRawActivitySummaries({
required int startTime,
required int endTime,
}) async {
final points = await _rawDataSource.getRawActivityData(startTime, endTime);
return points
.where((e) => e.endTime >= startTime && e.endTime <= endTime)
.toList()
..sort((a, b) => a.endTime.compareTo(b.endTime));
}
Future<void> markHrvStressUploaded({
required Iterable<int> rawEndTimes,
}) {
return _localStore.markHrvStressUploaded(
userId: _userId,
rawEndTimes: rawEndTimes,
);
}
Future<void> markRealtimeStressUploaded({
required Iterable<int> rawEndTimes,
}) {
return _localStore.markRealtimeStressUploaded(
userId: _userId,
rawEndTimes: rawEndTimes,
);
}
Future<void> _uploadResults() async {
// TODO: Upload OHOS result rows through OHOS/backend APIs and mark uploaded
// rows in _localStore after the contract is available.
if (_serverHealthApi == null) return;
}
Future<List<HealthRawDailyStressPoint>> _calculateAndStoreDailyStressPoints({
required int userId,
required List<HealthRawRealtimeStressPoint> realtimePoints,
required int nowSeconds,
}) async {
final todayDate = _dateKeyFromUnixSeconds(nowSeconds);
final affectedDates = <int>{
todayDate,
for (final point in realtimePoints)
_dateKeyFromUnixSeconds(point.rawEndTime),
}.toList()
..sort();
final existingDates = await _localStore.existingDailyStressDates(
userId: userId,
dates: affectedDates,
);
final dailyStressPoints = <HealthRawDailyStressPoint>[];
final emptyDates = <int>[];
for (final date in affectedDates) {
if (date != todayDate && existingDates.contains(date)) continue;
final (startTime, endTime) = _dayRangeFromDateKey(date);
final dayRealtimePoints = await _localStore.queryRealtimeStressPoints(
userId: userId,
startTime: startTime,
endTime: endTime,
);
final point = HealthRawDailyStressCalculator.calculate(
userId: userId,
date: date,
realtimePoints: dayRealtimePoints,
dataTime: date == todayDate ? nowSeconds : startTime,
);
if (point == null) {
emptyDates.add(date);
continue;
}
dailyStressPoints.add(point);
}
await _localStore.upsertDailyStressPoints(
userId: userId,
points: dailyStressPoints,
);
await _localStore.deleteDailyStressDates(
userId: userId,
dates: emptyDates,
);
return dailyStressPoints;
}
Future<List<HealthRawSleepResult>> _calculateAndStoreSleepResults({
required int userId,
required List<HealthKitRawDataPoint> sleepIntervals,
required int? latestSleepResultTime,
}) async {
if (sleepIntervals.isEmpty) return const <HealthRawSleepResult>[];
final days = {
for (final interval in sleepIntervals)
DateTime.fromMillisecondsSinceEpoch(interval.endTime * 1000)
}.map((date) => DateTime(date.year, date.month, date.day)).toList()
..sort((a, b) => a.compareTo(b));
final results = <HealthRawSleepResult>[];
for (final day in days) {
final calculation = HealthSleepCalculator.calculateDay(
day: day,
sleepIntervals: sleepIntervals,
);
final merged = calculation.mergeSleepTimeRange;
final score = calculation.score;
final state = calculation.state;
if (merged == null || score == null || state == null) continue;
if (!calculation.hasValidSleep) continue;
if (latestSleepResultTime != null &&
merged.endTime <= latestSleepResultTime) {
continue;
}
results.add(
HealthRawSleepResult(
userId: userId,
date: merged.endTime,
startDate: merged.startTime,
sleepScore: score,
sleepState: state.value,
inBedMinutes: calculation.summary.timeInBedMinutes,
awakMinutes: calculation.summary.awakeMinutes,
sleepMinutes: calculation.summary.sleepMinutes,
uploaded: false,
),
);
}
await _localStore.upsertSleepResults(userId: userId, results: results);
return results;
}
List<HealthRawHrvStressPoint> _filterNewHrvStressPoints(
List<HealthRawHrvStressPoint> points,
int? latestRawEndTime,
) {
if (latestRawEndTime == null) return points;
return points
.where((point) => point.rawEndTime > latestRawEndTime)
.toList();
}
List<HealthRawRealtimeStressPoint> _filterNewRealtimeStressPoints(
List<HealthRawRealtimeStressPoint> points,
int? latestRawEndTime,
) {
if (latestRawEndTime == null) return points;
return points
.where((point) => point.rawEndTime > latestRawEndTime)
.toList();
}
Future<List<HealthKitRawDataPoint>> _fetchRawDataInChunks(
int dataType,
int startTime,
int endTime, {
required int readChunkDays,
}) async {
final points = <HealthKitRawDataPoint>[];
await for (final point in streamRawData(
dataType: dataType,
startTime: startTime,
endTime: endTime,
readChunkDays: readChunkDays,
)) {
points.add(point);
}
points.sort((a, b) => a.endTime.compareTo(b.endTime));
return points;
}
Future<List<HealthKitRawDataPoint>> _fetchSleepIntervalsInChunks(
int startTime,
int endTime, {
required int readChunkDays,
}) async {
final points = <HealthKitRawDataPoint>[];
await for (final point in streamRawSleepData(
startTime: startTime,
endTime: endTime,
readChunkDays: readChunkDays,
)) {
points.add(point);
}
points.sort((a, b) => a.endTime.compareTo(b.endTime));
return points;
}
Future<List<HealthKitRawWorkoutDataPoint>> _fetchWorkoutIntervalsInChunks(
int startTime,
int endTime, {
required int readChunkDays,
}) async {
final points = <HealthKitRawWorkoutDataPoint>[];
await for (final point in streamRawWorkoutData(
startTime: startTime,
endTime: endTime,
readChunkDays: readChunkDays,
)) {
points.add(point);
}
points.sort((a, b) => a.endTime.compareTo(b.endTime));
return points;
}
static int? _minNullable(int? a, int? b) {
if (a == null) return b;
if (b == null) return a;
return math.min(a, b);
}
static int _dateKeyFromUnixSeconds(int seconds) {
final date = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
return date.year * 10000 + date.month * 100 + date.day;
}
static (int startTime, int endTime) _dayRangeFromDateKey(int dateKey) {
final year = dateKey ~/ 10000;
final month = (dateKey ~/ 100) % 100;
final day = dateKey % 100;
final start = DateTime(year, month, day);
return (
start.millisecondsSinceEpoch ~/ 1000,
start.add(const Duration(days: 1)).millisecondsSinceEpoch ~/ 1000 - 1,
);
}
static void _logError(
String message,
Object error,
StackTrace stackTrace,
) {
final tagged = 'OHOSHealthRawDataCoreService $message: $error';
developer.log(
tagged,
name: 'OHOSHealthRawDataCoreService',
error: error,
stackTrace: stackTrace,
);
debugPrint(tagged);
try {
AppLogger.e(tagged, error, stackTrace);
} catch (_) {
// Logger may be unavailable in isolated unit tests.
}
}
}
... ...
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import '../logging/app_logger.dart';
... ...
import 'package:doublefeel_flutter/core/error/app_error.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart';
import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
import 'package:doublefeel_flutter/data/models/health/activity/activity_burn_statistics_data_v2.dart';
... ...
import 'dart:convert';
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
... ... @@ -73,14 +72,7 @@ class UserPreferencesStorage {
Future<void> _syncHealthRawDatabaseToNative() async {
if (!Get.isRegistered<HealthRawDataCoreService>()) return;
final databasePath =
await Get.find<HealthRawDataCoreService>().databaseFilePath();
await HealthKitRawDataHostApi().syncDatabase(
hrDataPath: databasePath,
hrvDataPath: databasePath,
sleepDataPath: databasePath,
avgRealtimeStressDataPath: databasePath,
);
await Get.find<HealthRawDataCoreService>().syncDatabaseToNativeIfNeeded();
}
Future<void> _persist(UserPreferences value) async {
... ...
... ... @@ -8,7 +8,7 @@ import 'package:intl/date_symbol_data_local.dart';
import 'app/bootstrap/app_bootstrap.dart';
import 'app/double_feel_app.dart';
import 'core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart';
import 'core/services/raw_data_service/health_raw_data_core_service.dart';
import 'core/theme/app_theme.dart';
import 'data/local/user_preferences_storage.dart';
... ...
... ... @@ -117,6 +117,7 @@ abstract class PlatformHostApi {
String getFullUserAgent();
/// 是否是中国大陆地区
@async
bool isChinaRegion();
/// 申请通知权限
... ...
import 'dart:io';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/apple_health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ohos/ohos_health_raw_data_core_service.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('facade routes iOS calls to Apple implementation', () async {
final service = HealthRawDataCoreService(
appleService: _FakeAppleHealthRawDataCoreService(label: 'apple'),
ohosService: _FakeOhosHealthRawDataCoreService(label: 'ohos'),
targetPlatform: TargetPlatform.iOS,
);
expect(await service.databaseFilePath(), 'apple');
expect(await service.performHealthDataUpload(), isTrue);
});
test('facade routes non-iOS calls to OHOS implementation', () async {
final service = HealthRawDataCoreService(
appleService: _FakeAppleHealthRawDataCoreService(label: 'apple'),
ohosService: _FakeOhosHealthRawDataCoreService(label: 'ohos'),
targetPlatform: TargetPlatform.android,
);
expect(await service.databaseFilePath(), 'ohos');
expect(await service.performHealthDataUpload(), isFalse);
});
test('OHOS result database is isolated from iOS result database', () async {
final tempDir = await Directory.systemTemp.createTemp('health_raw_test_');
addTearDown(() => tempDir.delete(recursive: true));
const userId = 42;
final apple = AppleHealthRawDataCoreService(
localStore: HealthRawStressLocalStore(rootDirectory: tempDir),
userIdProvider: () => userId,
uploadResultsAfterCalculation: false,
);
final ohos = OHOSHealthRawDataCoreService(
localStore: HealthRawStressLocalStore(
rootDirectory: tempDir,
databaseNamePrefix: 'ohos_',
),
userIdProvider: () => userId,
uploadResultsAfterCalculation: false,
);
expect(
await apple.databaseFilePath(),
'${tempDir.path}/hrv_result_$userId.sqlite',
);
expect(
await ohos.databaseFilePath(),
'${tempDir.path}/ohos_hrv_result_$userId.sqlite',
);
});
}
class _FakeAppleHealthRawDataCoreService extends AppleHealthRawDataCoreService {
_FakeAppleHealthRawDataCoreService({required this.label})
: super(
userIdProvider: () => 1,
uploadResultsAfterCalculation: false,
);
final String label;
@override
Future<String> databaseFilePath() async {
return label;
}
@override
Future<bool> performHealthDataUpload() async {
return true;
}
}
class _FakeOhosHealthRawDataCoreService extends OHOSHealthRawDataCoreService {
_FakeOhosHealthRawDataCoreService({required this.label})
: super(
userIdProvider: () => 1,
uploadResultsAfterCalculation: false,
);
final String label;
@override
Future<String> databaseFilePath() async {
return label;
}
@override
Future<bool> performHealthDataUpload() async {
return false;
}
}
... ...
import 'dart:async';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_local_notification.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/apple_health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/apple_health_raw_local_notification.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_local_data_convert.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart';
import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
import 'package:doublefeel_flutter/pigeon/health_kit_api.g.dart';
import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
Future<void> _pumpAsyncUploads() async {
... ... @@ -50,7 +52,7 @@ void main() {
]);
final store = _MemoryHealthRawStressLocalStore();
final service = HealthRawDataCoreService(
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
... ... @@ -185,7 +187,7 @@ void main() {
uploaded: true,
),
);
final service = HealthRawDataCoreService(
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
... ... @@ -331,7 +333,7 @@ void main() {
uploaded: true,
),
);
final service = HealthRawDataCoreService(
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
... ... @@ -392,7 +394,7 @@ void main() {
test('startCoreCaculate skips raw reads when health auth is missing',
() async {
final api = _FakeHealthKitRawDataHostApi();
final service = HealthRawDataCoreService(
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(status: 2),
rawDataApi: api,
localStore: _MemoryHealthRawStressLocalStore(),
... ... @@ -417,7 +419,7 @@ void main() {
_point(now - 120, 35),
]);
final service = HealthRawDataCoreService(
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: _MemoryHealthRawStressLocalStore(),
... ... @@ -454,7 +456,7 @@ void main() {
),
]);
final store = _MemoryHealthRawStressLocalStore();
final service = HealthRawDataCoreService(
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
... ... @@ -508,7 +510,7 @@ void main() {
),
]);
final store = _MemoryHealthRawStressLocalStore();
final service = HealthRawDataCoreService(
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
... ... @@ -587,7 +589,7 @@ void main() {
],
);
final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher();
final service = HealthRawDataCoreService(
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
... ... @@ -644,7 +646,7 @@ void main() {
],
);
final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher();
final service = HealthRawDataCoreService(
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
... ... @@ -699,7 +701,7 @@ void main() {
),
]);
final notificationDispatcher = _FakeHealthRawLocalNotificationDispatcher();
final service = HealthRawDataCoreService(
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: _MemoryHealthRawStressLocalStore(),
... ... @@ -768,7 +770,7 @@ void main() {
),
],
);
final service = HealthRawDataCoreService(
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
... ... @@ -809,7 +811,7 @@ void main() {
dailyStressPoints: const <HealthRawDailyStressPoint>[],
),
);
final service = HealthRawDataCoreService(
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
... ... @@ -845,7 +847,7 @@ void main() {
const base = 1800000000;
final api = _FakeHealthKitRawDataHostApi();
final store = _MemoryHealthRawStressLocalStore();
final service = HealthRawDataCoreService(
final service = AppleHealthRawDataCoreService(
healthApi: _FakeHealthKitHostApi(),
rawDataApi: api,
localStore: store,
... ... @@ -864,7 +866,11 @@ void main() {
),
);
final result = await LocalHealthDataSource(coreService: service)
final facade = HealthRawDataCoreService(
appleService: service,
targetPlatform: TargetPlatform.iOS,
);
final result = await LocalHealthDataSource(coreService: facade)
.getHealthDataEverUploaded();
expect(result, isA<AppSuccess>());
... ...
import 'dart:io';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/health_raw_local_notification.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ios/apple_health_raw_local_notification.dart';
import 'package:doublefeel_flutter/l10n/gen/app_localizations_zh.dart';
import 'package:flutter_test/flutter_test.dart';
... ...