|
...
|
...
|
@@ -27,8 +27,13 @@ void main() { |
|
|
|
endTime: base - 4 * 60 * 60,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 3,
|
|
|
|
dataType: 2,
|
|
|
|
startTime: base - 4 * 60 * 60,
|
|
|
|
endTime: base - 3 * 60 * 60,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 3,
|
|
|
|
startTime: base - 3 * 60 * 60,
|
|
|
|
endTime: base - 1 * 60 * 60,
|
|
|
|
),
|
|
|
|
],
|
|
...
|
...
|
@@ -54,6 +59,193 @@ void main() { |
|
|
|
expect(store.realtimeStressPoints, hasLength(1));
|
|
|
|
expect(store.dailyStressPoints, hasLength(1));
|
|
|
|
expect(store.sleepResults, hasLength(1));
|
|
|
|
expect(store.sleepResults.single.awakMinutes, 60);
|
|
|
|
expect(store.sleepResults.single.sleepMinutes, 6 * 60);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('OHOS core excludes awake sleep stages from stored sleep duration',
|
|
|
|
() async {
|
|
|
|
final store = _FakeHealthRawStressLocalStore();
|
|
|
|
final service = OHOSHealthRawDataCoreService(
|
|
|
|
rawDataSource: _FakeHealthRawDataSource(
|
|
|
|
pointsByDataType: const {},
|
|
|
|
sleepPoints: [
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 1,
|
|
|
|
startTime: 1788398340,
|
|
|
|
endTime: 1788399300,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 2,
|
|
|
|
startTime: 1788375120,
|
|
|
|
endTime: 1788375480,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 1,
|
|
|
|
startTime: 1788375480,
|
|
|
|
endTime: 1788378420,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 4,
|
|
|
|
startTime: 1788378420,
|
|
|
|
endTime: 1788379200,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 2,
|
|
|
|
startTime: 1788379200,
|
|
|
|
endTime: 1788379380,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 1,
|
|
|
|
startTime: 1788379380,
|
|
|
|
endTime: 1788381300,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 4,
|
|
|
|
startTime: 1788381300,
|
|
|
|
endTime: 1788383100,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 1,
|
|
|
|
startTime: 1788383100,
|
|
|
|
endTime: 1788384660,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 4,
|
|
|
|
startTime: 1788384660,
|
|
|
|
endTime: 1788385020,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 1,
|
|
|
|
startTime: 1788385020,
|
|
|
|
endTime: 1788385740,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 4,
|
|
|
|
startTime: 1788385740,
|
|
|
|
endTime: 1788386460,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 1,
|
|
|
|
startTime: 1788386460,
|
|
|
|
endTime: 1788387480,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 5,
|
|
|
|
startTime: 1788387480,
|
|
|
|
endTime: 1788389640,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 1,
|
|
|
|
startTime: 1788389640,
|
|
|
|
endTime: 1788391620,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 4,
|
|
|
|
startTime: 1788391620,
|
|
|
|
endTime: 1788393660,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 5,
|
|
|
|
startTime: 1788393660,
|
|
|
|
endTime: 1788394320,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 1,
|
|
|
|
startTime: 1788394320,
|
|
|
|
endTime: 1788395760,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 5,
|
|
|
|
startTime: 1788395760,
|
|
|
|
endTime: 1788396300,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 1,
|
|
|
|
startTime: 1788396300,
|
|
|
|
endTime: 1788397680,
|
|
|
|
),
|
|
|
|
HealthKitRawDataPoint(
|
|
|
|
dataType: 2,
|
|
|
|
startTime: 1788397680,
|
|
|
|
endTime: 1788398340,
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
|
|
|
localStore: store,
|
|
|
|
userIdProvider: () => 42,
|
|
|
|
uploadResultsAfterCalculation: false,
|
|
|
|
healthReadAuthorizationChecker: () async => false,
|
|
|
|
);
|
|
|
|
|
|
|
|
final result = await service.syncAndStore(
|
|
|
|
startTime: 1788375120,
|
|
|
|
endTime: 1788399300,
|
|
|
|
readChunkDays: 1,
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(result.sleepResults, hasLength(1));
|
|
|
|
expect(store.sleepResults.single.awakMinutes, 20);
|
|
|
|
expect(store.sleepResults.single.sleepMinutes, 6 * 60 + 23);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('OHOS core recalculates daily stress for synced historical backfill',
|
|
|
|
() async {
|
|
|
|
final backfillDay = DateTime(2026, 8, 30);
|
|
|
|
final backfillBase =
|
|
|
|
backfillDay.add(const Duration(hours: 10)).millisecondsSinceEpoch ~/
|
|
|
|
1000;
|
|
|
|
final latestBase = DateTime(2026, 9, 3, 10).millisecondsSinceEpoch ~/ 1000;
|
|
|
|
final syncLocalStore =
|
|
|
|
_FakeOhosHealthRawDataLocalStore(latestDataTime: latestBase);
|
|
|
|
final remote = _HistoricalBackfillRemoteDataSource(
|
|
|
|
hrvItems: [_rawItem(1, backfillBase, 45)],
|
|
|
|
heartRateItems: [
|
|
|
|
for (var index = 0; index < 10; index++)
|
|
|
|
_rawItem(2, backfillBase + index * 6 * 60, 100),
|
|
|
|
],
|
|
|
|
);
|
|
|
|
final store = _FakeHealthRawStressLocalStore()
|
|
|
|
..hrvStressPoints.add(_existingHrvResult(latestBase))
|
|
|
|
..realtimeStressPoints.add(_existingRealtimeResult(latestBase));
|
|
|
|
final service = OHOSHealthRawDataCoreService(
|
|
|
|
rawDataSource: OhosHealthRawDataSource(
|
|
|
|
syncService: OhosHealthRawDataSyncService(
|
|
|
|
remoteDataSource: remote,
|
|
|
|
localStore: syncLocalStore,
|
|
|
|
),
|
|
|
|
),
|
|
|
|
localStore: store,
|
|
|
|
userIdProvider: () => 42,
|
|
|
|
uploadResultsAfterCalculation: false,
|
|
|
|
healthReadAuthorizationChecker: () async => true,
|
|
|
|
);
|
|
|
|
|
|
|
|
final result = await service.startCoreCaculate(
|
|
|
|
endTime: latestBase + Duration.secondsPerHour,
|
|
|
|
readChunkDays: 1,
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(
|
|
|
|
remote.calls
|
|
|
|
.where((call) => call.dataType == 1 || call.dataType == 2)
|
|
|
|
.map((call) => _dateKey(call.startTime)),
|
|
|
|
everyElement(lessThanOrEqualTo(20260830)),
|
|
|
|
);
|
|
|
|
expect(
|
|
|
|
store.hrvStressPoints.any((point) => point.rawEndTime == backfillBase),
|
|
|
|
isTrue,
|
|
|
|
);
|
|
|
|
expect(
|
|
|
|
store.realtimeStressPoints.any(
|
|
|
|
(point) => _dateKey(point.rawEndTime) == 20260830,
|
|
|
|
),
|
|
|
|
isTrue,
|
|
|
|
);
|
|
|
|
expect(result.dailyStressPoints.map((point) => point.date),
|
|
|
|
contains(20260830));
|
|
|
|
expect(
|
|
|
|
store.dailyStressPoints.map((point) => point.date), contains(20260830));
|
|
|
|
});
|
|
|
|
|
|
|
|
test('OHOS core aborts calculation and shows debug toast when sync fails',
|
|
...
|
...
|
@@ -112,10 +304,63 @@ class _FailingOhosHealthRawDataRemoteDataSource |
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _HistoricalBackfillRemoteDataSource
|
|
|
|
implements OhosHealthRawDataRemoteDataSource {
|
|
|
|
_HistoricalBackfillRemoteDataSource({
|
|
|
|
required this.hrvItems,
|
|
|
|
required this.heartRateItems,
|
|
|
|
});
|
|
|
|
|
|
|
|
final List<OhosHealthRawDataItem> hrvItems;
|
|
|
|
final List<OhosHealthRawDataItem> heartRateItems;
|
|
|
|
final calls = <_RemoteRawCall>[];
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<OhosHealthRawDataPage> fetchRawDataPage({
|
|
|
|
required int dataType,
|
|
|
|
required int startTime,
|
|
|
|
required int endTime,
|
|
|
|
}) async {
|
|
|
|
calls.add(
|
|
|
|
_RemoteRawCall(
|
|
|
|
dataType: dataType,
|
|
|
|
startTime: startTime,
|
|
|
|
endTime: endTime,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
if (dataType == 1) return OhosHealthRawDataPage(items: hrvItems);
|
|
|
|
if (dataType == 2) return OhosHealthRawDataPage(items: heartRateItems);
|
|
|
|
return const OhosHealthRawDataPage(items: <OhosHealthRawDataItem>[]);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<V2ActivityTarget?> fetchActivityGoal() async {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class _RemoteRawCall {
|
|
|
|
const _RemoteRawCall({
|
|
|
|
required this.dataType,
|
|
|
|
required this.startTime,
|
|
|
|
required this.endTime,
|
|
|
|
});
|
|
|
|
|
|
|
|
final int dataType;
|
|
|
|
final int startTime;
|
|
|
|
final int endTime;
|
|
|
|
}
|
|
|
|
|
|
|
|
class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
|
|
|
|
_FakeOhosHealthRawDataLocalStore({int? latestDataTime})
|
|
|
|
: _latestDataTime = latestDataTime;
|
|
|
|
|
|
|
|
final int? _latestDataTime;
|
|
|
|
final storedBatches = <List<OhosHealthRawDataItem>>[];
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<int?> latestDataTime({required int dataType}) async {
|
|
|
|
return null;
|
|
|
|
return _latestDataTime;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
...
|
...
|
@@ -124,7 +369,15 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { |
|
|
|
required int startTime,
|
|
|
|
required int endTime,
|
|
|
|
}) async {
|
|
|
|
return const <OhosHealthRawDataItem>[];
|
|
|
|
return storedBatches
|
|
|
|
.expand((batch) => batch)
|
|
|
|
.where(
|
|
|
|
(item) =>
|
|
|
|
item.dataType == dataType &&
|
|
|
|
item.dataTime >= startTime &&
|
|
|
|
item.dataTime <= endTime,
|
|
|
|
)
|
|
|
|
.toList(growable: false);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
...
|
...
|
@@ -140,6 +393,7 @@ class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore { |
|
|
|
required int dataType,
|
|
|
|
required List<OhosHealthRawDataItem> items,
|
|
|
|
}) async {
|
|
|
|
storedBatches.add(items);
|
|
|
|
return items.length;
|
|
|
|
}
|
|
|
|
}
|
|
...
|
...
|
@@ -301,3 +555,47 @@ HealthKitRawDataPoint _point(int dataType, int time, double value) { |
|
|
|
value: value,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
OhosHealthRawDataItem _rawItem(int dataType, int time, double value) {
|
|
|
|
return OhosHealthRawDataItem(
|
|
|
|
dataType: dataType,
|
|
|
|
dataTime: time,
|
|
|
|
payload: <String, Object?>{
|
|
|
|
'time': time,
|
|
|
|
'value': value,
|
|
|
|
'is_asleep': 0,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
HealthRawHrvStressPoint _existingHrvResult(int time) {
|
|
|
|
return HealthRawHrvStressPoint(
|
|
|
|
userId: 42,
|
|
|
|
rawEndTime: time,
|
|
|
|
rawHrv: 50,
|
|
|
|
result: 50,
|
|
|
|
sourceStartTime: time,
|
|
|
|
sourceEndTime: time,
|
|
|
|
state: HealthRawStressState.normal,
|
|
|
|
baselineHrv: 50,
|
|
|
|
baselineAwakeHrv: 50,
|
|
|
|
baselineSleepHrv: null,
|
|
|
|
baselineRestingHr: 65,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
HealthRawRealtimeStressPoint _existingRealtimeResult(int time) {
|
|
|
|
return HealthRawRealtimeStressPoint(
|
|
|
|
userId: 42,
|
|
|
|
rawEndTime: time,
|
|
|
|
rawHr: 80,
|
|
|
|
result: 50,
|
|
|
|
sourceStartTime: time,
|
|
|
|
sourceEndTime: time,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
int _dateKey(int seconds) {
|
|
|
|
final date = DateTime.fromMillisecondsSinceEpoch(seconds * 1000);
|
|
|
|
return date.year * 10000 + date.month * 100 + date.day;
|
|
|
|
} |
...
|
...
|
|