|
|
|
import 'package:doublefeel_flutter/core/logging/app_logger.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:flutter/foundation.dart';
|
|
|
|
|
|
|
|
import 'huawei_health_data_type.dart';
|
|
|
|
|
|
|
|
class OhosHealthRawDataType {
|
|
|
|
const OhosHealthRawDataType._();
|
|
|
|
|
|
...
|
...
|
@@ -11,16 +19,20 @@ class OhosHealthRawDataSyncService { |
|
|
|
OhosHealthRawDataRemoteDataSource? remoteDataSource,
|
|
|
|
OhosHealthRawDataLocalStore? localStore,
|
|
|
|
DateTime Function()? nowProvider,
|
|
|
|
void Function(String message)? logSink,
|
|
|
|
}) : _remoteDataSource =
|
|
|
|
remoteDataSource ?? const TodoOhosHealthRawDataRemoteDataSource(),
|
|
|
|
_localStore = localStore ?? const TodoOhosHealthRawDataLocalStore(),
|
|
|
|
_nowProvider = nowProvider ?? DateTime.now;
|
|
|
|
_nowProvider = nowProvider ?? DateTime.now,
|
|
|
|
_logSink = logSink;
|
|
|
|
|
|
|
|
static const int defaultLookbackDays = 183;
|
|
|
|
static const String logMarker = '[OHOS_HEALTH_RAW_SYNC]';
|
|
|
|
|
|
|
|
final OhosHealthRawDataRemoteDataSource _remoteDataSource;
|
|
|
|
final OhosHealthRawDataLocalStore _localStore;
|
|
|
|
final DateTime Function() _nowProvider;
|
|
|
|
final void Function(String message)? _logSink;
|
|
|
|
|
|
|
|
Future<OhosHealthRawDataSyncResult> syncRawData({
|
|
|
|
required int dataType,
|
|
...
|
...
|
@@ -38,34 +50,79 @@ class OhosHealthRawDataSyncService { |
|
|
|
throw ArgumentError.value(endTime, 'endTime');
|
|
|
|
}
|
|
|
|
|
|
|
|
String? pageToken;
|
|
|
|
final fetchRanges = _splitIntoFetchRanges(
|
|
|
|
startTime: resolvedStartTime,
|
|
|
|
endTime: resolvedEndTime,
|
|
|
|
);
|
|
|
|
_log(
|
|
|
|
'sync_start dataType=$dataType '
|
|
|
|
'startTime=$resolvedStartTime endTime=$resolvedEndTime '
|
|
|
|
'rangeCount=${fetchRanges.length}',
|
|
|
|
);
|
|
|
|
|
|
|
|
var pageCount = 0;
|
|
|
|
var storedCount = 0;
|
|
|
|
|
|
|
|
do {
|
|
|
|
final page = await _remoteDataSource.fetchRawDataPage(
|
|
|
|
dataType: dataType,
|
|
|
|
startTime: resolvedStartTime,
|
|
|
|
endTime: resolvedEndTime,
|
|
|
|
pageToken: pageToken,
|
|
|
|
for (var rangeIndex = 0; rangeIndex < fetchRanges.length; rangeIndex++) {
|
|
|
|
final range = fetchRanges[rangeIndex];
|
|
|
|
_log(
|
|
|
|
'segment_start dataType=$dataType '
|
|
|
|
'segment=${rangeIndex + 1}/${fetchRanges.length} '
|
|
|
|
'startTime=${range.startTime} endTime=${range.endTime} '
|
|
|
|
'startDate=${_dateKeyFromUnixSeconds(range.startTime)} '
|
|
|
|
'endDate=${_dateKeyFromUnixSeconds(range.endTime)}',
|
|
|
|
);
|
|
|
|
pageCount += 1;
|
|
|
|
|
|
|
|
if (page.items.isNotEmpty) {
|
|
|
|
await _localStore.upsertRawDataBatch(
|
|
|
|
String? pageToken;
|
|
|
|
do {
|
|
|
|
final page = await _remoteDataSource.fetchRawDataPage(
|
|
|
|
dataType: dataType,
|
|
|
|
items: page.items,
|
|
|
|
startTime: range.startTime,
|
|
|
|
endTime: range.endTime,
|
|
|
|
pageToken: pageToken,
|
|
|
|
);
|
|
|
|
pageCount += 1;
|
|
|
|
_log(
|
|
|
|
'page_fetched dataType=$dataType '
|
|
|
|
'segment=${rangeIndex + 1}/${fetchRanges.length} '
|
|
|
|
'page=$pageCount items=${page.items.length} '
|
|
|
|
'nextPageToken=${page.nextPageToken ?? ''}',
|
|
|
|
);
|
|
|
|
storedCount += page.items.length;
|
|
|
|
}
|
|
|
|
|
|
|
|
pageToken = page.nextPageToken;
|
|
|
|
} while (pageToken != null && pageToken.isNotEmpty);
|
|
|
|
if (page.items.isNotEmpty) {
|
|
|
|
await _localStore.upsertRawDataBatch(
|
|
|
|
dataType: dataType,
|
|
|
|
items: page.items,
|
|
|
|
);
|
|
|
|
storedCount += page.items.length;
|
|
|
|
_log(
|
|
|
|
'page_stored dataType=$dataType '
|
|
|
|
'segment=${rangeIndex + 1}/${fetchRanges.length} '
|
|
|
|
'storedItems=${page.items.length} totalStored=$storedCount',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
pageToken = page.nextPageToken;
|
|
|
|
} while (pageToken != null && pageToken.isNotEmpty);
|
|
|
|
|
|
|
|
_log(
|
|
|
|
'segment_finish dataType=$dataType '
|
|
|
|
'segment=${rangeIndex + 1}/${fetchRanges.length}',
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
_log(
|
|
|
|
'sync_finish dataType=$dataType '
|
|
|
|
'startTime=$resolvedStartTime endTime=$resolvedEndTime '
|
|
|
|
'rangeCount=${fetchRanges.length} pageCount=$pageCount '
|
|
|
|
'storedCount=$storedCount',
|
|
|
|
);
|
|
|
|
|
|
|
|
return OhosHealthRawDataSyncResult(
|
|
|
|
dataType: dataType,
|
|
|
|
startTime: resolvedStartTime,
|
|
|
|
endTime: resolvedEndTime,
|
|
|
|
segmentCount: fetchRanges.length,
|
|
|
|
pageCount: pageCount,
|
|
|
|
storedCount: storedCount,
|
|
|
|
);
|
|
...
|
...
|
@@ -88,6 +145,46 @@ class OhosHealthRawDataSyncService { |
|
|
|
int _unixSeconds(DateTime time) {
|
|
|
|
return time.millisecondsSinceEpoch ~/ 1000;
|
|
|
|
}
|
|
|
|
|
|
|
|
List<OhosHealthRawDataFetchRange> _splitIntoFetchRanges({
|
|
|
|
required int startTime,
|
|
|
|
required int endTime,
|
|
|
|
}) {
|
|
|
|
final ranges = <OhosHealthRawDataFetchRange>[];
|
|
|
|
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 segmentEndDateTime =
|
|
|
|
monthEndTime.isBefore(end) ? monthEndTime : end;
|
|
|
|
final segmentStartTime = _unixSeconds(cursor);
|
|
|
|
final segmentEndTime = _unixSeconds(segmentEndDateTime);
|
|
|
|
ranges.add(
|
|
|
|
OhosHealthRawDataFetchRange(
|
|
|
|
startTime: segmentStartTime,
|
|
|
|
endTime: segmentEndTime,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
cursor = DateTime.fromMillisecondsSinceEpoch(
|
|
|
|
(segmentEndTime + 1) * 1000,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return ranges;
|
|
|
|
}
|
|
|
|
|
|
|
|
void _log(String message) {
|
|
|
|
final taggedMessage = '$logMarker $message';
|
|
|
|
debugPrint(taggedMessage);
|
|
|
|
_logSink?.call(taggedMessage);
|
|
|
|
try {
|
|
|
|
AppLogger.i(taggedMessage);
|
|
|
|
} catch (_) {
|
|
|
|
// AppLogger may not be initialized in isolated test/bootstrap contexts.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
abstract class OhosHealthRawDataRemoteDataSource {
|
|
...
|
...
|
@@ -108,6 +205,111 @@ abstract class OhosHealthRawDataLocalStore { |
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
abstract class OhosHarmonyRawDataClient {
|
|
|
|
Future<AppResult<HmHealthData>> getHealthData(
|
|
|
|
HuaweiHealthDataType dataType,
|
|
|
|
int startDate,
|
|
|
|
int endDate,
|
|
|
|
);
|
|
|
|
|
|
|
|
Future<AppResult<HmSleepData>> getSleepData(
|
|
|
|
int startDate,
|
|
|
|
int endDate,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
class OhosHarmonyHealthRawDataRemoteDataSource
|
|
|
|
implements OhosHealthRawDataRemoteDataSource {
|
|
|
|
const OhosHarmonyHealthRawDataRemoteDataSource(this._client);
|
|
|
|
|
|
|
|
final OhosHarmonyRawDataClient _client;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<OhosHealthRawDataPage> fetchRawDataPage({
|
|
|
|
required int dataType,
|
|
|
|
required int startTime,
|
|
|
|
required int endTime,
|
|
|
|
String? pageToken,
|
|
|
|
}) async {
|
|
|
|
final startDate = _dateKeyFromUnixSeconds(startTime);
|
|
|
|
final endDate = _dateKeyFromUnixSeconds(endTime);
|
|
|
|
if (dataType == OhosHealthRawDataType.sleepAnalysis) {
|
|
|
|
final result = await _client.getSleepData(startDate, endDate);
|
|
|
|
return switch (result) {
|
|
|
|
AppSuccess<HmSleepData>(:final data) => OhosHealthRawDataPage(
|
|
|
|
items: _sleepItems(data),
|
|
|
|
),
|
|
|
|
AppFailure<HmSleepData>(:final error) => throw StateError(
|
|
|
|
'OHOS sleep raw data fetch failed: $error',
|
|
|
|
),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
final huaweiDataType = _resolveHuaweiDataType(dataType);
|
|
|
|
final result = await _client.getHealthData(
|
|
|
|
huaweiDataType,
|
|
|
|
startDate,
|
|
|
|
endDate,
|
|
|
|
);
|
|
|
|
return switch (result) {
|
|
|
|
AppSuccess<HmHealthData>(:final data) => OhosHealthRawDataPage(
|
|
|
|
items: _healthItems(
|
|
|
|
dataType: dataType,
|
|
|
|
data: data,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
AppFailure<HmHealthData>(:final error) => throw StateError(
|
|
|
|
'OHOS health raw data fetch failed: $error',
|
|
|
|
),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
HuaweiHealthDataType _resolveHuaweiDataType(int dataType) {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
throw ArgumentError.value(dataType, 'dataType');
|
|
|
|
}
|
|
|
|
|
|
|
|
List<OhosHealthRawDataItem> _healthItems({
|
|
|
|
required int dataType,
|
|
|
|
required HmHealthData data,
|
|
|
|
}) {
|
|
|
|
return (data.list ?? const <HmHealthDataItem>[])
|
|
|
|
.where((item) => item.time != null)
|
|
|
|
.map(
|
|
|
|
(item) => OhosHealthRawDataItem(
|
|
|
|
dataType: item.dataType?.toInt() ?? dataType,
|
|
|
|
dataTime: item.time!.toInt(),
|
|
|
|
payload: Map<String, Object?>.from(item.toJson()),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
.toList(growable: false);
|
|
|
|
}
|
|
|
|
|
|
|
|
List<OhosHealthRawDataItem> _sleepItems(HmSleepData data) {
|
|
|
|
return (data.list ?? const <HmSleepDataItem>[])
|
|
|
|
.where((item) => item.toTime != null)
|
|
|
|
.map(
|
|
|
|
(item) => OhosHealthRawDataItem(
|
|
|
|
dataType:
|
|
|
|
item.dataType?.toInt() ?? OhosHealthRawDataType.sleepAnalysis,
|
|
|
|
dataTime: item.toTime!.toInt(),
|
|
|
|
payload: Map<String, Object?>.from(item.toJson()),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
.toList(growable: false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class TodoOhosHealthRawDataRemoteDataSource
|
|
|
|
implements OhosHealthRawDataRemoteDataSource {
|
|
|
|
const TodoOhosHealthRawDataRemoteDataSource();
|
|
...
|
...
|
@@ -165,11 +367,22 @@ class OhosHealthRawDataItem { |
|
|
|
final Map<String, Object?> payload;
|
|
|
|
}
|
|
|
|
|
|
|
|
class OhosHealthRawDataFetchRange {
|
|
|
|
const OhosHealthRawDataFetchRange({
|
|
|
|
required this.startTime,
|
|
|
|
required this.endTime,
|
|
|
|
});
|
|
|
|
|
|
|
|
final int startTime;
|
|
|
|
final int endTime;
|
|
|
|
}
|
|
|
|
|
|
|
|
class OhosHealthRawDataSyncResult {
|
|
|
|
const OhosHealthRawDataSyncResult({
|
|
|
|
required this.dataType,
|
|
|
|
required this.startTime,
|
|
|
|
required this.endTime,
|
|
|
|
this.segmentCount = 1,
|
|
|
|
required this.pageCount,
|
|
|
|
required this.storedCount,
|
|
|
|
});
|
|
...
|
...
|
@@ -177,6 +390,12 @@ class OhosHealthRawDataSyncResult { |
|
|
|
final int dataType;
|
|
|
|
final int startTime;
|
|
|
|
final int endTime;
|
|
|
|
final int segmentCount;
|
|
|
|
final int pageCount;
|
|
|
|
final int storedCount;
|
|
|
|
}
|
|
|
|
|
|
|
|
int _dateKeyFromUnixSeconds(int seconds) {
|
|
|
|
final dateTime = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
|
|
|
|
return dateTime.year * 10000 + dateTime.month * 100 + dateTime.day;
|
|
|
|
} |
...
|
...
|
|