Commit 0f66e87bf17202eaf78b3560d0dc52dadb238846

Authored by 权海
1 parent fbaf35cc

feat(ui):鸿蒙数据接入

... ... @@ -14,7 +14,6 @@ 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';
... ... @@ -361,29 +360,16 @@ class DeveloperOptionsController extends GetxController {
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,
),
);
}
AppLogger.i(
'${OhosHealthRawDataSyncService.logMarker} '
'developer_options_pull_calculation_start '
'startTime=$startTime endTime=$endTime',
);
final results = await syncService.syncCalculationRawData(
startTime: startTime,
endTime: endTime,
);
final storedCount = results.fold<int>(
0,
... ...
... ... @@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/core/error/http_error_handling_policy.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ohos/huawei_health_data_type.dart';
import 'package:doublefeel_flutter/data/models/harmony/hm_health_data.dart';
import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart';
import 'package:doublefeel_flutter/data/models/harmony/hm_workout_data.dart';
import 'package:doublefeel_flutter/data/models/harmony/privacy_records.dart';
import '../../result/app_result.dart';
... ... @@ -83,4 +84,25 @@ class HarmonyApi {
},
);
}
/// startTime: 开始时间,Unix 秒
/// endTime: 结束时间,Unix 秒
Future<AppResult<HmWorkoutData>> getWorkoutData(int startTime, int endTime) {
return safeCall(
call: () async {
final queryParameters = <String, dynamic>{
'start_time': startTime,
'end_time': endTime,
};
final response = await _dioClient.dio.get(
ApiPaths.hmActivityTime,
queryParameters: queryParameters,
);
return HmWorkoutData.fromJson(
response.data as Map<String, dynamic>,
);
},
);
}
}
... ...
... ... @@ -95,6 +95,7 @@ abstract final class ApiPaths {
static const hmAuth = '/client/doublefeel/huawei_hm/auth/';
static const hmHealthData = '/client/doublefeel/huawei_hm/health_data/';
static const hmSleepData = '/client/doublefeel/huawei_hm/sleep_data/';
static const hmPrivacyRecords = '/client/doublefeel/huawei_hm/privacy_records/';
static const hmActivityTime = '/client/doublefeel/huawei_hm/activity_time/';
static const hmPrivacyRecords =
'/client/doublefeel/huawei_hm/privacy_records/';
}
... ...
import 'package:flutter/foundation.dart';
import '../../platform/pigeon_api_facade.dart';
import 'platform_ohos/huawei_health_data_type.dart';
import 'platform_ohos/ohos_harmony_health_raw_data_sync_service_factory.dart';
import 'platform_ohos/ohos_health_raw_data_sync_service.dart';
... ... @@ -138,12 +139,39 @@ class OhosHealthRawDataSource implements HealthRawDataSource {
OhosHealthRawDataSource({
OhosHealthRawDataSyncService? syncService,
int Function()? userIdProvider,
}) : _syncService = syncService ??
bool syncBeforeRead = false,
}) : _syncService = syncService ??
createDefaultOhosHealthRawDataSyncService(
userIdProvider: userIdProvider,
);
),
_syncBeforeRead = syncBeforeRead;
final OhosHealthRawDataSyncService _syncService;
final bool _syncBeforeRead;
Future<List<OhosHealthRawDataSyncResult>> syncCalculationRawData({
int? startTime,
int? endTime,
List<int>? dataTypes,
}) {
return _syncService.syncCalculationRawData(
startTime: startTime,
endTime: endTime,
dataTypes: dataTypes,
);
}
Future<OhosHealthRawDataSyncResult> syncRawData({
required int dataType,
int? startTime,
int? endTime,
}) {
return _syncService.syncRawData(
dataType: dataType,
startTime: startTime,
endTime: endTime,
);
}
@override
Future<bool> hasHealthData() async {
... ... @@ -157,13 +185,29 @@ class OhosHealthRawDataSource implements HealthRawDataSource {
int startTime,
int endTime,
) async {
await _syncService.syncRawData(
if (_syncBeforeRead) {
await _syncService.syncRawData(
dataType: dataType,
startTime: startTime,
endTime: endTime,
);
}
final items = await _syncService.queryRawData(
dataType: dataType,
startTime: startTime,
endTime: endTime,
);
// TODO: Read synced OHOS raw points from local database.
return const <HealthKitRawDataPoint>[];
return items
.map(
(item) => HealthKitRawDataPoint(
dataType: dataType,
startTime: item.dataTime,
endTime: item.dataTime,
value: _doublePayload(item.payload, 'value'),
isMotionLike: _boolPayload(item.payload, 'is_motion_like'),
),
)
.toList(growable: false);
}
@override
... ... @@ -171,13 +215,24 @@ class OhosHealthRawDataSource implements HealthRawDataSource {
int startTime,
int endTime,
) async {
await _syncService.syncRawData(
if (_syncBeforeRead) {
await _syncService.syncRawData(
dataType: OhosHealthRawDataType.sleepAnalysis,
startTime: startTime,
endTime: endTime,
);
}
final items = await _syncService.queryRawData(
dataType: OhosHealthRawDataType.sleepAnalysis,
startTime: startTime,
endTime: endTime,
);
// TODO: Read synced OHOS sleep groups from local database.
return const <HealthKitRawSleepDataPoint>[];
return [
HealthKitRawSleepDataPoint(
dataType: OhosHealthRawDataType.sleepAnalysis,
sleepDataPoints: items.map(_sleepPointFromItem).toList(growable: false),
),
];
}
@override
... ... @@ -185,13 +240,62 @@ class OhosHealthRawDataSource implements HealthRawDataSource {
int startTime,
int endTime,
) async {
await _syncService.syncRawData(
if (_syncBeforeRead) {
await Future.wait(
<int>[
OhosHealthRawDataType.activitySummary,
HuaweiHealthDataType.exerciseDuration.dataType,
HuaweiHealthDataType.standingDuration.dataType,
HuaweiHealthDataType.stepCount.dataType,
].map(
(dataType) => _syncService.syncRawData(
dataType: dataType,
startTime: startTime,
endTime: endTime,
),
),
eagerError: true,
);
}
final activity = await _syncService.queryRawData(
dataType: OhosHealthRawDataType.activitySummary,
startTime: startTime,
endTime: endTime,
);
// TODO: Read synced OHOS activity summaries from local database.
return const <HealthKitRawActivityDataPoint>[];
final exercise = await _syncService.queryRawData(
dataType: HuaweiHealthDataType.exerciseDuration.dataType,
startTime: startTime,
endTime: endTime,
);
final standing = await _syncService.queryRawData(
dataType: HuaweiHealthDataType.standingDuration.dataType,
startTime: startTime,
endTime: endTime,
);
final byTime = <int, HealthKitRawActivityDataPoint>{};
for (final item in activity) {
final point = byTime.putIfAbsent(
item.dataTime,
() => HealthKitRawActivityDataPoint(endTime: item.dataTime),
);
point.activeEnergyBurned = _doublePayload(item.payload, 'value');
}
for (final item in exercise) {
final point = byTime.putIfAbsent(
item.dataTime,
() => HealthKitRawActivityDataPoint(endTime: item.dataTime),
);
point.appleExerciseTime = _doublePayload(item.payload, 'value');
}
for (final item in standing) {
final point = byTime.putIfAbsent(
item.dataTime,
() => HealthKitRawActivityDataPoint(endTime: item.dataTime),
);
point.appleStandHours = _doublePayload(item.payload, 'value');
}
return byTime.values.toList(growable: false)
..sort((a, b) => a.endTime.compareTo(b.endTime));
}
@override
... ... @@ -199,12 +303,67 @@ class OhosHealthRawDataSource implements HealthRawDataSource {
int startTime,
int endTime,
) async {
await _syncService.syncRawData(
if (_syncBeforeRead) {
await _syncService.syncRawData(
dataType: OhosHealthRawDataType.workout,
startTime: startTime,
endTime: endTime,
);
}
final items = await _syncService.queryRawData(
dataType: OhosHealthRawDataType.workout,
startTime: startTime,
endTime: endTime,
);
// TODO: Read synced OHOS workout intervals from local database.
return const <HealthKitRawWorkoutDataPoint>[];
return items.map(_workoutPointFromItem).toList(growable: false);
}
HealthKitRawDataPoint _sleepPointFromItem(OhosHealthRawDataItem item) {
return HealthKitRawDataPoint(
dataType: _appleSleepType(item.dataType),
startTime: _intPayload(item.payload, 'from_time') ?? item.dataTime,
endTime: _intPayload(item.payload, 'to_time') ?? item.dataTime,
value: null,
);
}
HealthKitRawWorkoutDataPoint _workoutPointFromItem(
OhosHealthRawDataItem item,
) {
return HealthKitRawWorkoutDataPoint(
workoutType: _intPayload(item.payload, 'activity_type') ?? 0,
startTime: _intPayload(item.payload, 'from_time') ?? item.dataTime,
endTime: _intPayload(item.payload, 'to_time') ?? item.dataTime,
);
}
int _appleSleepType(int ohosSleepType) {
return switch (ohosSleepType) {
1 => 4, // deep
2 => 3, // light/core
3 => 5, // REM
4 => 2, // awake
5 => 1, // nap/asleep
6 => 0, // in bed
7 => 1, // manual sleep
_ => 1,
};
}
int? _intPayload(Map<String, Object?> payload, String key) {
final value = payload[key];
return value is num ? value.toInt() : null;
}
double? _doublePayload(Map<String, Object?> payload, String key) {
final value = payload[key];
return value is num ? value.toDouble() : null;
}
bool? _boolPayload(Map<String, Object?> payload, String key) {
final value = payload[key];
if (value is bool) return value;
if (value is num) return value != 0;
return null;
}
}
... ...
... ... @@ -2402,11 +2402,12 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
whereArgs: [row['raw_end_time']],
limit: 1,
);
if (existing.isEmpty) {
await db.insert(table, row);
final existingRow = existing.isEmpty
? await _insertOrQueryRawResultOnConflict(db, table, row)
: existing.first;
if (existingRow == null) {
return;
}
final existingRow = existing.first;
if (_matchesStoredValues(
existingRow, row, _rawResultStoredValueKeys(row))) {
await db.update(
... ... @@ -2452,6 +2453,26 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
);
}
Future<Map<String, Object?>?> _insertOrQueryRawResultOnConflict(
DatabaseExecutor db,
String table,
Map<String, Object?> row,
) async {
try {
await db.insert(table, row);
return null;
} on DatabaseException catch (error) {
if (!error.isUniqueConstraintError()) rethrow;
}
final rows = await db.query(
table,
where: 'raw_end_time = ?',
whereArgs: [row['raw_end_time']],
limit: 1,
);
return rows.isEmpty ? null : rows.first;
}
Future<void> _upsertDailyStressResettingUploaded(
DatabaseExecutor db,
Map<String, Object?> row,
... ... @@ -2467,10 +2488,35 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
limit: 1,
);
if (existing.isEmpty) {
await db.insert(dailyStressResultsTable, row);
final existingRow = await _insertOrQueryOnConflict(
db,
dailyStressResultsTable,
row,
column: 'date',
);
if (existingRow == null) return;
await _updateDailyStressResettingUploaded(
db,
row,
existingRow,
isToday: isToday,
);
return;
}
final existingRow = existing.first;
await _updateDailyStressResettingUploaded(
db,
row,
existing.first,
isToday: isToday,
);
}
Future<void> _updateDailyStressResettingUploaded(
DatabaseExecutor db,
Map<String, Object?> row,
Map<String, Object?> existingRow, {
required bool isToday,
}) async {
final valueChanged = !_matchesStoredValues(
existingRow,
row,
... ... @@ -2503,10 +2549,24 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
limit: 1,
);
if (existing.isEmpty) {
await db.insert(sleepResultsTable, row);
final existingRow = await _insertOrQueryOnConflict(
db,
sleepResultsTable,
row,
column: 'date',
);
if (existingRow == null) return;
await _updateSleepResultResettingUploaded(db, row, existingRow);
return;
}
final existingRow = existing.first;
await _updateSleepResultResettingUploaded(db, row, existing.first);
}
Future<void> _updateSleepResultResettingUploaded(
DatabaseExecutor db,
Map<String, Object?> row,
Map<String, Object?> existingRow,
) async {
final valueChanged = !_matchesStoredValues(
existingRow,
row,
... ... @@ -2538,6 +2598,27 @@ CREATE TABLE IF NOT EXISTS $sleepResultsTable (
);
}
Future<Map<String, Object?>?> _insertOrQueryOnConflict(
DatabaseExecutor db,
String table,
Map<String, Object?> row, {
required String column,
}) async {
try {
await db.insert(table, row);
return null;
} on DatabaseException catch (error) {
if (!error.isUniqueConstraintError()) rethrow;
}
final rows = await db.query(
table,
where: '$column = ?',
whereArgs: [row[column]],
limit: 1,
);
return rows.isEmpty ? null : rows.first;
}
int _currentUnixSeconds() {
return DateTime.now().millisecondsSinceEpoch ~/ 1000;
}
... ...
... ... @@ -9,7 +9,7 @@ enum HuaweiHealthDataType {
heartRate(2),
/// 血氧饱和度
bloodOxygenSaturation(3),
// bloodOxygenSaturation(3),
/// 活动量
activity(4),
... ... @@ -24,22 +24,22 @@ enum HuaweiHealthDataType {
stepCount(7),
/// 步行心率
walkingHeartRate(8),
// walkingHeartRate(8),
/// 静息心率
restingHeartRate(9),
restingHeartRate(9);
/// 睡眠心率
sleepingHeartRate(10),
// sleepingHeartRate(10),
/// 手腕温度
wristTemperature(11),
// wristTemperature(11),
/// 睡眠呼吸频率
sleepingRespiratoryRate(12),
// sleepingRespiratoryRate(12),
/// 房颤
atrialFibrillation(13);
// atrialFibrillation(13);
const HuaweiHealthDataType(this.dataType);
... ...
... ... @@ -2,6 +2,7 @@ import 'package:doublefeel_flutter/core/network/api/harmony_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/data/models/harmony/hm_health_data.dart';
import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart';
import 'package:doublefeel_flutter/data/models/harmony/hm_workout_data.dart';
import 'package:get/get.dart';
import 'huawei_health_data_type.dart';
... ... @@ -43,4 +44,9 @@ class HarmonyApiOhosRawDataClient implements OhosHarmonyRawDataClient {
Future<AppResult<HmSleepData>> getSleepData(int startDate, int endDate) {
return _harmonyApi.getSleepData(startDate, endDate);
}
@override
Future<AppResult<HmWorkoutData>> getWorkoutData(int startTime, int endTime) {
return _harmonyApi.getWorkoutData(startTime, endTime);
}
}
... ...
... ... @@ -5,12 +5,15 @@ import 'dart:isolate';
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:get/get.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 '../../../network/api/harmony_api.dart';
import '../../../result/app_result.dart';
import '../health_raw_data_source.dart';
import '../health_raw_models.dart';
import '../health_raw_stress_calculator.dart';
... ... @@ -28,16 +31,22 @@ class OHOSHealthRawDataCoreService {
int Function()? userIdProvider,
bool uploadResultsAfterCalculation = true,
HealthApi? serverHealthApi,
HarmonyApi? harmonyApi,
Future<bool> Function()? healthReadAuthorizationChecker,
}) : _rawDataSource = rawDataSource ??
OhosHealthRawDataSource(
userIdProvider: userIdProvider,
syncBeforeRead: false,
),
_localStore = localStore ??
HealthRawStressLocalStore(databaseNamePrefix: 'ohos_'),
_environmentConfig = environmentConfig,
_userIdProvider = userIdProvider,
_uploadResultsAfterCalculation = uploadResultsAfterCalculation,
_serverHealthApi = serverHealthApi;
_serverHealthApi = serverHealthApi,
_harmonyApi = harmonyApi ??
(Get.isRegistered<HarmonyApi>() ? Get.find<HarmonyApi>() : null),
_healthReadAuthorizationChecker = healthReadAuthorizationChecker;
final HealthRawDataSource _rawDataSource;
final HealthRawStressLocalStore _localStore;
... ... @@ -45,6 +54,8 @@ class OHOSHealthRawDataCoreService {
final int Function()? _userIdProvider;
final bool _uploadResultsAfterCalculation;
final HealthApi? _serverHealthApi;
final HarmonyApi? _harmonyApi;
final Future<bool> Function()? _healthReadAuthorizationChecker;
final StreamController<HealthRawDataUpdatedEvent>
_healthDataUpdatedController =
StreamController<HealthRawDataUpdatedEvent>.broadcast();
... ... @@ -192,6 +203,17 @@ class OHOSHealthRawDataCoreService {
}
final userId = _userId;
final hasAuthorization = await _hasHealthReadAuthorization();
if (!hasAuthorization) {
_logInfo('skip calculate without OHOS health privacy authorization');
return HealthRawStressCalculationResult(
userId: userId,
hrvStressPoints: const <HealthRawHrvStressPoint>[],
realtimeStressPoints: const <HealthRawRealtimeStressPoint>[],
dailyStressPoints: const <HealthRawDailyStressPoint>[],
sleepResults: const <HealthRawSleepResult>[],
);
}
await _localStore.ensureReadable(userId);
final effectiveEndTime =
endTime ?? DateTime.now().millisecondsSinceEpoch ~/ 1000;
... ... @@ -204,6 +226,10 @@ class OHOSHealthRawDataCoreService {
if (effectiveEndTime < requestedStartTime) {
throw ArgumentError.value(endTime, 'endTime');
}
await _syncCalculationRawDataIfNeeded(
startTime: requestedStartTime,
endTime: effectiveEndTime,
);
final hrvContextStart = await _localStore.latestHrvSourceStartTime(userId);
final realtimeContextStart =
... ... @@ -420,7 +446,10 @@ class OHOSHealthRawDataCoreService {
required int startTime,
required int endTime,
int readChunkDays = defaultReadChunkDays,
}) {
}) async {
if (!await _hasHealthReadAuthorization()) {
return const <HealthKitRawDataPoint>[];
}
return _fetchRawDataInChunks(
dataType,
startTime,
... ... @@ -433,7 +462,10 @@ class OHOSHealthRawDataCoreService {
required int startTime,
required int endTime,
int readChunkDays = defaultReadChunkDays,
}) {
}) async {
if (!await _hasHealthReadAuthorization()) {
return const <HealthKitRawDataPoint>[];
}
return _fetchSleepIntervalsInChunks(
startTime,
endTime,
... ... @@ -445,6 +477,9 @@ class OHOSHealthRawDataCoreService {
required int startTime,
required int endTime,
}) async {
if (!await _hasHealthReadAuthorization()) {
return const <HealthKitRawActivityDataPoint>[];
}
final points = await _rawDataSource.getRawActivityData(startTime, endTime);
return points
.where((e) => e.endTime >= startTime && e.endTime <= endTime)
... ... @@ -476,6 +511,40 @@ class OHOSHealthRawDataCoreService {
if (_serverHealthApi == null) return;
}
Future<void> _syncCalculationRawDataIfNeeded({
required int startTime,
required int endTime,
}) async {
final rawDataSource = _rawDataSource;
if (rawDataSource is! OhosHealthRawDataSource) return;
await rawDataSource.syncCalculationRawData(
startTime: startTime,
endTime: endTime,
);
}
Future<bool> _hasHealthReadAuthorization() async {
final checker = _healthReadAuthorizationChecker;
if (checker != null) {
return checker();
}
final harmonyApi = _harmonyApi;
if (harmonyApi == null) {
_logInfo('skip health privacy authorization check: HarmonyApi missing');
return false;
}
final result = await harmonyApi.getPrivacyRecords();
return switch (result) {
AppSuccess(:final data) => data.opinion == 1,
AppFailure(:final error) => _logPrivacyAuthorizationFailure(error),
};
}
bool _logPrivacyAuthorizationFailure(Object error) {
_logInfo('OHOS health privacy authorization check failed: $error');
return false;
}
Future<List<HealthRawDailyStressPoint>> _calculateAndStoreDailyStressPoints({
required int userId,
required List<HealthRawRealtimeStressPoint> realtimePoints,
... ... @@ -683,4 +752,15 @@ class OHOSHealthRawDataCoreService {
// Logger may be unavailable in isolated unit tests.
}
}
static void _logInfo(String message) {
final tagged = 'OHOSHealthRawDataCoreService $message';
developer.log(tagged, name: 'OHOSHealthRawDataCoreService');
debugPrint(tagged);
try {
AppLogger.i(tagged);
} catch (_) {
// Logger may be unavailable in isolated unit tests.
}
}
}
... ...
import 'dart:convert';
import 'dart:io';
import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
... ... @@ -20,6 +21,7 @@ class OhosHealthRawDataSqliteStore implements OhosHealthRawDataLocalStore {
static const rawDataTable = 'ohos_raw_data';
static const sleepDataTable = 'ohos_sleep_data';
static const logMarker = '[OHOS_RAW_DATA_DB]';
final int Function()? _userIdProvider;
final Directory? _rootDirectory;
... ... @@ -46,53 +48,159 @@ class OhosHealthRawDataSqliteStore implements OhosHealthRawDataLocalStore {
if (dataType == OhosHealthRawDataType.sleepAnalysis) {
final rows = await db.query(
sleepDataTable,
columns: ['to_time'],
orderBy: 'to_time DESC',
columns: ['from_time'],
orderBy: 'from_time DESC',
limit: 1,
);
return rows.isEmpty ? null : rows.first['to_time'] as int;
return rows.isEmpty ? null : rows.first['from_time'] as int;
}
if (dataType == OhosHealthRawDataType.workout) {
final table = _rawDataTable(dataType);
await _createRawIntervalDataTypeTable(db, table);
final rows = await db.query(
table,
columns: ['from_time'],
orderBy: 'from_time DESC',
limit: 1,
);
return rows.isEmpty ? null : rows.first['from_time'] as int;
}
final storedDataType = _storedHealthDataType(dataType);
final table = _rawDataTable(storedDataType);
await _createRawDataTypeTable(db, table);
final rows = await db.query(
rawDataTable,
columns: ['data_time'],
where: 'data_type = ?',
whereArgs: [storedDataType],
orderBy: 'data_time DESC',
table,
columns: ['time'],
orderBy: 'time DESC',
limit: 1,
);
return rows.isEmpty ? null : rows.first['data_time'] as int;
return rows.isEmpty ? null : rows.first['time'] as int;
}
@override
Future<void> upsertRawDataBatch({
Future<int> upsertRawDataBatch({
required int dataType,
required List<OhosHealthRawDataItem> items,
}) async {
if (items.isEmpty) return;
if (items.isEmpty) return 0;
final db = await _database(_userId);
final createTime = _nowProvider().millisecondsSinceEpoch ~/ 1000;
final latestTime = await latestDataTime(dataType: dataType);
final filteredItems = items
.where(
(item) => _shouldStoreItem(
dataType: dataType,
item: item,
latestTime: latestTime,
),
)
.toList(growable: false);
if (filteredItems.isEmpty) {
_log(
'skip_store_no_new_items dataType=$dataType latestTime=$latestTime '
'incoming=${items.length}',
);
return 0;
}
var storedCount = 0;
await db.transaction((txn) async {
if (dataType == OhosHealthRawDataType.sleepAnalysis) {
for (final item in items) {
await txn.insert(
for (final item in filteredItems) {
final rowId = await txn.insert(
sleepDataTable,
_sleepRow(item, createTime),
conflictAlgorithm: ConflictAlgorithm.ignore,
);
if (rowId > 0) storedCount += 1;
}
return;
}
if (dataType == OhosHealthRawDataType.workout) {
final table = _rawDataTable(dataType);
await _createRawIntervalDataTypeTable(txn, table);
for (final item in filteredItems) {
final rowId = await txn.insert(
table,
_intervalRow(item, createTime),
conflictAlgorithm: ConflictAlgorithm.ignore,
);
if (rowId > 0) storedCount += 1;
}
return;
}
for (final item in items) {
await txn.insert(
rawDataTable,
final storedDataType = _storedHealthDataType(dataType);
final table = _rawDataTable(storedDataType);
await _createRawDataTypeTable(txn, table);
for (final item in filteredItems) {
final rowId = await txn.insert(
table,
_rawRow(item, createTime),
conflictAlgorithm: ConflictAlgorithm.ignore,
conflictAlgorithm: _dailyItemNeedsRefresh(
dataType: dataType,
item: item,
latestTime: latestTime,
)
? ConflictAlgorithm.replace
: ConflictAlgorithm.ignore,
);
if (rowId > 0) storedCount += 1;
}
});
final skippedCount = filteredItems.length - storedCount;
if (skippedCount > 0) {
_log(
'skip_store_duplicates dataType=$dataType latestTime=$latestTime '
'incoming=${items.length} candidates=${filteredItems.length} '
'duplicates=$skippedCount storedItems=$storedCount',
);
}
return storedCount;
}
@override
Future<List<OhosHealthRawDataItem>> queryRawData({
required int dataType,
required int startTime,
required int endTime,
}) async {
final db = await _database(_userId);
if (dataType == OhosHealthRawDataType.sleepAnalysis) {
final rows = await db.query(
sleepDataTable,
where: 'to_time >= ? AND from_time <= ?',
whereArgs: [startTime, endTime],
orderBy: 'from_time ASC',
);
return rows.map(_sleepItemFromRow).toList(growable: false);
}
if (dataType == OhosHealthRawDataType.workout) {
final table = _rawDataTable(dataType);
await _createRawIntervalDataTypeTable(db, table);
final rows = await db.query(
table,
where: 'to_time >= ? AND from_time <= ?',
whereArgs: [startTime, endTime],
orderBy: 'from_time ASC',
);
return rows
.map((row) => _intervalItemFromRow(row, dataType))
.toList(growable: false);
}
final storedDataType = _storedHealthDataType(dataType);
final table = _rawDataTable(storedDataType);
await _createRawDataTypeTable(db, table);
final rows = await db.query(
table,
where: 'time >= ? AND time <= ?',
whereArgs: [startTime, endTime],
orderBy: 'time ASC',
);
return rows
.map((row) => _rawItemFromRow(row, storedDataType))
.toList(growable: false);
}
Future<void> close({int? userId}) async {
... ... @@ -114,6 +222,7 @@ class OhosHealthRawDataSqliteStore implements OhosHealthRawDataLocalStore {
final existing = _opened[userId];
if (existing != null && existing.isOpen) return existing;
final path = await dbPath(userId);
_log('open_database userId=$userId path=$path');
final parent = File(path).parent;
if (!await parent.exists()) {
await parent.create(recursive: true);
... ... @@ -130,35 +239,66 @@ class OhosHealthRawDataSqliteStore implements OhosHealthRawDataLocalStore {
return db;
}
void _log(String message) {
final taggedMessage = '$logMarker $message';
debugPrint(taggedMessage);
try {
AppLogger.i(taggedMessage);
} catch (_) {
// AppLogger may not be initialized in isolated test/bootstrap contexts.
}
}
Future<void> _createTables(Database db) async {
await db.execute('''
CREATE TABLE IF NOT EXISTS $rawDataTable (
CREATE TABLE IF NOT EXISTS $sleepDataTable (
id INTEGER PRIMARY KEY AUTOINCREMENT,
data_type INTEGER NOT NULL,
data_time INTEGER NOT NULL,
payload TEXT NOT NULL,
data_type INTEGER,
from_time INTEGER NOT NULL,
to_time INTEGER NOT NULL,
create_time INTEGER NOT NULL,
UNIQUE (data_type, data_time, payload)
UNIQUE (from_time)
)
''');
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_ohos_sleep_data_time '
'ON $sleepDataTable(to_time)',
);
}
Future<void> _createRawDataTypeTable(
DatabaseExecutor db,
String table,
) async {
await db.execute('''
CREATE TABLE IF NOT EXISTS $sleepDataTable (
CREATE TABLE IF NOT EXISTS $table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
data_type INTEGER NOT NULL,
from_time INTEGER NOT NULL,
to_time INTEGER NOT NULL,
payload TEXT NOT NULL,
time INTEGER NOT NULL,
value REAL,
is_asleep INTEGER,
create_time INTEGER NOT NULL,
UNIQUE (data_type, from_time, to_time, payload)
UNIQUE (time)
)
''');
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_ohos_raw_data_time '
'ON $rawDataTable(data_type, data_time)',
'CREATE INDEX IF NOT EXISTS idx_${table}_time ON $table(time)',
);
}
Future<void> _createRawIntervalDataTypeTable(
DatabaseExecutor db, String table) async {
await db.execute('''
CREATE TABLE IF NOT EXISTS $table (
id INTEGER PRIMARY KEY AUTOINCREMENT,
activity_type INTEGER,
from_time INTEGER NOT NULL,
to_time INTEGER NOT NULL,
create_time INTEGER NOT NULL,
UNIQUE (from_time)
)
''');
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_ohos_sleep_data_time '
'ON $sleepDataTable(to_time)',
'CREATE INDEX IF NOT EXISTS idx_${table}_time ON $table(to_time)',
);
}
... ... @@ -167,9 +307,25 @@ CREATE TABLE IF NOT EXISTS $sleepDataTable (
int createTime,
) {
return {
'data_type': item.dataType,
'data_time': item.dataTime,
'payload': jsonEncode(item.payload),
'time': _numPayload(item.payload, 'time') ?? item.dataTime,
'value': _numPayload(item.payload, 'value'),
'is_asleep': _numPayload(item.payload, 'is_asleep'),
'create_time': createTime,
};
}
Map<String, Object?> _intervalRow(
OhosHealthRawDataItem item,
int createTime,
) {
return {
'activity_type': _numPayload(item.payload, 'activity_type'),
'from_time': _numPayload(item.payload, 'from_time') ??
_numPayload(item.payload, 'start_time') ??
0,
'to_time': _numPayload(item.payload, 'to_time') ??
_numPayload(item.payload, 'end_time') ??
item.dataTime,
'create_time': createTime,
};
}
... ... @@ -179,10 +335,9 @@ CREATE TABLE IF NOT EXISTS $sleepDataTable (
int createTime,
) {
return {
'data_type': item.dataType,
'data_type': _numPayload(item.payload, '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,
};
}
... ... @@ -191,12 +346,120 @@ CREATE TABLE IF NOT EXISTS $sleepDataTable (
if (dataType == OhosHealthRawDataType.activitySummary) {
return HuaweiHealthDataType.activity.dataType;
}
if (dataType == OhosHealthRawDataType.workout) {
return HuaweiHealthDataType.exerciseDuration.dataType;
}
return dataType;
}
String _rawDataTable(int dataType) => dataType < 0
? '${rawDataTable}_m${-dataType}'
: '${rawDataTable}_$dataType';
OhosHealthRawDataItem _rawItemFromRow(
Map<String, Object?> row,
int dataType,
) {
final dataTime = row['time'] as int;
return OhosHealthRawDataItem(
dataType: dataType,
dataTime: dataTime,
payload: <String, Object?>{
'time': dataTime,
'value': row['value'],
'is_asleep': row['is_asleep'],
},
);
}
OhosHealthRawDataItem _sleepItemFromRow(Map<String, Object?> row) {
final toTime = row['to_time'] as int;
final dataType =
(row['data_type'] as int?) ?? OhosHealthRawDataType.sleepAnalysis;
return OhosHealthRawDataItem(
dataType: dataType,
dataTime: toTime,
payload: <String, Object?>{
'data_type': dataType,
'from_time': row['from_time'],
'to_time': toTime,
},
);
}
OhosHealthRawDataItem _intervalItemFromRow(
Map<String, Object?> row,
int dataType,
) {
final toTime = row['to_time'] as int;
return OhosHealthRawDataItem(
dataType: dataType,
dataTime: toTime,
payload: <String, Object?>{
'activity_type': row['activity_type'],
'from_time': row['from_time'],
'to_time': toTime,
},
);
}
bool _shouldStoreItem({
required int dataType,
required OhosHealthRawDataItem item,
required int? latestTime,
}) {
if (latestTime == null) return true;
final keyTime = _dedupeKeyTime(dataType: dataType, item: item);
if (_isDailyDataType(dataType)) {
return keyTime >= _startOfLocalDay(latestTime) - Duration.secondsPerDay;
}
return keyTime > latestTime;
}
int _dedupeKeyTime({
required int dataType,
required OhosHealthRawDataItem item,
}) {
if (dataType == OhosHealthRawDataType.sleepAnalysis ||
dataType == OhosHealthRawDataType.workout) {
return _numPayload(item.payload, 'from_time') ??
_numPayload(item.payload, 'start_time') ??
item.dataTime;
}
return _numPayload(item.payload, 'time') ?? item.dataTime;
}
bool _isDailyDataType(int dataType) {
final storedDataType = _storedHealthDataType(dataType);
return _isDailyHealthDataType(storedDataType);
}
bool _dailyItemNeedsRefresh({
required int dataType,
required OhosHealthRawDataItem item,
required int? latestTime,
}) {
if (!_isDailyDataType(dataType) || latestTime == null) return false;
final keyTime = _dedupeKeyTime(dataType: dataType, item: item);
final refreshStart = _startOfLocalDay(latestTime);
final todayStart = _startOfLocalDay(
_nowProvider().millisecondsSinceEpoch ~/ 1000,
);
final tomorrowStart = todayStart + Duration.secondsPerDay;
return keyTime >= refreshStart && keyTime < tomorrowStart;
}
bool _isDailyHealthDataType(int dataType) {
return dataType == HuaweiHealthDataType.activity.dataType ||
dataType == HuaweiHealthDataType.exerciseDuration.dataType ||
dataType == HuaweiHealthDataType.standingDuration.dataType ||
dataType == HuaweiHealthDataType.stepCount.dataType;
}
int _startOfLocalDay(int seconds) {
final dateTime = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
return DateTime(dateTime.year, dateTime.month, dateTime.day)
.millisecondsSinceEpoch ~/
1000;
}
int? _numPayload(Map<String, Object?> payload, String key) {
final value = payload[key];
return value is num ? value.toInt() : null;
... ...
import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
import 'package:doublefeel_flutter/data/models/harmony/hm_health_data.dart';
import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart';
import 'package:doublefeel_flutter/data/models/harmony/hm_workout_data.dart';
import 'package:flutter/foundation.dart';
import 'huawei_health_data_type.dart';
... ... @@ -27,12 +29,23 @@ class OhosHealthRawDataSyncService {
_logSink = logSink;
static const int defaultLookbackDays = 183;
static const int heartRateFetchChunkDays = 10;
static const int defaultFetchChunkDays = 30;
static const String logMarker = '[OHOS_HEALTH_RAW_SYNC]';
static final List<int> calculationDataTypes = List<int>.unmodifiable(
<int>[
for (final type in HuaweiHealthDataType.values) type.dataType,
OhosHealthRawDataType.sleepAnalysis,
OhosHealthRawDataType.workout,
],
);
final OhosHealthRawDataRemoteDataSource _remoteDataSource;
final OhosHealthRawDataLocalStore _localStore;
final DateTime Function() _nowProvider;
final void Function(String message)? _logSink;
final Map<String, Future<OhosHealthRawDataSyncResult>> _runningSyncs =
<String, Future<OhosHealthRawDataSyncResult>>{};
Future<OhosHealthRawDataSyncResult> syncRawData({
required int dataType,
... ... @@ -40,24 +53,121 @@ class OhosHealthRawDataSyncService {
int? endTime,
}) async {
final resolvedEndTime = endTime ?? _unixSeconds(_nowProvider());
final resolvedStartTime = startTime ??
await _resolveStartTime(
dataType: dataType,
endTime: resolvedEndTime,
);
final resolvedStartTime = await _resolveStartTime(
dataType: dataType,
requestedStartTime: startTime,
endTime: resolvedEndTime,
);
if (resolvedEndTime < resolvedStartTime) {
throw ArgumentError.value(endTime, 'endTime');
}
final fetchRanges = _splitIntoFetchRanges(
final syncKey = '$dataType:$resolvedStartTime:$resolvedEndTime';
final running = _runningSyncs[syncKey];
if (running != null) {
_log(
'sync_duplicate_join dataType=$dataType '
'startTime=$resolvedStartTime endTime=$resolvedEndTime',
);
return running;
}
final task = _syncResolvedRawData(
dataType: dataType,
startTime: resolvedStartTime,
endTime: resolvedEndTime,
);
_runningSyncs[syncKey] = task;
try {
return await task;
} catch (error, stackTrace) {
_log(
'sync_failed dataType=$dataType '
'startTime=$resolvedStartTime endTime=$resolvedEndTime '
'error=$error stackTrace=$stackTrace',
);
rethrow;
} finally {
if (identical(_runningSyncs[syncKey], task)) {
_runningSyncs.remove(syncKey);
}
}
}
Future<List<OhosHealthRawDataSyncResult>> syncCalculationRawData({
int? startTime,
int? endTime,
List<int>? dataTypes,
}) async {
final resolvedEndTime = endTime ?? _unixSeconds(_nowProvider());
final resolvedDataTypes = dataTypes ?? calculationDataTypes;
_log(
'calculation_sync_start dataTypes=${resolvedDataTypes.join(',')} '
'startTime=${startTime ?? ''} endTime=$resolvedEndTime',
);
final List<OhosHealthRawDataSyncResult> results;
try {
results = await Future.wait(
resolvedDataTypes.map(
(dataType) => syncRawData(
dataType: dataType,
startTime: startTime,
endTime: resolvedEndTime,
),
),
eagerError: true,
);
} catch (error, stackTrace) {
_log(
'calculation_sync_failed dataTypes=${resolvedDataTypes.join(',')} '
'startTime=${startTime ?? ''} endTime=$resolvedEndTime '
'error=$error stackTrace=$stackTrace',
);
rethrow;
}
final storedCount = results.fold<int>(
0,
(sum, result) => sum + result.storedCount,
);
final pageCount = results.fold<int>(
0,
(sum, result) => sum + result.pageCount,
);
_log(
'calculation_sync_finish dataTypes=${resolvedDataTypes.join(',')} '
'pageCount=$pageCount storedCount=$storedCount',
);
return results;
}
Future<List<OhosHealthRawDataItem>> queryRawData({
required int dataType,
required int startTime,
required int endTime,
}) {
return _localStore.queryRawData(
dataType: dataType,
startTime: startTime,
endTime: endTime,
);
}
Future<OhosHealthRawDataSyncResult> _syncResolvedRawData({
required int dataType,
required int startTime,
required int endTime,
}) async {
final fetchRanges = _splitIntoFetchRanges(
dataType: dataType,
startTime: startTime,
endTime: endTime,
);
final fetchChunkDays = _fetchChunkDays(dataType);
_log(
'sync_start dataType=$dataType '
'startTime=$resolvedStartTime endTime=$resolvedEndTime '
'rangeCount=${fetchRanges.length}',
'startTime=$startTime endTime=$endTime '
'chunkDays=$fetchChunkDays rangeCount=${fetchRanges.length}',
);
var pageCount = 0;
... ... @@ -68,6 +178,7 @@ class OhosHealthRawDataSyncService {
_log(
'segment_start dataType=$dataType '
'segment=${rangeIndex + 1}/${fetchRanges.length} '
'chunkDays=$fetchChunkDays '
'startTime=${range.startTime} endTime=${range.endTime} '
'startDate=${_dateKeyFromUnixSeconds(range.startTime)} '
'endDate=${_dateKeyFromUnixSeconds(range.endTime)}',
... ... @@ -90,15 +201,16 @@ class OhosHealthRawDataSyncService {
);
if (page.items.isNotEmpty) {
await _localStore.upsertRawDataBatch(
final pageStoredCount = await _localStore.upsertRawDataBatch(
dataType: dataType,
items: page.items,
);
storedCount += page.items.length;
storedCount += pageStoredCount;
_log(
'page_stored dataType=$dataType '
'segment=${rangeIndex + 1}/${fetchRanges.length} '
'storedItems=${page.items.length} totalStored=$storedCount',
'fetchedItems=${page.items.length} '
'storedItems=$pageStoredCount totalStored=$storedCount',
);
}
... ... @@ -113,15 +225,16 @@ class OhosHealthRawDataSyncService {
_log(
'sync_finish dataType=$dataType '
'startTime=$resolvedStartTime endTime=$resolvedEndTime '
'rangeCount=${fetchRanges.length} pageCount=$pageCount '
'startTime=$startTime endTime=$endTime '
'chunkDays=$fetchChunkDays rangeCount=${fetchRanges.length} '
'pageCount=$pageCount '
'storedCount=$storedCount',
);
return OhosHealthRawDataSyncResult(
dataType: dataType,
startTime: resolvedStartTime,
endTime: resolvedEndTime,
startTime: startTime,
endTime: endTime,
segmentCount: fetchRanges.length,
pageCount: pageCount,
storedCount: storedCount,
... ... @@ -130,35 +243,56 @@ class OhosHealthRawDataSyncService {
Future<int> _resolveStartTime({
required int dataType,
required int? requestedStartTime,
required int endTime,
}) async {
final latestDataTime = await _localStore.latestDataTime(dataType: dataType);
if (latestDataTime != null) {
return latestDataTime;
}
final fallbackStartTime = _unixSeconds(
_nowProvider().subtract(const Duration(days: defaultLookbackDays)),
);
return fallbackStartTime > endTime ? endTime : fallbackStartTime;
final baseStartTime = latestDataTime ??
requestedStartTime ??
_unixSeconds(
_nowProvider().subtract(const Duration(days: defaultLookbackDays)),
);
final dayStartTime = _startOfLocalDay(baseStartTime);
final resolvedStartTime = _isDailyDataType(dataType)
? dayStartTime - Duration.secondsPerDay
: dayStartTime;
return resolvedStartTime > endTime ? endTime : resolvedStartTime;
}
int _unixSeconds(DateTime time) {
return time.millisecondsSinceEpoch ~/ 1000;
}
int _startOfLocalDay(int seconds) {
final dateTime = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
return _unixSeconds(DateTime(dateTime.year, dateTime.month, dateTime.day));
}
bool _isDailyDataType(int dataType) {
return dataType == HuaweiHealthDataType.activity.dataType ||
dataType == HuaweiHealthDataType.exerciseDuration.dataType ||
dataType == HuaweiHealthDataType.standingDuration.dataType ||
dataType == HuaweiHealthDataType.stepCount.dataType ||
dataType == OhosHealthRawDataType.activitySummary;
}
List<OhosHealthRawDataFetchRange> _splitIntoFetchRanges({
required int dataType,
required int startTime,
required int endTime,
}) {
final ranges = <OhosHealthRawDataFetchRange>[];
final fetchChunkDays = _fetchChunkDays(dataType);
var cursor = DateTime.fromMillisecondsSinceEpoch(startTime * 1000);
final end = DateTime.fromMillisecondsSinceEpoch(endTime * 1000);
while (!cursor.isAfter(end)) {
final nextMonthStart = DateTime(cursor.year, cursor.month + 1);
final monthEndTime = nextMonthStart.subtract(const Duration(seconds: 1));
final cursorDateStart = DateTime(cursor.year, cursor.month, cursor.day);
final segmentEndOfDay = cursorDateStart
.add(Duration(days: fetchChunkDays))
.subtract(const Duration(seconds: 1));
final segmentEndDateTime =
monthEndTime.isBefore(end) ? monthEndTime : end;
segmentEndOfDay.isBefore(end) ? segmentEndOfDay : end;
final segmentStartTime = _unixSeconds(cursor);
final segmentEndTime = _unixSeconds(segmentEndDateTime);
ranges.add(
... ... @@ -175,6 +309,12 @@ class OhosHealthRawDataSyncService {
return ranges;
}
int _fetchChunkDays(int dataType) {
return dataType == HealthDataUploadType.heartRate.type
? heartRateFetchChunkDays
: defaultFetchChunkDays;
}
void _log(String message) {
final taggedMessage = '$logMarker $message';
debugPrint(taggedMessage);
... ... @@ -199,10 +339,16 @@ abstract class OhosHealthRawDataRemoteDataSource {
abstract class OhosHealthRawDataLocalStore {
Future<int?> latestDataTime({required int dataType});
Future<void> upsertRawDataBatch({
Future<int> upsertRawDataBatch({
required int dataType,
required List<OhosHealthRawDataItem> items,
});
Future<List<OhosHealthRawDataItem>> queryRawData({
required int dataType,
required int startTime,
required int endTime,
});
}
abstract class OhosHarmonyRawDataClient {
... ... @@ -216,13 +362,22 @@ abstract class OhosHarmonyRawDataClient {
int startDate,
int endDate,
);
Future<AppResult<HmWorkoutData>> getWorkoutData(
int startTime,
int endTime,
);
}
class OhosHarmonyHealthRawDataRemoteDataSource
implements OhosHealthRawDataRemoteDataSource {
const OhosHarmonyHealthRawDataRemoteDataSource(this._client);
OhosHarmonyHealthRawDataRemoteDataSource(
this._client, {
DateTime Function()? nowProvider,
}) : _nowProvider = nowProvider ?? DateTime.now;
final OhosHarmonyRawDataClient _client;
final DateTime Function() _nowProvider;
@override
Future<OhosHealthRawDataPage> fetchRawDataPage({
... ... @@ -232,18 +387,34 @@ class OhosHarmonyHealthRawDataRemoteDataSource
String? pageToken,
}) async {
final startDate = _dateKeyFromUnixSeconds(startTime);
final endDate = _dateKeyFromUnixSeconds(endTime);
final endDate = _endDateKeyFromUnixSeconds(endTime);
if (dataType == OhosHealthRawDataType.sleepAnalysis) {
final result = await _client.getSleepData(startDate, endDate);
return switch (result) {
AppSuccess<HmSleepData>(:final data) => OhosHealthRawDataPage(
items: _sleepItems(data),
items: _intervalItems(
data: data,
fallbackDataType: OhosHealthRawDataType.sleepAnalysis,
),
),
AppFailure<HmSleepData>(:final error) => throw StateError(
'OHOS sleep raw data fetch failed: $error',
),
};
}
if (dataType == OhosHealthRawDataType.workout) {
final result = await _client.getWorkoutData(startTime, endTime);
return switch (result) {
AppSuccess<HmWorkoutData>(:final data) => OhosHealthRawDataPage(
items: _workoutItems(
data: data,
),
),
AppFailure<HmWorkoutData>(:final error) => throw StateError(
'OHOS workout raw data fetch failed: $error',
),
};
}
final huaweiDataType = _resolveHuaweiDataType(dataType);
final result = await _client.getHealthData(
... ... @@ -268,9 +439,6 @@ class OhosHarmonyHealthRawDataRemoteDataSource
if (dataType == OhosHealthRawDataType.activitySummary) {
return HuaweiHealthDataType.activity;
}
if (dataType == OhosHealthRawDataType.workout) {
return HuaweiHealthDataType.exerciseDuration;
}
for (final value in HuaweiHealthDataType.values) {
if (value.dataType == dataType) {
return value;
... ... @@ -295,19 +463,47 @@ class OhosHarmonyHealthRawDataRemoteDataSource
.toList(growable: false);
}
List<OhosHealthRawDataItem> _sleepItems(HmSleepData data) {
List<OhosHealthRawDataItem> _intervalItems({
required HmSleepData data,
required int fallbackDataType,
}) {
return (data.list ?? const <HmSleepDataItem>[])
.where((item) => item.toTime != null)
.map(
(item) => OhosHealthRawDataItem(
dataType:
item.dataType?.toInt() ?? OhosHealthRawDataType.sleepAnalysis,
dataType: item.dataType?.toInt() ?? fallbackDataType,
dataTime: item.toTime!.toInt(),
payload: Map<String, Object?>.from(item.toJson()),
),
)
.toList(growable: false);
}
List<OhosHealthRawDataItem> _workoutItems({
required HmWorkoutData data,
}) {
return (data.list ?? const <HmWorkoutDataItem>[])
.where((item) => item.endTime != null)
.map(
(item) => OhosHealthRawDataItem(
dataType: OhosHealthRawDataType.workout,
dataTime: item.endTime!.toInt(),
payload: Map<String, Object?>.from(item.toJson()),
),
)
.toList(growable: false);
}
int _endDateKeyFromUnixSeconds(int seconds) {
final end = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
final now = _nowProvider();
final today = DateTime(now.year, now.month, now.day);
final endDate = DateTime(end.year, end.month, end.day);
if (endDate == today) {
return _dateKeyFromDateTime(today.add(const Duration(days: 1)));
}
return _dateKeyFromDateTime(endDate);
}
}
class TodoOhosHealthRawDataRemoteDataSource
... ... @@ -337,11 +533,21 @@ class TodoOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
}
@override
Future<void> upsertRawDataBatch({
Future<int> upsertRawDataBatch({
required int dataType,
required List<OhosHealthRawDataItem> items,
}) async {
// TODO: Store one fetched OHOS raw-data page into local database.
return 0;
}
@override
Future<List<OhosHealthRawDataItem>> queryRawData({
required int dataType,
required int startTime,
required int endTime,
}) async {
return const <OhosHealthRawDataItem>[];
}
}
... ... @@ -397,5 +603,9 @@ class OhosHealthRawDataSyncResult {
int _dateKeyFromUnixSeconds(int seconds) {
final dateTime = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
return _dateKeyFromDateTime(dateTime);
}
int _dateKeyFromDateTime(DateTime dateTime) {
return dateTime.year * 10000 + dateTime.month * 100 + dateTime.day;
}
... ...
... ... @@ -38,6 +38,14 @@ class HmSleepDataItem {
num? fromTime;
num? toTime;
/// 0:未知
/// 1:深睡
/// 2:浅睡
/// 3:REM
/// 4:清醒
/// 5:午睡(零星小睡)
/// 6:卧床
/// 7:睡眠(手工)
num? dataType;
Map<String, dynamic> toJson() {
... ...
class HmWorkoutData {
HmWorkoutData({
this.list,
});
HmWorkoutData.fromJson(dynamic json) {
if (json['list'] != null) {
list = <HmWorkoutDataItem>[];
json['list'].forEach((v) {
list?.add(HmWorkoutDataItem.fromJson(v));
});
}
}
List<HmWorkoutDataItem>? list;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
if (list != null) {
map['list'] = list?.map((v) => v.toJson()).toList();
}
return map;
}
}
class HmWorkoutDataItem {
HmWorkoutDataItem({
this.startTime,
this.endTime,
this.activityType,
});
HmWorkoutDataItem.fromJson(dynamic json) {
startTime = json['start_time'];
endTime = json['end_time'];
activityType = json['activity_type'];
}
num? startTime;
num? endTime;
num? activityType;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['start_time'] = startTime;
map['end_time'] = endTime;
map['activity_type'] = activityType;
return map;
}
}
... ...
... ... @@ -6,7 +6,7 @@ packages:
description:
name: _fe_analyzer_shared
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "85.0.0"
analyzer:
... ... @@ -14,7 +14,7 @@ packages:
description:
name: analyzer
sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.7.1"
archive:
... ... @@ -22,7 +22,7 @@ packages:
description:
name: archive
sha256: ace891da0862b0e4cabbb064ee3fd87b2728b898949fdb366d83fe98342c9f19
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.2.0"
args:
... ... @@ -30,7 +30,7 @@ packages:
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.7.0"
async:
... ... @@ -38,7 +38,7 @@ packages:
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.11.0"
boolean_selector:
... ... @@ -46,7 +46,7 @@ packages:
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
build:
... ... @@ -54,7 +54,7 @@ packages:
description:
name: build
sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.2"
build_config:
... ... @@ -62,7 +62,7 @@ packages:
description:
name: build_config
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.2"
build_daemon:
... ... @@ -70,7 +70,7 @@ packages:
description:
name: build_daemon
sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.4"
build_resolvers:
... ... @@ -78,7 +78,7 @@ packages:
description:
name: build_resolvers
sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.4"
build_runner:
... ... @@ -86,7 +86,7 @@ packages:
description:
name: build_runner
sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.15"
build_runner_core:
... ... @@ -94,7 +94,7 @@ packages:
description:
name: build_runner_core
sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.0.0"
built_collection:
... ... @@ -102,7 +102,7 @@ packages:
description:
name: built_collection
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.1.1"
built_value:
... ... @@ -110,7 +110,7 @@ packages:
description:
name: built_value
sha256: "31b24be6615ec7fcf70b3aa5a7469fe35826485e639a16dd7eb83ba30e4cc6a8"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.12.7"
cached_network_image:
... ... @@ -118,7 +118,7 @@ packages:
description:
name: cached_network_image
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.4.1"
cached_network_image_platform_interface:
... ... @@ -126,7 +126,7 @@ packages:
description:
name: cached_network_image_platform_interface
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.1"
cached_network_image_web:
... ... @@ -134,7 +134,7 @@ packages:
description:
name: cached_network_image_web
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.1"
characters:
... ... @@ -142,7 +142,7 @@ packages:
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
checked_yaml:
... ... @@ -150,7 +150,7 @@ packages:
description:
name: checked_yaml
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.3"
clock:
... ... @@ -158,7 +158,7 @@ packages:
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
code_builder:
... ... @@ -166,7 +166,7 @@ packages:
description:
name: code_builder
sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.10.1"
collection:
... ... @@ -174,7 +174,7 @@ packages:
description:
name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.19.0"
convert:
... ... @@ -182,7 +182,7 @@ packages:
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.2"
cross_file:
... ... @@ -190,7 +190,7 @@ packages:
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.4+2"
crypto:
... ... @@ -198,7 +198,7 @@ packages:
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.7"
cupertino_icons:
... ... @@ -206,7 +206,7 @@ packages:
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.8"
dart_style:
... ... @@ -214,7 +214,7 @@ packages:
description:
name: dart_style
sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.1"
dio:
... ... @@ -222,7 +222,7 @@ packages:
description:
name: dio
sha256: "0df44ebba85e503958eb75d07eedd3c86275a58c1d3eda2f2ce8f0a2c3abbb3c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.11.0"
dio_web_adapter:
... ... @@ -230,7 +230,7 @@ packages:
description:
name: dio_web_adapter
sha256: "0786d0b7295a373de356fc0af4f6f1d0ab2844ed31b19dfc5e7556b70e24212c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.1"
equatable:
... ... @@ -238,7 +238,7 @@ packages:
description:
name: equatable
sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0"
fake_async:
... ... @@ -246,7 +246,7 @@ packages:
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.1"
ffi:
... ... @@ -254,7 +254,7 @@ packages:
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.3"
file:
... ... @@ -262,7 +262,7 @@ packages:
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.1"
file_selector_linux:
... ... @@ -270,7 +270,7 @@ packages:
description:
name: file_selector_linux
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.9.3+2"
file_selector_macos:
... ... @@ -278,7 +278,7 @@ packages:
description:
name: file_selector_macos
sha256: "8c9250b2bd2d8d4268e39c82543bacbaca0fda7d29e0728c3c4bbb7c820fd711"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.9.4+3"
file_selector_platform_interface:
... ... @@ -286,7 +286,7 @@ packages:
description:
name: file_selector_platform_interface
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.6.2"
file_selector_windows:
... ... @@ -294,7 +294,7 @@ packages:
description:
name: file_selector_windows
sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.9.3+4"
fixnum:
... ... @@ -302,7 +302,7 @@ packages:
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
fl_chart:
... ... @@ -310,7 +310,7 @@ packages:
description:
name: fl_chart
sha256: "5276944c6ffc975ae796569a826c38a62d2abcf264e26b88fa6f482e107f4237"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.70.2"
flutter:
... ... @@ -323,7 +323,7 @@ packages:
description:
name: flutter_cache_manager
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.4.1"
flutter_lints:
... ... @@ -331,7 +331,7 @@ packages:
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.0"
flutter_localizations:
... ... @@ -344,7 +344,7 @@ packages:
description:
name: flutter_plugin_android_lifecycle
sha256: "6382ce712ff69b0f719640ce957559dde459e55ecd433c767e06d139ddf16cab"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.29"
flutter_test:
... ... @@ -357,7 +357,7 @@ packages:
description:
name: flutter_timezone
sha256: "869677426fde92dbe170fb7d2d4929f2a8343c2f5f62f08b0bb64f908630b073"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.1.0"
flutter_web_plugins:
... ... @@ -379,7 +379,7 @@ packages:
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.0.0"
get:
... ... @@ -387,7 +387,7 @@ packages:
description:
name: get
sha256: "5ed34a7925b85336e15d472cc4cfe7d9ebf4ab8e8b9f688585bf6b50f4c3d79a"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.7.3"
glob:
... ... @@ -395,7 +395,7 @@ packages:
description:
name: glob
sha256: "218aeb56050c714f62a3182775320dfa04602b55074873e24e31bbd39bda96fb"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
graphs:
... ... @@ -403,7 +403,7 @@ packages:
description:
name: graphs
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.2"
http:
... ... @@ -411,7 +411,7 @@ packages:
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.6.0"
http_multi_server:
... ... @@ -419,7 +419,7 @@ packages:
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.2"
http_parser:
... ... @@ -427,7 +427,7 @@ packages:
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.1.2"
image_cropper:
... ... @@ -471,7 +471,7 @@ packages:
description:
name: image_picker_android
sha256: e83b2b05141469c5e19d77e1dfa11096b6b1567d09065b2265d7c6904560050c
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.8.13"
image_picker_for_web:
... ... @@ -479,7 +479,7 @@ packages:
description:
name: image_picker_for_web
sha256: "40c2a6a0da15556dc0f8e38a3246064a971a9f512386c3339b89f76db87269b6"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.0"
image_picker_ios:
... ... @@ -487,7 +487,7 @@ packages:
description:
name: image_picker_ios
sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.8.13"
image_picker_linux:
... ... @@ -495,7 +495,7 @@ packages:
description:
name: image_picker_linux
sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.2"
image_picker_macos:
... ... @@ -503,7 +503,7 @@ packages:
description:
name: image_picker_macos
sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.2"
image_picker_ohos:
... ... @@ -520,7 +520,7 @@ packages:
description:
name: image_picker_platform_interface
sha256: "9f143b0dba3e459553209e20cc425c9801af48e6dfa4f01a0fcf927be3f41665"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.11.0"
image_picker_windows:
... ... @@ -528,7 +528,7 @@ packages:
description:
name: image_picker_windows
sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.2"
intl:
... ... @@ -536,7 +536,7 @@ packages:
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.19.0"
io:
... ... @@ -544,7 +544,7 @@ packages:
description:
name: io
sha256: "2635216ca6a737e60de577ffa1a48a0bec76ca8a62917cfc1bb88c14c570646f"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
js:
... ... @@ -552,7 +552,7 @@ packages:
description:
name: js
sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.1"
json_annotation:
... ... @@ -560,7 +560,7 @@ packages:
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.9.0"
leak_tracker:
... ... @@ -568,7 +568,7 @@ packages:
description:
name: leak_tracker
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "10.0.7"
leak_tracker_flutter_testing:
... ... @@ -576,7 +576,7 @@ packages:
description:
name: leak_tracker_flutter_testing
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.8"
leak_tracker_testing:
... ... @@ -584,7 +584,7 @@ packages:
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.1"
lints:
... ... @@ -592,7 +592,7 @@ packages:
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.1.1"
logger:
... ... @@ -600,7 +600,7 @@ packages:
description:
name: logger
sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.7.0"
logging:
... ... @@ -608,7 +608,7 @@ packages:
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
lottie:
... ... @@ -616,7 +616,7 @@ packages:
description:
name: lottie
sha256: c5fa04a80a620066c15cf19cc44773e19e9b38e989ff23ea32e5903ef1015950
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.3.1"
matcher:
... ... @@ -624,7 +624,7 @@ packages:
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.16+1"
material_color_utilities:
... ... @@ -632,7 +632,7 @@ packages:
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.11.1"
meta:
... ... @@ -640,7 +640,7 @@ packages:
description:
name: meta
sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.19.0"
mime:
... ... @@ -648,7 +648,7 @@ packages:
description:
name: mime
sha256: bd47de35f07e27267e69c8c8b22edf9473bfee170a60d60fcc93730c5144b7f6
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0"
octo_image:
... ... @@ -656,7 +656,7 @@ packages:
description:
name: octo_image
sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.0"
package_config:
... ... @@ -664,7 +664,7 @@ packages:
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.0"
path:
... ... @@ -672,23 +672,24 @@ packages:
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.0"
path_provider:
dependency: "direct main"
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path: "packages/path_provider/path_provider"
ref: master
resolved-ref: d25cb31f29abf5b1d67364c184b3f354b5ad1928
url: "https://gitcode.com/openharmony-sig/flutter_packages.git"
source: git
version: "2.1.0"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.17"
path_provider_foundation:
... ... @@ -696,7 +697,7 @@ packages:
description:
name: path_provider_foundation
sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
path_provider_linux:
... ... @@ -704,15 +705,24 @@ packages:
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.1"
path_provider_ohos:
dependency: transitive
description:
path: "packages/path_provider/path_provider_ohos"
ref: HEAD
resolved-ref: d25cb31f29abf5b1d67364c184b3f354b5ad1928
url: "https://gitcode.com/openharmony-tpc/flutter_packages.git"
source: git
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
path_provider_windows:
... ... @@ -720,7 +730,7 @@ packages:
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.0"
permission_handler:
... ... @@ -728,7 +738,7 @@ packages:
description:
name: permission_handler
sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "11.4.0"
permission_handler_android:
... ... @@ -736,7 +746,7 @@ packages:
description:
name: permission_handler_android
sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "12.1.0"
permission_handler_apple:
... ... @@ -744,7 +754,7 @@ packages:
description:
name: permission_handler_apple
sha256: f49cb15a064ea9d974fc7fbb302099353b7b170d07284e86e264561579e5bcf8
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "9.6.1"
permission_handler_html:
... ... @@ -752,7 +762,7 @@ packages:
description:
name: permission_handler_html
sha256: "6ea98b3f17f60d3b527f2647ed2ab4dc0f6bfe25b22cb1c363f5d8f62252f6ac"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.4+1"
permission_handler_ohos:
... ... @@ -769,7 +779,7 @@ packages:
description:
name: permission_handler_platform_interface
sha256: a5c8a97ecf5616112a5b16d4b8e9ec0e5ae90ef63ac69c0d7b8ae240be760b23
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.4.0"
permission_handler_windows:
... ... @@ -777,7 +787,7 @@ packages:
description:
name: permission_handler_windows
sha256: caeae01858a0a7d2df67a445ac98e1ad95e55a0e77c73044f4e9b1c8c2289cbd
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.2"
pigeon:
... ... @@ -794,7 +804,7 @@ packages:
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.6"
plugin_platform_interface:
... ... @@ -802,7 +812,7 @@ packages:
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.8"
pool:
... ... @@ -810,7 +820,7 @@ packages:
description:
name: pool
sha256: "4177f68c237ea2128d1bee66ac17b2ce05ba3dbaafcbdd54c5d40a39d0b6b11c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.5.3"
posix:
... ... @@ -818,7 +828,7 @@ packages:
description:
name: posix
sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.5.2"
pretty_dio_logger:
... ... @@ -826,7 +836,7 @@ packages:
description:
name: pretty_dio_logger
sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.0"
pub_semver:
... ... @@ -834,7 +844,7 @@ packages:
description:
name: pub_semver
sha256: "261236774e8b1d69cfc6b9eabbc96c40f25e7a2d6b171f3385d4f65d5734fb24"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.1"
pubspec_parse:
... ... @@ -842,7 +852,7 @@ packages:
description:
name: pubspec_parse
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.5.0"
rxdart:
... ... @@ -850,7 +860,7 @@ packages:
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.28.0"
share_plus:
... ... @@ -885,7 +895,7 @@ packages:
description:
name: shared_preferences_android
sha256: "5bcf0772a761b04f8c6bf814721713de6f3e5d9d89caf8d3fe031b02a342379e"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.11"
shared_preferences_foundation:
... ... @@ -893,7 +903,7 @@ packages:
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.4"
shared_preferences_linux:
... ... @@ -901,7 +911,7 @@ packages:
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
shared_preferences_ohos:
... ... @@ -918,7 +928,7 @@ packages:
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
shared_preferences_web:
... ... @@ -926,7 +936,7 @@ packages:
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.3"
shared_preferences_windows:
... ... @@ -934,7 +944,7 @@ packages:
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
shelf:
... ... @@ -942,7 +952,7 @@ packages:
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.2"
shelf_web_socket:
... ... @@ -950,7 +960,7 @@ packages:
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.0"
simple_gesture_detector:
... ... @@ -958,7 +968,7 @@ packages:
description:
name: simple_gesture_detector
sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.1"
sky_engine:
... ... @@ -971,7 +981,7 @@ packages:
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.0"
sqflite:
... ... @@ -988,7 +998,7 @@ packages:
description:
name: sqflite_android
sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.0"
sqflite_common:
... ... @@ -996,7 +1006,7 @@ packages:
description:
name: sqflite_common
sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.5.4+6"
sqflite_darwin:
... ... @@ -1004,7 +1014,7 @@ packages:
description:
name: sqflite_darwin
sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1+1"
sqflite_ohos:
... ... @@ -1021,7 +1031,7 @@ packages:
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.0"
stack_trace:
... ... @@ -1029,7 +1039,7 @@ packages:
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.12.0"
stream_channel:
... ... @@ -1037,7 +1047,7 @@ packages:
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
stream_transform:
... ... @@ -1045,7 +1055,7 @@ packages:
description:
name: stream_transform
sha256: a00e5f18bffc764f923e7dec1038527f7fe7a1791361a7117f0358193f13d53a
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
string_scanner:
... ... @@ -1053,7 +1063,7 @@ packages:
description:
name: string_scanner
sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.0"
synchronized:
... ... @@ -1061,7 +1071,7 @@ packages:
description:
name: synchronized
sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.3.0+3"
table_calendar:
... ... @@ -1069,7 +1079,7 @@ packages:
description:
name: table_calendar
sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.3"
term_glyph:
... ... @@ -1077,7 +1087,7 @@ packages:
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.1"
test_api:
... ... @@ -1085,7 +1095,7 @@ packages:
description:
name: test_api
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.7.3"
thinking_analytics:
... ... @@ -1093,7 +1103,7 @@ packages:
description:
name: thinking_analytics
sha256: b01cac0b5482e71c1d75c44c77d27f427662cc65a77b7bc3c8b49617d7a01e02
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.3.3"
timing:
... ... @@ -1101,7 +1111,7 @@ packages:
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.2"
typed_data:
... ... @@ -1109,7 +1119,7 @@ packages:
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.4.0"
url_launcher_linux:
... ... @@ -1117,7 +1127,7 @@ packages:
description:
name: url_launcher_linux
sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.2.1"
url_launcher_platform_interface:
... ... @@ -1125,7 +1135,7 @@ packages:
description:
name: url_launcher_platform_interface
sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.2"
url_launcher_web:
... ... @@ -1133,7 +1143,7 @@ packages:
description:
name: url_launcher_web
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.1"
url_launcher_windows:
... ... @@ -1141,7 +1151,7 @@ packages:
description:
name: url_launcher_windows
sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.4"
uuid:
... ... @@ -1149,7 +1159,7 @@ packages:
description:
name: uuid
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.6.0"
vector_math:
... ... @@ -1157,7 +1167,7 @@ packages:
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.4"
video_thumbnail:
... ... @@ -1165,7 +1175,7 @@ packages:
description:
name: video_thumbnail
sha256: "181a0c205b353918954a881f53a3441476b9e301641688a581e0c13f00dc588b"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.5.6"
vm_service:
... ... @@ -1173,7 +1183,7 @@ packages:
description:
name: vm_service
sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "14.3.0"
watcher:
... ... @@ -1181,7 +1191,7 @@ packages:
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.1"
web:
... ... @@ -1189,7 +1199,7 @@ packages:
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
web_socket:
... ... @@ -1197,7 +1207,7 @@ packages:
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.1"
web_socket_channel:
... ... @@ -1205,7 +1215,7 @@ packages:
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.3"
webview_flutter:
... ... @@ -1258,7 +1268,7 @@ packages:
description:
name: win32
sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.10.1"
xdg_directories:
... ... @@ -1266,7 +1276,7 @@ packages:
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
yaml:
... ... @@ -1274,7 +1284,7 @@ packages:
description:
name: yaml
sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea
url: "https://pub.dev"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.1.4"
sdks:
... ...
... ... @@ -64,9 +64,9 @@ dependencies:
# path_provider: 2.1.5
path_provider:
git:
url: https://gitcode.com/openharmony-tpc/flutter_packages.git
url: https://gitcode.com/openharmony-sig/flutter_packages.git
path: packages/path_provider/path_provider
ref: br_path_provider-v2.1.5_ohos
ref: master
thinking_analytics: ^3.3.2
flutter_localizations:
sdk: flutter
... ... @@ -81,11 +81,14 @@ dependencies:
flutter_timezone: ^5.1.0
# share_plus (git) 间接依赖 path_provider 的 git 版本,
# 与 cached_network_image → flutter_cache_manager 依赖的 hosted 版本冲突。
# 强制使用 hosted 版本统一来源。
# share_plus (git) 间接依赖 openharmony-sig/flutter_packages.git@master 的
# path_provider,direct dependency 也保持同源,避免 pub source 冲突。
dependency_overrides:
path_provider: 2.1.5
path_provider:
git:
url: https://gitcode.com/openharmony-sig/flutter_packages.git
path: packages/path_provider/path_provider
ref: master
# image_cropper_platform_interface: 7.1.0
# Override meta to satisfy flutter_timezone 5.1.0 (requires ^1.16.0)
# while the ohos flutter_test bundle pins 1.15.0.
... ...
import 'dart:async';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ohos/huawei_health_data_type.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/platform_ohos/ohos_health_raw_data_sync_service.dart';
import 'package:doublefeel_flutter/data/models/harmony/hm_health_data.dart';
import 'package:doublefeel_flutter/data/models/harmony/hm_sleep_data.dart';
import 'package:doublefeel_flutter/data/models/harmony/hm_workout_data.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
... ... @@ -38,13 +41,14 @@ void main() {
final result = await service.syncRawData(dataType: 2, endTime: 2000);
expect(result.startTime, 1000);
final expectedStart = _unixSeconds(DateTime(1970));
expect(result.startTime, expectedStart);
expect(result.endTime, 2000);
expect(result.segmentCount, 1);
expect(result.pageCount, 2);
expect(result.storedCount, 2);
expect(remote.calls.map((e) => e.pageToken), [null, 'next']);
expect(remote.calls.every((e) => e.startTime == 1000), isTrue);
expect(remote.calls.every((e) => e.startTime == expectedStart), isTrue);
expect(local.storedBatches.map((e) => e.length), [1, 1]);
});
... ... @@ -63,14 +67,14 @@ void main() {
final result = await service.syncRawData(dataType: 7);
final expectedStart = now
.subtract(
const Duration(
days: OhosHealthRawDataSyncService.defaultLookbackDays,
),
)
.millisecondsSinceEpoch ~/
1000;
final fallback = now.subtract(
const Duration(
days: OhosHealthRawDataSyncService.defaultLookbackDays,
),
);
final expectedStart =
_unixSeconds(DateTime(fallback.year, fallback.month, fallback.day)) -
Duration.secondsPerDay;
final expectedEnd = now.millisecondsSinceEpoch ~/ 1000;
expect(result.startTime, expectedStart);
... ... @@ -81,7 +85,7 @@ void main() {
expect(local.storedBatches, isEmpty);
});
test('syncRawData splits cross-month ranges and writes marked logs',
test('syncRawData splits non-heart-rate ranges every 30 days and logs marker',
() async {
final start = _unixSeconds(DateTime(2026, 1, 30, 10));
final end = _unixSeconds(DateTime(2026, 3, 2, 9));
... ... @@ -104,12 +108,74 @@ void main() {
),
],
),
]);
final local = _FakeOhosHealthRawDataLocalStore();
final logs = <String>[];
final service = OhosHealthRawDataSyncService(
remoteDataSource: remote,
localStore: local,
logSink: logs.add,
);
final result = await service.syncRawData(
dataType: 1,
startTime: start,
endTime: end,
);
expect(result.segmentCount, 2);
expect(result.pageCount, 2);
expect(result.storedCount, 2);
expect(remote.calls.map((e) => _dateKey(e.startTime)), [
20260130,
20260301,
]);
expect(remote.calls.map((e) => _dateKey(e.endTime)), [
20260228,
20260302,
]);
expect(
logs,
contains(
contains(OhosHealthRawDataSyncService.logMarker),
),
);
expect(
logs,
contains(contains('segment_start dataType=1 segment=1/2 chunkDays=30')),
);
expect(logs, contains(contains('sync_finish dataType=1')));
expect(logs, contains(contains('chunkDays=30 rangeCount=2')));
});
test('syncRawData splits heart-rate ranges every 10 days', () async {
final start = _unixSeconds(DateTime(2026, 1, 30, 10));
final end = _unixSeconds(DateTime(2026, 2, 22, 9));
final remote = _FakeOhosHealthRawDataRemoteDataSource([
const OhosHealthRawDataPage(
items: [
OhosHealthRawDataItem(
dataType: 2,
dataTime: 1,
payload: {'value': 70},
),
],
),
const OhosHealthRawDataPage(
items: [
OhosHealthRawDataItem(
dataType: 1,
dataType: 2,
dataTime: 2,
payload: {'value': 71},
),
],
),
const OhosHealthRawDataPage(
items: [
OhosHealthRawDataItem(
dataType: 2,
dataTime: 3,
payload: {'value': 47},
payload: {'value': 72},
),
],
),
... ... @@ -123,32 +189,133 @@ void main() {
);
final result = await service.syncRawData(
dataType: 1,
dataType: HuaweiHealthDataType.heartRate.dataType,
startTime: start,
endTime: end,
);
expect(result.segmentCount, 3);
expect(result.pageCount, 3);
expect(result.storedCount, 3);
expect(remote.calls.map((e) => _dateKey(e.startTime)), [
20260130,
20260201,
20260301,
20260209,
20260219,
]);
expect(remote.calls.map((e) => _dateKey(e.endTime)), [
20260131,
20260228,
20260302,
20260208,
20260218,
20260222,
]);
expect(
logs,
contains(
contains(OhosHealthRawDataSyncService.logMarker),
),
contains(contains('segment_start dataType=2 segment=1/3 chunkDays=10')),
);
expect(logs, contains(contains('segment_start dataType=1 segment=1/3')));
expect(logs, contains(contains('sync_finish dataType=1')));
});
test('syncRawData joins duplicate in-flight syncs', () async {
final gate = Completer<void>();
final remote = _FakeOhosHealthRawDataRemoteDataSource(
[
const OhosHealthRawDataPage(
items: [
OhosHealthRawDataItem(
dataType: 1,
dataTime: 1001,
payload: {'value': 45},
),
],
),
],
gate: gate,
);
final local = _FakeOhosHealthRawDataLocalStore();
final logs = <String>[];
final service = OhosHealthRawDataSyncService(
remoteDataSource: remote,
localStore: local,
logSink: logs.add,
);
final first = service.syncRawData(
dataType: 1,
startTime: 1000,
endTime: 2000,
);
final second = service.syncRawData(
dataType: 1,
startTime: 1000,
endTime: 2000,
);
await Future<void>.delayed(Duration.zero);
expect(remote.calls, hasLength(1));
expect(logs, contains(contains('sync_duplicate_join dataType=1')));
gate.complete();
final results = await Future.wait([first, second]);
expect(results.first.storedCount, 1);
expect(results.last.storedCount, 1);
expect(remote.calls, hasLength(1));
expect(local.storedBatches, hasLength(1));
});
test('syncRawData fails immediately when a fetch page fails', () async {
final remote = _FakeOhosHealthRawDataRemoteDataSource(
const <OhosHealthRawDataPage>[],
failingDataTypes: {1},
);
final local = _FakeOhosHealthRawDataLocalStore();
final logs = <String>[];
final service = OhosHealthRawDataSyncService(
remoteDataSource: remote,
localStore: local,
logSink: logs.add,
);
await expectLater(
service.syncRawData(dataType: 1, startTime: 1000, endTime: 2000),
throwsA(isA<StateError>()),
);
expect(local.storedBatches, isEmpty);
expect(logs, contains(contains('sync_failed dataType=1')));
});
test('syncCalculationRawData syncs every calculation dependency type',
() async {
final remote = _FakeOhosHealthRawDataRemoteDataSource([]);
final local = _FakeOhosHealthRawDataLocalStore();
final logs = <String>[];
final service = OhosHealthRawDataSyncService(
remoteDataSource: remote,
localStore: local,
logSink: logs.add,
);
final results = await service.syncCalculationRawData(
startTime: 1000,
endTime: 2000,
);
expect(
results.map((result) => result.dataType),
[
for (final type in HuaweiHealthDataType.values) type.dataType,
OhosHealthRawDataType.sleepAnalysis,
OhosHealthRawDataType.workout,
],
);
expect(
remote.calls.map((call) => call.dataType).toSet(),
{
for (final type in HuaweiHealthDataType.values) type.dataType,
OhosHealthRawDataType.sleepAnalysis,
OhosHealthRawDataType.workout,
},
);
expect(logs, contains(contains('calculation_sync_start')));
expect(logs, contains(contains('calculation_sync_finish')));
});
test('Harmony remote data source calls getHealthData and converts items',
... ... @@ -169,7 +336,10 @@ void main() {
],
),
);
final remote = OhosHarmonyHealthRawDataRemoteDataSource(client);
final remote = OhosHarmonyHealthRawDataRemoteDataSource(
client,
nowProvider: () => DateTime(2026, 9, 1),
);
final page = await remote.fetchRawDataPage(
dataType: HuaweiHealthDataType.hrv.dataType,
... ... @@ -187,6 +357,24 @@ void main() {
expect(page.items.single.payload['value'], 53);
});
test('Harmony remote data source uses tomorrow as endDate for today data',
() async {
final client = _FakeOhosHarmonyRawDataClient();
final remote = OhosHarmonyHealthRawDataRemoteDataSource(
client,
nowProvider: () => DateTime(2026, 8, 31, 10),
);
await remote.fetchRawDataPage(
dataType: HuaweiHealthDataType.hrv.dataType,
startTime: _unixSeconds(DateTime(2026, 8, 30)),
endTime: _unixSeconds(DateTime(2026, 8, 31, 12)),
);
expect(client.healthCalls.single.startDate, 20260830);
expect(client.healthCalls.single.endDate, 20260901);
});
test('Harmony remote data source calls getSleepData and converts items',
() async {
final client = _FakeOhosHarmonyRawDataClient(
... ... @@ -218,13 +406,52 @@ void main() {
expect(page.items.single.dataTime, 1787870000);
expect(page.items.single.payload['from_time'], 1787850000);
});
test('Harmony remote data source calls dedicated workout API', () async {
final client = _FakeOhosHarmonyRawDataClient(
workoutData: HmWorkoutData(
list: [
HmWorkoutDataItem(
startTime: 1787850000,
endTime: 1787870000,
activityType: 90,
),
],
),
);
final remote = OhosHarmonyHealthRawDataRemoteDataSource(client);
final startTime = _unixSeconds(DateTime(2026, 8));
final endTime = _unixSeconds(DateTime(2026, 8, 2, 23, 59, 59));
final page = await remote.fetchRawDataPage(
dataType: OhosHealthRawDataType.workout,
startTime: startTime,
endTime: endTime,
);
expect(client.healthCalls, isEmpty);
expect(client.workoutCalls.single.startDate, startTime);
expect(client.workoutCalls.single.endDate, endTime);
expect(page.items.single.dataType, OhosHealthRawDataType.workout);
expect(page.items.single.dataTime, 1787870000);
expect(page.items.single.payload['start_time'], 1787850000);
expect(page.items.single.payload['end_time'], 1787870000);
expect(page.items.single.payload['activity_type'], 90);
});
}
class _FakeOhosHealthRawDataRemoteDataSource
implements OhosHealthRawDataRemoteDataSource {
_FakeOhosHealthRawDataRemoteDataSource(this._pages);
_FakeOhosHealthRawDataRemoteDataSource(
this._pages, {
Completer<void>? gate,
Set<int> failingDataTypes = const <int>{},
}) : _gate = gate,
_failingDataTypes = failingDataTypes;
final List<OhosHealthRawDataPage> _pages;
final Completer<void>? _gate;
final Set<int> _failingDataTypes;
final List<_FetchCall> calls = <_FetchCall>[];
@override
... ... @@ -242,6 +469,12 @@ class _FakeOhosHealthRawDataRemoteDataSource
pageToken: pageToken,
),
);
if (_gate != null) {
await _gate.future;
}
if (_failingDataTypes.contains(dataType)) {
throw StateError('failed dataType=$dataType');
}
final pageIndex = calls.length - 1;
if (pageIndex >= _pages.length) {
return const OhosHealthRawDataPage(items: <OhosHealthRawDataItem>[]);
... ... @@ -264,11 +497,29 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
}
@override
Future<void> upsertRawDataBatch({
Future<int> upsertRawDataBatch({
required int dataType,
required List<OhosHealthRawDataItem> items,
}) async {
storedBatches.add(items);
return items.length;
}
@override
Future<List<OhosHealthRawDataItem>> queryRawData({
required int dataType,
required int startTime,
required int endTime,
}) async {
return storedBatches
.expand((batch) => batch)
.where(
(item) =>
item.dataType == dataType &&
item.dataTime >= startTime &&
item.dataTime <= endTime,
)
.toList(growable: false);
}
}
... ... @@ -290,13 +541,17 @@ class _FakeOhosHarmonyRawDataClient implements OhosHarmonyRawDataClient {
_FakeOhosHarmonyRawDataClient({
HmHealthData? healthData,
HmSleepData? sleepData,
HmWorkoutData? workoutData,
}) : _healthData = healthData ?? HmHealthData(),
_sleepData = sleepData ?? HmSleepData();
_sleepData = sleepData ?? HmSleepData(),
_workoutData = workoutData ?? HmWorkoutData();
final HmHealthData _healthData;
final HmSleepData _sleepData;
final HmWorkoutData _workoutData;
final List<_HealthCall> healthCalls = <_HealthCall>[];
final List<_SleepCall> sleepCalls = <_SleepCall>[];
final List<_SleepCall> workoutCalls = <_SleepCall>[];
@override
Future<AppResult<HmHealthData>> getHealthData(
... ... @@ -327,6 +582,20 @@ class _FakeOhosHarmonyRawDataClient implements OhosHarmonyRawDataClient {
);
return AppSuccess(_sleepData);
}
@override
Future<AppResult<HmWorkoutData>> getWorkoutData(
int startDate,
int endDate,
) async {
workoutCalls.add(
_SleepCall(
startDate: startDate,
endDate: endDate,
),
);
return AppSuccess(_workoutData);
}
}
class _HealthCall {
... ...