Commit fbaf35ccc8c34c5eeb73732f1f2e27f98493b0f5

Authored by 权海
1 parent f64340df

feat(ui):ohos原始数据获取

@@ -12,6 +12,9 @@ import '../../../../core/logging/app_logger.dart'; @@ -12,6 +12,9 @@ import '../../../../core/logging/app_logger.dart';
12 import '../../../../core/services/raw_data_service/health_raw_data_core_service.dart'; 12 import '../../../../core/services/raw_data_service/health_raw_data_core_service.dart';
13 import '../../../../core/services/raw_data_service/health_raw_models.dart'; 13 import '../../../../core/services/raw_data_service/health_raw_models.dart';
14 import '../../../../core/services/raw_data_service/platform_ios/apple_health_raw_local_notification.dart'; 14 import '../../../../core/services/raw_data_service/platform_ios/apple_health_raw_local_notification.dart';
  15 +import '../../../../core/services/raw_data_service/platform_ohos/ohos_harmony_health_raw_data_sync_service_factory.dart';
  16 +import '../../../../core/services/raw_data_service/platform_ohos/ohos_health_raw_data_sync_service.dart';
  17 +import '../../../../data/models/enums/app_enums.dart';
15 import '../../../../data/local/user_preferences_storage.dart'; 18 import '../../../../data/local/user_preferences_storage.dart';
16 import '../../../../l10n/l10n_extensions.dart'; 19 import '../../../../l10n/l10n_extensions.dart';
17 import '../../../../core/platform/pigeon_api_facade.dart'; 20 import '../../../../core/platform/pigeon_api_facade.dart';
@@ -35,6 +38,7 @@ enum DeveloperOptionsAction { @@ -35,6 +38,7 @@ enum DeveloperOptionsAction {
35 shareFlutterObserverRecord, 38 shareFlutterObserverRecord,
36 shareLocalNotificationDebugRecord, 39 shareLocalNotificationDebugRecord,
37 sendTestHrvLocalNotification, 40 sendTestHrvLocalNotification,
  41 + pullOhosRawData,
38 clearHealthRawDataDatabaseAndUploadLog, 42 clearHealthRawDataDatabaseAndUploadLog,
39 globalEnv, 43 globalEnv,
40 } 44 }
@@ -92,6 +96,10 @@ class DeveloperOptionsController extends GetxController { @@ -92,6 +96,10 @@ class DeveloperOptionsController extends GetxController {
92 action: DeveloperOptionsAction.sendTestHrvLocalNotification, 96 action: DeveloperOptionsAction.sendTestHrvLocalNotification,
93 ), 97 ),
94 DeveloperOptionsItem( 98 DeveloperOptionsItem(
  99 + title: '主动拉取 OHOS 原始数据',
  100 + action: DeveloperOptionsAction.pullOhosRawData,
  101 + ),
  102 + DeveloperOptionsItem(
95 title: '清除本地数据库、上传日志', 103 title: '清除本地数据库、上传日志',
96 action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog, 104 action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog,
97 ), 105 ),
@@ -123,6 +131,10 @@ class DeveloperOptionsController extends GetxController { @@ -123,6 +131,10 @@ class DeveloperOptionsController extends GetxController {
123 action: DeveloperOptionsAction.shareLocalNotificationDebugRecord, 131 action: DeveloperOptionsAction.shareLocalNotificationDebugRecord,
124 ), 132 ),
125 DeveloperOptionsItem( 133 DeveloperOptionsItem(
  134 + title: '主动拉取 OHOS 原始数据',
  135 + action: DeveloperOptionsAction.pullOhosRawData,
  136 + ),
  137 + DeveloperOptionsItem(
126 title: '清除本地数据库、上传日志', 138 title: '清除本地数据库、上传日志',
127 action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog, 139 action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog,
128 ), 140 ),
@@ -153,6 +165,9 @@ class DeveloperOptionsController extends GetxController { @@ -153,6 +165,9 @@ class DeveloperOptionsController extends GetxController {
153 case DeveloperOptionsAction.sendTestHrvLocalNotification: 165 case DeveloperOptionsAction.sendTestHrvLocalNotification:
154 await sendTestHrvLocalNotification(); 166 await sendTestHrvLocalNotification();
155 break; 167 break;
  168 + case DeveloperOptionsAction.pullOhosRawData:
  169 + await pullOhosRawData();
  170 + break;
156 case DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog: 171 case DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog:
157 await clearHealthRawDataDatabaseAndUploadLog(); 172 await clearHealthRawDataDatabaseAndUploadLog();
158 break; 173 break;
@@ -331,6 +346,64 @@ class DeveloperOptionsController extends GetxController { @@ -331,6 +346,64 @@ class DeveloperOptionsController extends GetxController {
331 } 346 }
332 } 347 }
333 348
  349 + Future<void> pullOhosRawData() async {
  350 + try {
  351 + final userId =
  352 + _userPreferencesStorage.preferences.value.meUserInfo?.id ?? 0;
  353 + if (userId <= 0) {
  354 + Get.snackbar('OHOS 拉取失败', '当前用户 id 无效');
  355 + return;
  356 + }
  357 + final syncService = createDefaultOhosHealthRawDataSyncService(
  358 + userIdProvider: () => userId,
  359 + );
  360 + final now = DateTime.now();
  361 + final endTime = now.millisecondsSinceEpoch ~/ 1000;
  362 + final startTime =
  363 + now.subtract(const Duration(days: 7)).millisecondsSinceEpoch ~/ 1000;
  364 + final dataTypes = <int>[
  365 + HealthDataUploadType.hrv.type,
  366 + HealthDataUploadType.heartRate.type,
  367 + HealthDataUploadType.restingHeartRate.type,
  368 + OhosHealthRawDataType.sleepAnalysis,
  369 + ];
  370 +
  371 + await LoadingService.instance.run(() async {
  372 + final results = <OhosHealthRawDataSyncResult>[];
  373 + for (final dataType in dataTypes) {
  374 + AppLogger.i(
  375 + '${OhosHealthRawDataSyncService.logMarker} '
  376 + 'developer_options_pull_start dataType=$dataType '
  377 + 'startTime=$startTime endTime=$endTime',
  378 + );
  379 + results.add(
  380 + await syncService.syncRawData(
  381 + dataType: dataType,
  382 + startTime: startTime,
  383 + endTime: endTime,
  384 + ),
  385 + );
  386 + }
  387 +
  388 + final storedCount = results.fold<int>(
  389 + 0,
  390 + (sum, result) => sum + result.storedCount,
  391 + );
  392 + final pageCount = results.fold<int>(
  393 + 0,
  394 + (sum, result) => sum + result.pageCount,
  395 + );
  396 + Get.snackbar(
  397 + 'OHOS 原始数据拉取完成',
  398 + '类型 ${results.length} 个,页数 $pageCount,写入 $storedCount 条',
  399 + );
  400 + });
  401 + } catch (error, stackTrace) {
  402 + AppLogger.e('Pull OHOS raw data failed', error, stackTrace);
  403 + Get.snackbar('OHOS 拉取失败', error.toString());
  404 + }
  405 + }
  406 +
