Commit fbaf35ccc8c34c5eeb73732f1f2e27f98493b0f5

Authored by 权海
1 parent f64340df

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

... ... @@ -12,6 +12,9 @@ import '../../../../core/logging/app_logger.dart';
import '../../../../core/services/raw_data_service/health_raw_data_core_service.dart';
import '../../../../core/services/raw_data_service/health_raw_models.dart';
import '../../../../core/services/raw_data_service/platform_ios/apple_health_raw_local_notification.dart';
import '../../../../core/services/raw_data_service/platform_ohos/ohos_harmony_health_raw_data_sync_service_factory.dart';
import '../../../../core/services/raw_data_service/platform_ohos/ohos_health_raw_data_sync_service.dart';
import '../../../../data/models/enums/app_enums.dart';
import '../../../../data/local/user_preferences_storage.dart';
import '../../../../l10n/l10n_extensions.dart';
import '../../../../core/platform/pigeon_api_facade.dart';
... ... @@ -35,6 +38,7 @@ enum DeveloperOptionsAction {
shareFlutterObserverRecord,
shareLocalNotificationDebugRecord,
sendTestHrvLocalNotification,
pullOhosRawData,
clearHealthRawDataDatabaseAndUploadLog,
globalEnv,
}
... ... @@ -92,6 +96,10 @@ class DeveloperOptionsController extends GetxController {
action: DeveloperOptionsAction.sendTestHrvLocalNotification,
),
DeveloperOptionsItem(
title: '主动拉取 OHOS 原始数据',
action: DeveloperOptionsAction.pullOhosRawData,
),
DeveloperOptionsItem(
title: '清除本地数据库、上传日志',
action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog,
),
... ... @@ -123,6 +131,10 @@ class DeveloperOptionsController extends GetxController {
action: DeveloperOptionsAction.shareLocalNotificationDebugRecord,
),
DeveloperOptionsItem(
title: '主动拉取 OHOS 原始数据',
action: DeveloperOptionsAction.pullOhosRawData,
),
DeveloperOptionsItem(
title: '清除本地数据库、上传日志',
action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog,
),
... ... @@ -153,6 +165,9 @@ class DeveloperOptionsController extends GetxController {
case DeveloperOptionsAction.sendTestHrvLocalNotification:
await sendTestHrvLocalNotification();
break;
case DeveloperOptionsAction.pullOhosRawData:
await pullOhosRawData();
break;
case DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog:
await clearHealthRawDataDatabaseAndUploadLog();
break;
... ... @@ -331,6 +346,64 @@ class DeveloperOptionsController extends GetxController {
}
}
Future<void> pullOhosRawData() async {
try {
final userId =
_userPreferencesStorage.preferences.value.meUserInfo?.id ?? 0;
if (userId <= 0) {
Get.snackbar('OHOS 拉取失败', '当前用户 id 无效');
return;
}
final syncService = createDefaultOhosHealthRawDataSyncService(
userIdProvider: () => userId,
);
final now = DateTime.now();
final endTime = now.millisecondsSinceEpoch ~/ 1000;
final startTime =
now.subtract(const Duration(days: 7)).millisecondsSinceEpoch ~/ 1000;
final dataTypes = <int>[
HealthDataUploadType.hrv.type,
HealthDataUploadType.heartRate.type,
HealthDataUploadType.restingHeartRate.type,
OhosHealthRawDataType.sleepAnalysis,
];
await LoadingService.instance.run(() async {
final results = <OhosHealthRawDataSyncResult>[];
for (final dataType in dataTypes) {
AppLogger.i(
'${OhosHealthRawDataSyncService.logMarker} '
'developer_options_pull_start dataType=$dataType '
'startTime=$startTime endTime=$endTime',
);
results.add(
await syncService.syncRawData(
dataType: dataType,
startTime: startTime,
endTime: endTime,
),
);
}
final storedCount = results.fold<int>(
0,
(sum, result) => sum + result.storedCount,
);
final pageCount = results.fold<int>(
0,
(sum, result) => sum + result.pageCount,
);
Get.snackbar(
'OHOS 原始数据拉取完成',
'类型 ${results.length} 个,页数 $pageCount,写入 $storedCount 条',
);
});
} catch (error, stackTrace) {
AppLogger.e('Pull OHOS raw data failed', error, stackTrace);
Get.snackbar('OHOS 拉取失败', error.toString());
}
}
Future<void> clearHealthRawDataDatabaseAndUploadLog() async {
try {
await _healthRawDataCoreService.clearLocalDatabaseAndUploadLog();
... ...
... ... @@ -47,7 +47,7 @@ class HarmonyApi {
return safeCall(
call: () async {
final queryParameters = <String, dynamic>{
'date_type': dataType.dataType,
'data_type': dataType.dataType,
'start_date': startDate,
'end_date': endDate,
};
... ...
... ... @@ -137,8 +137,11 @@ class AppleHealthRawDataSource implements HealthRawDataSource {
class OhosHealthRawDataSource implements HealthRawDataSource {
OhosHealthRawDataSource({
OhosHealthRawDataSyncService? syncService,
}) : _syncService =
syncService ?? createDefaultOhosHealthRawDataSyncService();
int Function()? userIdProvider,
}) : _syncService = syncService ??
createDefaultOhosHealthRawDataSyncService(
userIdProvider: userIdProvider,
);
final OhosHealthRawDataSyncService _syncService;
... ...
... ... @@ -5,13 +5,20 @@ import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart';
import 'package:get/get.dart';
import 'huawei_health_data_type.dart';
import 'ohos_health_raw_data_local_store.dart';
import 'ohos_health_raw_data_sync_service.dart';
OhosHealthRawDataSyncService createDefaultOhosHealthRawDataSyncService() {
OhosHealthRawDataSyncService createDefaultOhosHealthRawDataSyncService({
int Function()? userIdProvider,
}) {
final localStore = OhosHealthRawDataSqliteStore(
userIdProvider: userIdProvider,
);
if (!Get.isRegistered<HarmonyApi>()) {
return OhosHealthRawDataSyncService();
return OhosHealthRawDataSyncService(localStore: localStore);
}
return OhosHealthRawDataSyncService(
localStore: localStore,
remoteDataSource: OhosHarmonyHealthRawDataRemoteDataSource(
HarmonyApiOhosRawDataClient(Get.find<HarmonyApi>()),
),
... ...
... ... @@ -28,7 +28,10 @@ class OHOSHealthRawDataCoreService {
int Function()? userIdProvider,
bool uploadResultsAfterCalculation = true,
HealthApi? serverHealthApi,
}) : _rawDataSource = rawDataSource ?? OhosHealthRawDataSource(),
}) : _rawDataSource = rawDataSource ??
OhosHealthRawDataSource(
userIdProvider: userIdProvider,
),
_localStore = localStore ??
HealthRawStressLocalStore(databaseNamePrefix: 'ohos_'),
_environmentConfig = environmentConfig,
... ...
import 'dart:convert';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import 'huawei_health_data_type.dart';
import 'ohos_health_raw_data_sync_service.dart';
class OhosHealthRawDataSqliteStore implements OhosHealthRawDataLocalStore {
OhosHealthRawDataSqliteStore({
required int Function()? userIdProvider,
Directory? rootDirectory,
DatabaseFactory? databaseFactory,
DateTime Function()? nowProvider,
}) : _userIdProvider = userIdProvider,
_rootDirectory = rootDirectory,
_databaseFactory = databaseFactory,
_nowProvider = nowProvider ?? DateTime.now;
static const rawDataTable = 'ohos_raw_data';
static const sleepDataTable = 'ohos_sleep_data';
final int Function()? _userIdProvider;
final Directory? _rootDirectory;
final DatabaseFactory? _databaseFactory;
final DateTime Function() _nowProvider;
final Map<int, Database> _opened = <int, Database>{};
int get _userId {
final userId = _userIdProvider?.call() ?? 0;
if (userId <= 0) {
throw StateError('OhosHealthRawDataSqliteStore requires a valid userId');
}
return userId;
}
Future<String> dbPath(int userId) async {
final dir = _rootDirectory ?? await getApplicationDocumentsDirectory();
return '${dir.path}/ohos_raw_data_$userId.sqlite';
}
@override
Future<int?> latestDataTime({required int dataType}) async {
final db = await _database(_userId);
if (dataType == OhosHealthRawDataType.sleepAnalysis) {
final rows = await db.query(
sleepDataTable,
columns: ['to_time'],
orderBy: 'to_time DESC',
limit: 1,
);
return rows.isEmpty ? null : rows.first['to_time'] as int;
}
final storedDataType = _storedHealthDataType(dataType);
final rows = await db.query(
rawDataTable,
columns: ['data_time'],
where: 'data_type = ?',
whereArgs: [storedDataType],
orderBy: 'data_time DESC',
limit: 1,
);
return rows.isEmpty ? null : rows.first['data_time'] as int;
}
@override
Future<void> upsertRawDataBatch({
required int dataType,
required List<OhosHealthRawDataItem> items,
}) async {
if (items.isEmpty) return;
final db = await _database(_userId);
final createTime = _nowProvider().millisecondsSinceEpoch ~/ 1000;
await db.transaction((txn) async {
if (dataType == OhosHealthRawDataType.sleepAnalysis) {
for (final item in items) {
await txn.insert(
sleepDataTable,
_sleepRow(item, createTime),
conflictAlgorithm: ConflictAlgorithm.ignore,
);
}
return;
}
for (final item in items) {
await txn.insert(
rawDataTable,
_rawRow(item, createTime),
conflictAlgorithm: ConflictAlgorithm.ignore,
);
}
});
}
Future<void> close({int? userId}) async {
if (userId != null) {
final db = _opened.remove(userId);
if (db != null) {
await db.close();
}
return;
}
final databases = _opened.values.toList();
_opened.clear();
for (final db in databases) {
await db.close();
}
}
Future<Database> _database(int userId) async {
final existing = _opened[userId];
if (existing != null && existing.isOpen) return existing;
final path = await dbPath(userId);
final parent = File(path).parent;
if (!await parent.exists()) {
await parent.create(recursive: true);
}
final factory = _databaseFactory ?? databaseFactory;
final db = await factory.openDatabase(
path,
options: OpenDatabaseOptions(
version: 1,
onCreate: (db, version) => _createTables(db),
),
);
_opened[userId] = db;
return db;
}
Future<void> _createTables(Database db) async {
await db.execute('''
CREATE TABLE IF NOT EXISTS $rawDataTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
data_type INTEGER NOT NULL,
data_time INTEGER NOT NULL,
payload TEXT NOT NULL,
create_time INTEGER NOT NULL,
UNIQUE (data_type, data_time, payload)
)
''');
await db.execute('''
CREATE TABLE IF NOT EXISTS $sleepDataTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
data_type INTEGER NOT NULL,
from_time INTEGER NOT NULL,
to_time INTEGER NOT NULL,
payload TEXT NOT NULL,
create_time INTEGER NOT NULL,
UNIQUE (data_type, from_time, to_time, payload)
)
''');
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_ohos_raw_data_time '
'ON $rawDataTable(data_type, data_time)',
);
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_ohos_sleep_data_time '
'ON $sleepDataTable(to_time)',
);
}
Map<String, Object?> _rawRow(
OhosHealthRawDataItem item,
int createTime,
) {
return {
'data_type': item.dataType,
'data_time': item.dataTime,
'payload': jsonEncode(item.payload),
'create_time': createTime,
};
}
Map<String, Object?> _sleepRow(
OhosHealthRawDataItem item,
int createTime,
) {
return {
'data_type': item.dataType,
'from_time': _numPayload(item.payload, 'from_time') ?? 0,
'to_time': _numPayload(item.payload, 'to_time') ?? item.dataTime,
'payload': jsonEncode(item.payload),
'create_time': createTime,
};
}
int _storedHealthDataType(int dataType) {
if (dataType == OhosHealthRawDataType.activitySummary) {
return HuaweiHealthDataType.activity.dataType;
}
if (dataType == OhosHealthRawDataType.workout) {
return HuaweiHealthDataType.exerciseDuration.dataType;
}
return dataType;
}
int? _numPayload(Map<String, Object?> payload, String key) {
final value = payload[key];
return value is num ? value.toInt() : null;
}
}
... ...
... ... @@ -211,6 +211,10 @@ abstract class PlatformHostApi {
@async
bool performRestore();
/// 获取应用 Documents 目录路径。
// @async
// String getApplicationDocumentsPath();
/// 上传文件到云端
/// 注意catch flutter error
@async
... ...