334 Future<void> clearHealthRawDataDatabaseAndUploadLog() async { 407 Future<void> clearHealthRawDataDatabaseAndUploadLog() async {
335 try { 408 try {
336 await _healthRawDataCoreService.clearLocalDatabaseAndUploadLog(); 409 await _healthRawDataCoreService.clearLocalDatabaseAndUploadLog();
@@ -47,7 +47,7 @@ class HarmonyApi { @@ -47,7 +47,7 @@ class HarmonyApi {
47 return safeCall( 47 return safeCall(
48 call: () async { 48 call: () async {
49 final queryParameters = <String, dynamic>{ 49 final queryParameters = <String, dynamic>{
50 - 'date_type': dataType.dataType, 50 + 'data_type': dataType.dataType,
51 'start_date': startDate, 51 'start_date': startDate,
52 'end_date': endDate, 52 'end_date': endDate,
53 }; 53 };
@@ -137,8 +137,11 @@ class AppleHealthRawDataSource implements HealthRawDataSource { @@ -137,8 +137,11 @@ class AppleHealthRawDataSource implements HealthRawDataSource {
137 class OhosHealthRawDataSource implements HealthRawDataSource { 137 class OhosHealthRawDataSource implements HealthRawDataSource {
138 OhosHealthRawDataSource({ 138 OhosHealthRawDataSource({
139 OhosHealthRawDataSyncService? syncService, 139 OhosHealthRawDataSyncService? syncService,
140 - }) : _syncService =  
141 - syncService ?? createDefaultOhosHealthRawDataSyncService(); 140 + int Function()? userIdProvider,
  141 + }) : _syncService = syncService ??
  142 + createDefaultOhosHealthRawDataSyncService(
  143 + userIdProvider: userIdProvider,
  144 + );
142 145
143 final OhosHealthRawDataSyncService _syncService; 146 final OhosHealthRawDataSyncService _syncService;
144 147
@@ -5,13 +5,20 @@ import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart'; @@ -5,13 +5,20 @@ import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart';
5 import 'package:get/get.dart'; 5 import 'package:get/get.dart';
6 6
7 import 'huawei_health_data_type.dart'; 7 import 'huawei_health_data_type.dart';
  8 +import 'ohos_health_raw_data_local_store.dart';
8 import 'ohos_health_raw_data_sync_service.dart'; 9 import 'ohos_health_raw_data_sync_service.dart';
9 10
10 -OhosHealthRawDataSyncService createDefaultOhosHealthRawDataSyncService() { 11 +OhosHealthRawDataSyncService createDefaultOhosHealthRawDataSyncService({
  12 + int Function()? userIdProvider,
  13 +}) {
  14 + final localStore = OhosHealthRawDataSqliteStore(
  15 + userIdProvider: userIdProvider,
  16 + );
11 if (!Get.isRegistered<HarmonyApi>()) { 17 if (!Get.isRegistered<HarmonyApi>()) {
12 - return OhosHealthRawDataSyncService(); 18 + return OhosHealthRawDataSyncService(localStore: localStore);
13 } 19 }
14 return OhosHealthRawDataSyncService( 20 return OhosHealthRawDataSyncService(
  21 + localStore: localStore,
15 remoteDataSource: OhosHarmonyHealthRawDataRemoteDataSource( 22 remoteDataSource: OhosHarmonyHealthRawDataRemoteDataSource(
16 HarmonyApiOhosRawDataClient(Get.find<HarmonyApi>()), 23 HarmonyApiOhosRawDataClient(Get.find<HarmonyApi>()),
17 ), 24 ),
@@ -28,7 +28,10 @@ class OHOSHealthRawDataCoreService { @@ -28,7 +28,10 @@ class OHOSHealthRawDataCoreService {
28 int Function()? userIdProvider, 28 int Function()? userIdProvider,
29 bool uploadResultsAfterCalculation = true, 29 bool uploadResultsAfterCalculation = true,
30 HealthApi? serverHealthApi, 30 HealthApi? serverHealthApi,
31 - }) : _rawDataSource = rawDataSource ?? OhosHealthRawDataSource(), 31 + }) : _rawDataSource = rawDataSource ??
  32 + OhosHealthRawDataSource(
  33 + userIdProvider: userIdProvider,
  34 + ),
32 _localStore = localStore ?? 35 _localStore = localStore ??
33 HealthRawStressLocalStore(databaseNamePrefix: 'ohos_'), 36 HealthRawStressLocalStore(databaseNamePrefix: 'ohos_'),
34 _environmentConfig = environmentConfig, 37 _environmentConfig = environmentConfig,
  1 +import 'dart:convert';
  2 +import 'dart:io';
  3 +
  4 +import 'package:path_provider/path_provider.dart';
  5 +import 'package:sqflite/sqflite.dart';
  6 +
  7 +import 'huawei_health_data_type.dart';
  8 +import 'ohos_health_raw_data_sync_service.dart';
  9 +
  10 +class OhosHealthRawDataSqliteStore implements OhosHealthRawDataLocalStore {
  11 + OhosHealthRawDataSqliteStore({
  12 + required int Function()? userIdProvider,
  13 + Directory? rootDirectory,
  14 + DatabaseFactory? databaseFactory,
  15 + DateTime Function()? nowProvider,
  16 + }) : _userIdProvider = userIdProvider,
  17 + _rootDirectory = rootDirectory,
  18 + _databaseFactory = databaseFactory,
  19 + _nowProvider = nowProvider ?? DateTime.now;
  20 +
  21 + static const rawDataTable = 'ohos_raw_data';
  22 + static const sleepDataTable = 'ohos_sleep_data';
  23 +
  24 + final int Function()? _userIdProvider;
  25 + final Directory? _rootDirectory;
  26 + final DatabaseFactory? _databaseFactory;
  27 + final DateTime Function() _nowProvider;
  28 + final Map<int, Database> _opened = <int, Database>{};
  29 +
  30 + int get _userId {
  31 + final userId = _userIdProvider?.call() ?? 0;
  32 + if (userId <= 0) {
  33 + throw StateError('OhosHealthRawDataSqliteStore requires a valid userId');
  34 + }
  35 + return userId;
  36 + }
  37 +
  38 + Future<String> dbPath(int userId) async {
  39 + final dir = _rootDirectory ?? await getApplicationDocumentsDirectory();
  40 + return '${dir.path}/ohos_raw_data_$userId.sqlite';
  41 + }
  42 +
  43 + @override
  44 + Future<int?> latestDataTime({required int dataType}) async {
  45 + final db = await _database(_userId);
  46 + if (dataType == OhosHealthRawDataType.sleepAnalysis) {
  47 + final rows = await db.query(
  48 + sleepDataTable,
  49 + columns: ['to_time'],
  50 + orderBy: 'to_time DESC',
  51 + limit: 1,
  52 + );
  53 + return rows.isEmpty ? null : rows.first['to_time'] as int;
  54 + }
  55 +
  56 + final storedDataType = _storedHealthDataType(dataType);
  57 + final rows = await db.query(
  58 + rawDataTable,
  59 + columns: ['data_time'],
  60 + where: 'data_type = ?',
  61 + whereArgs: [storedDataType],
  62 + orderBy: 'data_time DESC',
  63 + limit: 1,
  64 + );
  65 + return rows.isEmpty ? null : rows.first['data_time'] as int;
  66 + }
  67 +
  68 + @override
  69 + Future<void> upsertRawDataBatch({
  70 + required int dataType,
  71 + required List<OhosHealthRawDataItem> items,
  72 + }) async {
  73 + if (items.isEmpty) return;
  74 + final db = await _database(_userId);
  75 + final createTime = _nowProvider().millisecondsSinceEpoch ~/ 1000;
  76 + await db.transaction((txn) async {
  77 + if (dataType == OhosHealthRawDataType.sleepAnalysis) {
  78 + for (final item in items) {
  79 + await txn.insert(
  80 + sleepDataTable,
  81 + _sleepRow(item, createTime),
  82 + conflictAlgorithm: ConflictAlgorithm.ignore,
  83 + );
  84 + }
  85 + return;
  86 + }
  87 +
  88 + for (final item in items) {
  89 + await txn.insert(
  90 + rawDataTable,
  91 + _rawRow(item, createTime),
  92 + conflictAlgorithm: ConflictAlgorithm.ignore,
  93 + );
  94 + }
  95 + });
  96 + }
  97 +
  98 + Future<void> close({int? userId}) async {
  99 + if (userId != null) {
  100 + final db = _opened.remove(userId);
  101 + if (db != null) {
  102 + await db.close();
  103 + }
  104 + return;
  105 + }
  106 + final databases = _opened.values.toList();
  107 + _opened.clear();
  108 + for (final db in databases) {
  109 + await db.close();
  110 + }
  111 + }
  112 +
  113 + Future<Database> _database(int userId) async {
  114 + final existing = _opened[userId];
  115 + if (existing != null && existing.isOpen) return existing;
  116 + final path = await dbPath(userId);
  117 + final parent = File(path).parent;
  118 + if (!await parent.exists()) {
  119 + await parent.create(recursive: true);
  120 + }
  121 + final factory = _databaseFactory ?? databaseFactory;
  122 + final db = await factory.openDatabase(
  123 + path,
  124 + options: OpenDatabaseOptions(
  125 + version: 1,
  126 + onCreate: (db, version) => _createTables(db),
  127 + ),
  128 + );
  129 + _opened[userId] = db;
  130 + return db;
  131 + }
  132 +
  133 + Future<void> _createTables(Database db) async {
  134 + await db.execute('''
  135 +CREATE TABLE IF NOT EXISTS $rawDataTable (
  136 + id INTEGER PRIMARY KEY AUTOINCREMENT,
  137 + data_type INTEGER NOT NULL,
  138 + data_time INTEGER NOT NULL,
  139 + payload TEXT NOT NULL,
  140 + create_time INTEGER NOT NULL,
  141 + UNIQUE (data_type, data_time, payload)
  142 +)
  143 +''');
  144 + await db.execute('''
  145 +CREATE TABLE IF NOT EXISTS $sleepDataTable (
  146 + id INTEGER PRIMARY KEY AUTOINCREMENT,
  147 + data_type INTEGER NOT NULL,
  148 + from_time INTEGER NOT NULL,
  149 + to_time INTEGER NOT NULL,
  150 + payload TEXT NOT NULL,
  151 + create_time INTEGER NOT NULL,
  152 + UNIQUE (data_type, from_time, to_time, payload)
  153 +)
  154 +''');
  155 + await db.execute(
  156 + 'CREATE INDEX IF NOT EXISTS idx_ohos_raw_data_time '
  157 + 'ON $rawDataTable(data_type, data_time)',
  158 + );
  159 + await db.execute(
  160 + 'CREATE INDEX IF NOT EXISTS idx_ohos_sleep_data_time '
  161 + 'ON $sleepDataTable(to_time)',
  162 + );
  163 + }
  164 +
  165 + Map<String, Object?> _rawRow(
  166 + OhosHealthRawDataItem item,
  167 + int createTime,
  168 + ) {
  169 + return {
  170 + 'data_type': item.dataType,
  171 + 'data_time': item.dataTime,
  172 + 'payload': jsonEncode(item.payload),
  173 + 'create_time': createTime,
  174 + };
  175 + }
  176 +
  177 + Map<String, Object?> _sleepRow(
  178 + OhosHealthRawDataItem item,
  179 + int createTime,
  180 + ) {
  181 + return {
  182 + 'data_type': item.dataType,
  183 + 'from_time': _numPayload(item.payload, 'from_time') ?? 0,
  184 + 'to_time': _numPayload(item.payload, 'to_time') ?? item.dataTime,
  185 + 'payload': jsonEncode(item.payload),
  186 + 'create_time': createTime,
  187 + };
  188 + }
  189 +
  190 + int _storedHealthDataType(int dataType) {
  191 + if (dataType == OhosHealthRawDataType.activitySummary) {
  192 + return HuaweiHealthDataType.activity.dataType;
  193 + }
  194 + if (dataType == OhosHealthRawDataType.workout) {
  195 + return HuaweiHealthDataType.exerciseDuration.dataType;
  196 + }
  197 + return dataType;
  198 + }
  199 +
  200 + int? _numPayload(Map<String, Object?> payload, String key) {
  201 + final value = payload[key];
  202 + return value is num ? value.toInt() : null;
  203 + }
  204 +}
@@ -211,6 +211,10 @@ abstract class PlatformHostApi { @@ -211,6 +211,10 @@ abstract class PlatformHostApi {
211 @async 211 @async
212 bool performRestore(); 212 bool performRestore();
213 213
  214 + /// 获取应用 Documents 目录路径。
  215 + // @async
  216 + // String getApplicationDocumentsPath();
  217 +
214 /// 上传文件到云端 218 /// 上传文件到云端
215 /// 注意catch flutter error 219 /// 注意catch flutter error
216 @async 220 @async