Commit b2e6377b163a9134e17effc6474fbd53ca183b63

Authored by 权海
1 parent c17ab416

feat(ui):数据获取兼容ohos

Showing 31 changed files with 849 additions and 563 deletions
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
import 'package:get/get.dart';
import '../../core/config/app_environment_config.dart';
... ... @@ -19,7 +19,6 @@ import '../../core/services/obs_upload_service.dart';
import '../../core/services/push_service.dart';
import '../../core/services/thinking_data_service.dart';
import '../../core/services/user_state_service.dart';
import '../../core/services/wear_engine_service.dart';
import '../../data/local/local_storage.dart';
import '../../data/local/user_account_storage.dart';
import '../../data/local/user_preferences_storage.dart';
... ... @@ -107,14 +106,6 @@ void registerInteractionDeps(
UserPreferencesStorage userPrefs,
) {
Get.lazyPut(() => InteractionApi(dioClient), fenix: true);
Get.lazyPut(
() => WearEngineService(
userPrefs,
Get.find<HealthApi>(),
Get.find<InteractionApi>(),
),
fenix: true,
);
}
/// Object storage upload API and service.
... ...
import 'package:doublefeel_flutter/core/network/api/health_api.dart';
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_datasource.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart';
... ...
import 'package:get/get.dart';
import '../../../../core/services/health_raw_data_core_service.dart';
import '../../../../core/services/raw_data_service/ios/health_raw_data_core_service.dart';
import '../../../../data/local/user_preferences_storage.dart';
import '../controllers/developer_options_controller.dart';
... ...
... ... @@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../../core/logging/app_logger.dart';
import '../../../../core/services/health_raw_data_core_service.dart';
import '../../../../core/services/raw_data_service/ios/health_raw_data_core_service.dart';
import '../../../../data/local/user_preferences_storage.dart';
import '../../../../pigeon/platform_api.g.dart';
import '../../../routes/app_pages.dart';
... ...
... ... @@ -3,7 +3,7 @@ import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/network/api/health_api.dart';
import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart';
... ...
... ... @@ -5,7 +5,7 @@ import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
import 'package:doublefeel_flutter/core/network/api/theme_api.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart';
... ...
... ... @@ -4,7 +4,7 @@ import 'package:doublefeel_flutter/core/constants/flutter_bridge_method_name.dar
import 'package:doublefeel_flutter/core/constants/intent_keys.dart';
import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/local/local_storage.dart';
... ...
... ... @@ -14,7 +14,7 @@ import 'package:doublefeel_flutter/core/network/api/friend_api.dart';
import 'package:doublefeel_flutter/core/network/api/pay_api.dart';
import 'package:doublefeel_flutter/core/network/api/vip_api.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/core/util/app_toast.dart';
... ...
import 'package:doublefeel_flutter/core/network/api/health_api.dart';
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_datasource.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart';
... ...
import 'package:doublefeel_flutter/core/network/api/health_api.dart';
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_datasource.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_datasource_wrapper.dart';
... ...
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
import 'package:flutter/scheduler.dart';
import 'package:get/get.dart';
... ... @@ -9,7 +9,6 @@ import '../../../../core/result/app_result.dart';
import '../../../../core/services/user_state_service.dart';
import '../../../../data/local/user_preferences_storage.dart';
import '../../../../data/models/local/user_preferences.dart';
import '../../../../data/models/user/user_models.dart';
/// Cold-start routing: restore session or open login.
class SplashController extends GetxController {
... ...
import 'package:doublefeel_flutter/core/logging/app_logger.dart';
import 'package:doublefeel_flutter/core/network/api/user_api.dart';
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:doublefeel_flutter/data/local/user_account_storage.dart';
... ...
... ... @@ -3,9 +3,10 @@ import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import '../../data/models/enums/app_enums.dart';
import '../../pigeon/health_kit_raw_data_api.g.dart';
import 'health_raw_data_core_service.dart';
import '../../../data/models/enums/app_enums.dart';
import '../../../pigeon/health_kit_raw_data_api.g.dart';
import 'ios/health_raw_data_core_service.dart';
import 'health_raw_models.dart';
import 'health_raw_stress_calculator.dart';
typedef FriendRawDataPageFetcher = Future<List<HealthKitRawDataPoint>>
... ...
import 'package:flutter/foundation.dart';
import '../../../pigeon/health_kit_raw_data_api.g.dart';
import 'ohos/ohos_health_raw_data_sync_service.dart';
abstract class HealthRawDataSource {
Future<bool> hasHealthData();
Future<List<HealthKitRawDataPoint>> getRawData(
int dataType,
int startTime,
int endTime,
);
Future<List<HealthKitRawSleepDataPoint>> getRawSleepData(
int startTime,
int endTime,
);
Future<List<HealthKitRawActivityDataPoint>> getRawActivityData(
int startTime,
int endTime,
);
Future<List<HealthKitRawWorkoutDataPoint>> getRawWorkoutData(
int startTime,
int endTime,
);
}
class PlatformHealthRawDataSource implements HealthRawDataSource {
PlatformHealthRawDataSource({
HealthRawDataSource? iosDataSource,
HealthRawDataSource? ohosDataSource,
TargetPlatform? targetPlatform,
}) : _iosDataSource = iosDataSource ?? AppleHealthRawDataSource(),
_ohosDataSource = ohosDataSource ?? OhosHealthRawDataSource(),
_targetPlatform = targetPlatform;
final HealthRawDataSource _iosDataSource;
final HealthRawDataSource _ohosDataSource;
final TargetPlatform? _targetPlatform;
HealthRawDataSource get _currentDataSource {
final platform = _targetPlatform ?? defaultTargetPlatform;
return platform == TargetPlatform.iOS ? _iosDataSource : _ohosDataSource;
}
@override
Future<bool> hasHealthData() {
return _currentDataSource.hasHealthData();
}
@override
Future<List<HealthKitRawDataPoint>> getRawData(
int dataType,
int startTime,
int endTime,
) {
return _currentDataSource.getRawData(dataType, startTime, endTime);
}
@override
Future<List<HealthKitRawSleepDataPoint>> getRawSleepData(
int startTime,
int endTime,
) {
return _currentDataSource.getRawSleepData(startTime, endTime);
}
@override
Future<List<HealthKitRawActivityDataPoint>> getRawActivityData(
int startTime,
int endTime,
) {
return _currentDataSource.getRawActivityData(startTime, endTime);
}
@override
Future<List<HealthKitRawWorkoutDataPoint>> getRawWorkoutData(
int startTime,
int endTime,
) {
return _currentDataSource.getRawWorkoutData(startTime, endTime);
}
}
// IOS
class AppleHealthRawDataSource implements HealthRawDataSource {
AppleHealthRawDataSource({
HealthKitRawDataHostApi? rawDataApi,
}) : _rawDataApi = rawDataApi ?? HealthKitRawDataHostApi();
final HealthKitRawDataHostApi _rawDataApi;
@override
Future<bool> hasHealthData() {
return _rawDataApi.hasHealthData();
}
@override
Future<List<HealthKitRawDataPoint>> getRawData(
int dataType,
int startTime,
int endTime,
) {
return _rawDataApi.getHealthKitRawData(dataType, startTime, endTime);
}
@override
Future<List<HealthKitRawSleepDataPoint>> getRawSleepData(
int startTime,
int endTime,
) {
return _rawDataApi.getHealthKitRawSleepData(startTime, endTime);
}
@override
Future<List<HealthKitRawActivityDataPoint>> getRawActivityData(
int startTime,
int endTime,
) {
return _rawDataApi.getHealthKitRawActivityData(startTime, endTime);
}
@override
Future<List<HealthKitRawWorkoutDataPoint>> getRawWorkoutData(
int startTime,
int endTime,
) {
return _rawDataApi.getHealthKitRawWorkoutData(startTime, endTime);
}
}
// ohos
class OhosHealthRawDataSource implements HealthRawDataSource {
OhosHealthRawDataSource({
OhosHealthRawDataSyncService? syncService,
}) : _syncService = syncService ?? OhosHealthRawDataSyncService();
final OhosHealthRawDataSyncService _syncService;
@override
Future<bool> hasHealthData() async {
// TODO: Query the OHOS local raw-data database when its schema is ready.
return false;
}
@override
Future<List<HealthKitRawDataPoint>> getRawData(
int dataType,
int startTime,
int endTime,
) async {
await _syncService.syncRawData(
dataType: dataType,
startTime: startTime,
endTime: endTime,
);
// TODO: Read synced OHOS raw points from local database.
return const <HealthKitRawDataPoint>[];
}
@override
Future<List<HealthKitRawSleepDataPoint>> getRawSleepData(
int startTime,
int endTime,
) async {
await _syncService.syncRawData(
dataType: OhosHealthRawDataType.sleepAnalysis,
startTime: startTime,
endTime: endTime,
);
// TODO: Read synced OHOS sleep groups from local database.
return const <HealthKitRawSleepDataPoint>[];
}
@override
Future<List<HealthKitRawActivityDataPoint>> getRawActivityData(
int startTime,
int endTime,
) async {
await _syncService.syncRawData(
dataType: OhosHealthRawDataType.activitySummary,
startTime: startTime,
endTime: endTime,
);
// TODO: Read synced OHOS activity summaries from local database.
return const <HealthKitRawActivityDataPoint>[];
}
@override
Future<List<HealthKitRawWorkoutDataPoint>> getRawWorkoutData(
int startTime,
int endTime,
) async {
await _syncService.syncRawData(
dataType: OhosHealthRawDataType.workout,
startTime: startTime,
endTime: endTime,
);
// TODO: Read synced OHOS workout intervals from local database.
return const <HealthKitRawWorkoutDataPoint>[];
}
}
... ...
import 'dart:math' as math;
import '../../../pigeon/health_kit_raw_data_api.g.dart';
import 'health_sleep_calculator.dart';
class HealthRawStressCalculationResult {
const HealthRawStressCalculationResult({
required this.userId,
required this.hrvStressPoints,
required this.realtimeStressPoints,
required this.dailyStressPoints,
this.sleepResults = const <HealthRawSleepResult>[],
});
final int userId;
final List<HealthRawHrvStressPoint> hrvStressPoints;
final List<HealthRawRealtimeStressPoint> realtimeStressPoints;
final List<HealthRawDailyStressPoint> dailyStressPoints;
final List<HealthRawSleepResult> sleepResults;
HealthRawStressCalculationResult copyWith({
List<HealthRawHrvStressPoint>? hrvStressPoints,
List<HealthRawRealtimeStressPoint>? realtimeStressPoints,
List<HealthRawDailyStressPoint>? dailyStressPoints,
List<HealthRawSleepResult>? sleepResults,
}) {
return HealthRawStressCalculationResult(
userId: userId,
hrvStressPoints: hrvStressPoints ?? this.hrvStressPoints,
realtimeStressPoints: realtimeStressPoints ?? this.realtimeStressPoints,
dailyStressPoints: dailyStressPoints ?? this.dailyStressPoints,
sleepResults: sleepResults ?? this.sleepResults,
);
}
}
class HealthRawDataUpdatedEvent {
const HealthRawDataUpdatedEvent({required this.dataTypes});
final List<int> dataTypes;
}
class HealthRawMarkedDataSet {
const HealthRawMarkedDataSet({
required this.hrv,
required this.heartRate,
required this.restingHeartRate,
});
final List<HealthRawMarkedDataPoint> hrv;
final List<HealthRawMarkedDataPoint> heartRate;
final List<HealthRawMarkedDataPoint> restingHeartRate;
}
class HealthRawMarkedDataPoint {
HealthRawMarkedDataPoint({
required this.raw,
required this.flags,
});
final HealthKitRawDataPoint raw;
HealthRawPointFlags flags;
int get startTime => raw.startTime;
int get endTime => raw.endTime;
double? get value => raw.value;
}
enum HealthRawPointContext {
workout,
workoutRecovery,
sleep,
suspectedActivity,
awake,
}
class HealthRawPointFlags {
const HealthRawPointFlags({
required this.isWorkout,
required this.isWorkoutRecovery,
required this.isSleepLikely,
required this.isSuspectedActivity,
});
const HealthRawPointFlags.none()
: isWorkout = false,
isWorkoutRecovery = false,
isSleepLikely = false,
isSuspectedActivity = false;
final bool isWorkout;
final bool isWorkoutRecovery;
final bool isSleepLikely;
final bool isSuspectedActivity;
HealthRawPointContext get context {
if (isWorkout) return HealthRawPointContext.workout;
if (isWorkoutRecovery) return HealthRawPointContext.workoutRecovery;
if (isSleepLikely) return HealthRawPointContext.sleep;
if (isSuspectedActivity) return HealthRawPointContext.suspectedActivity;
return HealthRawPointContext.awake;
}
bool get isAwakeBaselineEligible =>
!isSleepLikely && !isWorkout && !isWorkoutRecovery;
bool get isSleepHrvBaselineEligible =>
isSleepLikely && !isWorkout && !isWorkoutRecovery;
bool get isAwakeHrBaselineEligible =>
!isSleepLikely && !isWorkout && !isWorkoutRecovery;
bool get isSleepHrBaselineEligible =>
isSleepLikely && !isWorkout && !isWorkoutRecovery;
bool get isCurrentAwakeHrEligible => isAwakeHrBaselineEligible;
bool matchesContext(HealthRawPointContext pointContext) {
return context == pointContext;
}
HealthRawPointFlags copyWith({
bool? isWorkout,
bool? isWorkoutRecovery,
bool? isSleepLikely,
bool? isSuspectedActivity,
}) {
return HealthRawPointFlags(
isWorkout: isWorkout ?? this.isWorkout,
isWorkoutRecovery: isWorkoutRecovery ?? this.isWorkoutRecovery,
isSleepLikely: isSleepLikely ?? this.isSleepLikely,
isSuspectedActivity: isSuspectedActivity ?? this.isSuspectedActivity,
);
}
}
enum HealthRawStressState {
overload(1),
attention(2),
normal(3),
excellent(4);
const HealthRawStressState(this.value);
final int value;
}
class HealthRawHrvStressPoint {
const HealthRawHrvStressPoint({
required this.userId,
required this.rawEndTime,
required this.rawHrv,
required this.result,
required this.sourceStartTime,
required this.sourceEndTime,
required this.state,
required this.baselineHrv,
required this.baselineAwakeHrv,
required this.baselineSleepHrv,
required this.baselineRestingHr,
this.flags = const HealthRawPointFlags.none(),
this.uploaded = false,
});
final int userId;
final int rawEndTime;
final double rawHrv;
final double result;
final int sourceStartTime;
final int sourceEndTime;
final HealthRawStressState state;
final double baselineHrv;
final double baselineAwakeHrv;
final double? baselineSleepHrv;
final double baselineRestingHr;
final HealthRawPointFlags flags;
final bool uploaded;
factory HealthRawHrvStressPoint.fromDb(Map<String, Object?> row) {
final result = (row['result'] as num).toDouble();
final rawHrv = _positiveDouble(row['raw_hrv']) ?? result;
final baselineHrv = _positiveDouble(row['baseline_hrv']) ?? result;
final baselineAwakeHrv =
_positiveDouble(row['baseline_awake_hrv']) ?? baselineHrv;
return HealthRawHrvStressPoint(
userId: row['user_id'] as int,
rawEndTime: row['raw_end_time'] as int,
rawHrv: rawHrv,
result: result,
sourceStartTime: row['source_start_time'] as int,
sourceEndTime: row['source_end_time'] as int,
state: _stressStateFromDb(row['state']) ?? HealthRawStressState.normal,
baselineHrv: baselineHrv,
baselineAwakeHrv: baselineAwakeHrv,
baselineSleepHrv: _positiveDouble(row['baseline_sleep_hrv']),
baselineRestingHr: _positiveDouble(row['baseline_resting_hr']) ?? 65,
flags: _flagsFromDb(row),
uploaded: (row['uploaded'] as int) == 1,
);
}
}
class HealthRawRealtimeStressPoint {
const HealthRawRealtimeStressPoint({
required this.userId,
required this.rawEndTime,
required this.rawHr,
required this.result,
required this.sourceStartTime,
required this.sourceEndTime,
this.flags = const HealthRawPointFlags.none(),
this.uploaded = false,
});
final int userId;
final int rawEndTime;
final double rawHr;
final double result;
final int sourceStartTime;
final int sourceEndTime;
final HealthRawPointFlags flags;
final bool uploaded;
HealthRawStressState get state => healthRawRealtimeStressState(result);
bool get isWorkout => flags.isWorkout;
bool get isWorkoutRecovery => flags.isWorkoutRecovery;
bool get isSleepLikely => flags.isSleepLikely;
bool get isSuspectedActivity => flags.isSuspectedActivity;
factory HealthRawRealtimeStressPoint.fromDb(Map<String, Object?> row) {
return HealthRawRealtimeStressPoint(
userId: row['user_id'] as int,
rawEndTime: row['raw_end_time'] as int,
rawHr: (row['raw_hr'] as num?)?.toDouble() ?? 0,
result: (row['result'] as num).toDouble(),
sourceStartTime: row['source_start_time'] as int,
sourceEndTime: row['source_end_time'] as int,
flags: _flagsFromDb(row),
uploaded: (row['uploaded'] as int) == 1,
);
}
}
class HealthRawDailyStressPoint {
const HealthRawDailyStressPoint({
required this.userId,
required this.date,
required this.stressValue,
required this.stressScore,
required this.state,
required this.dataTime,
this.uploaded = false,
});
final int userId;
final int date;
final double stressValue;
final int stressScore;
final HealthRawStressState state;
final int dataTime;
final bool uploaded;
factory HealthRawDailyStressPoint.fromDb(Map<String, Object?> row) {
final stressScore = row['stress_score'] as int;
return HealthRawDailyStressPoint(
userId: row['user_id'] as int,
date: row['date'] as int,
stressValue: (row['stress_value'] as num).toDouble(),
stressScore: stressScore,
state: _stressStateFromDb(row['state']) ??
healthRawRealtimeStressState(stressScore),
dataTime: row['data_time'] as int,
uploaded: (row['uploaded'] as int) == 1,
);
}
}
class HealthRawSleepResult {
const HealthRawSleepResult({
required this.userId,
required this.date,
required this.startDate,
required this.sleepScore,
required this.sleepState,
required this.inBedMinutes,
required this.awakMinutes,
required this.sleepMinutes,
this.uploaded = false,
});
final int userId;
final int date;
final int startDate;
final int sleepScore;
final int sleepState;
final int inBedMinutes;
final int awakMinutes;
final int sleepMinutes;
final bool uploaded;
factory HealthRawSleepResult.fromDb(Map<String, Object?> row) {
return HealthRawSleepResult(
userId: row['user_id'] as int,
date: row['date'] as int,
startDate: row['start_date'] as int,
sleepScore: row['sleep_score'] as int,
sleepState: row['sleep_state'] as int,
inBedMinutes: row['in_bed_minutes'] as int,
awakMinutes: row['awak_minutes'] as int,
sleepMinutes: row['sleep_minutes'] as int,
uploaded: (row['uploaded'] as int? ?? 0) == 1,
);
}
int get sleepStateValue => sleepState;
String? get sleepStateDescription {
for (final state in HealthSleepState.values) {
if (state.value == sleepState) return state.description;
}
return null;
}
}
class HealthRawDailyStressCalculator {
const HealthRawDailyStressCalculator._();
static const int _confirmedActivityBufferSeconds = 12 * 60;
static HealthRawDailyStressPoint? calculate({
required int userId,
required int date,
required List<HealthRawRealtimeStressPoint> realtimePoints,
required int dataTime,
}) {
final validValuePoints =
realtimePoints.where((e) => _isValidStressValue(e.result)).toList();
final activityBuffers = _confirmedActivityBuffers(realtimePoints);
final values = validValuePoints
.where((e) => !_isInTimeRanges(e.rawEndTime, activityBuffers))
.map((e) => e.result)
.toList()
..sort();
if (values.isEmpty) return null;
final stressValue = _integerDouble(_average(values));
final stressScore = _dailyStressScore(values).round().clamp(1, 100).toInt();
return HealthRawDailyStressPoint(
userId: userId,
date: date,
stressValue: stressValue,
stressScore: stressScore,
state: healthRawRealtimeStressState(stressScore),
dataTime: dataTime,
);
}
static double _dailyStressScore(List<double> sortedValues) {
final median = _median(sortedValues);
final p75 = _percentile75(sortedValues);
return _round(_clamp(median * 0.60 + p75 * 0.40, 1, 100));
}
static List<({int start, int end})> _confirmedActivityBuffers(
List<HealthRawRealtimeStressPoint> realtimePoints,
) {
final ranges = realtimePoints
.where((e) =>
_isValidStressValue(e.result) &&
(e.isWorkout || e.isWorkoutRecovery))
.map((e) => (
start: e.rawEndTime - _confirmedActivityBufferSeconds,
end: e.rawEndTime + _confirmedActivityBufferSeconds,
))
.toList()
..sort((a, b) => a.start.compareTo(b.start));
if (ranges.isEmpty) return const <({int start, int end})>[];
final merged = <({int start, int end})>[];
var current = ranges.first;
for (final range in ranges.skip(1)) {
if (range.start <= current.end) {
current = (
start: current.start,
end: math.max(current.end, range.end),
);
} else {
merged.add(current);
current = range;
}
}
merged.add(current);
return merged;
}
static bool _isInTimeRanges(int time, List<({int start, int end})> ranges) {
return ranges.any((e) => time >= e.start && time <= e.end);
}
static bool _isValidStressValue(num value) => value >= 1 && value <= 100;
static double _average(List<double> values) =>
values.reduce((a, b) => a + b) / values.length;
static double _median(List<double> sortedValues) {
final length = sortedValues.length;
final middle = length ~/ 2;
if (length.isOdd) return sortedValues[middle];
return (sortedValues[middle - 1] + sortedValues[middle]) / 2;
}
static double _percentile75(List<double> sortedValues) {
final rank = (sortedValues.length * 0.75).ceil().clamp(
1,
sortedValues.length,
);
return sortedValues[rank - 1];
}
static double _clamp(num value, num min, num max) =>
math.max(min.toDouble(), math.min(max.toDouble(), value.toDouble()));
static double _round(double value) => (value * 10).round() / 10;
}
HealthRawStressState healthRawRealtimeStressState(num value) {
if (value >= 81) return HealthRawStressState.overload;
if (value >= 61) return HealthRawStressState.attention;
if (value >= 21) return HealthRawStressState.normal;
return HealthRawStressState.excellent;
}
HealthRawPointFlags _flagsFromDb(Map<String, Object?> row) {
return HealthRawPointFlags(
isWorkout: (row['is_workout'] as int? ?? 0) == 1,
isWorkoutRecovery: (row['is_workout_recovery'] as int? ?? 0) == 1,
isSleepLikely: (row['is_sleep_likely'] as int? ?? 0) == 1,
isSuspectedActivity: (row['is_suspected_activity'] as int? ?? 0) == 1,
);
}
double? _positiveDouble(Object? value) {
final result = (value as num?)?.toDouble();
if (result == null || result <= 0) return null;
return result;
}
double _integerDouble(num value) => value.toInt().toDouble();
HealthRawStressState? _stressStateFromDb(Object? value) {
final stateValue = value as int?;
if (stateValue == null) return null;
for (final state in HealthRawStressState.values) {
if (state.value == stateValue) return state;
}
return null;
}
... ...
import 'dart:math' as math;
import '../../pigeon/health_kit_raw_data_api.g.dart';
import 'health_raw_data_core_service.dart';
import '../../../pigeon/health_kit_raw_data_api.g.dart';
import 'health_raw_models.dart';
class HealthRawStressCalculator {
const HealthRawStressCalculator({required this.userId});
... ... @@ -567,11 +567,6 @@ class HealthRawStressCalculator {
return math.min(math.max(value, lower), upper).toDouble();
}
static double _round(double value, int places) {
final scale = math.pow(10, places).toDouble();
return (value * scale).round() / scale;
}
static double _integerDouble(num value) => value.toInt().toDouble();
static List<int> _daysInRange(int startTime, int endTime) {
... ...
import 'dart:math' as math;
import '../../pigeon/health_kit_raw_data_api.g.dart';
import '../../../pigeon/health_kit_raw_data_api.g.dart';
const healthSleepGoalMinutes = 8 * 60;
const _sleepContinuityToleranceSeconds = 1;
... ...
... ... @@ -8,18 +8,19 @@ import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import '../../data/models/enums/app_enums.dart';
import '../../l10n/l10n_extensions.dart';
import '../../pigeon/health_kit_api.g.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 '../result/app_result.dart';
import '../../../../data/models/enums/app_enums.dart';
import '../../../../l10n/l10n_extensions.dart';
import '../../../../pigeon/health_kit_api.g.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 '../../../result/app_result.dart';
import '../health_raw_models.dart';
import 'health_raw_local_notification_debug_store.dart';
import 'health_raw_local_notification.dart';
import 'health_raw_stress_calculator.dart';
import 'health_sleep_calculator.dart';
import '../health_raw_stress_calculator.dart';
import '../health_sleep_calculator.dart';
class HealthRawDataCoreService {
static const int defaultLookbackDays = 183;
... ... @@ -1643,460 +1644,6 @@ class HealthRawDataCoreService {
}
}
class HealthRawStressCalculationResult {
const HealthRawStressCalculationResult({
required this.userId,
required this.hrvStressPoints,
required this.realtimeStressPoints,
required this.dailyStressPoints,
this.sleepResults = const <HealthRawSleepResult>[],
});
final int userId;
final List<HealthRawHrvStressPoint> hrvStressPoints;
final List<HealthRawRealtimeStressPoint> realtimeStressPoints;
final List<HealthRawDailyStressPoint> dailyStressPoints;
final List<HealthRawSleepResult> sleepResults;
HealthRawStressCalculationResult copyWith({
List<HealthRawHrvStressPoint>? hrvStressPoints,
List<HealthRawRealtimeStressPoint>? realtimeStressPoints,
List<HealthRawDailyStressPoint>? dailyStressPoints,
List<HealthRawSleepResult>? sleepResults,
}) {
return HealthRawStressCalculationResult(
userId: userId,
hrvStressPoints: hrvStressPoints ?? this.hrvStressPoints,
realtimeStressPoints: realtimeStressPoints ?? this.realtimeStressPoints,
dailyStressPoints: dailyStressPoints ?? this.dailyStressPoints,
sleepResults: sleepResults ?? this.sleepResults,
);
}
}
class HealthRawDataUpdatedEvent {
const HealthRawDataUpdatedEvent({required this.dataTypes});
final List<int> dataTypes;
}
class HealthRawMarkedDataSet {
const HealthRawMarkedDataSet({
required this.hrv,
required this.heartRate,
required this.restingHeartRate,
});
final List<HealthRawMarkedDataPoint> hrv;
final List<HealthRawMarkedDataPoint> heartRate;
final List<HealthRawMarkedDataPoint> restingHeartRate;
}
class HealthRawMarkedDataPoint {
HealthRawMarkedDataPoint({
required this.raw,
required this.flags,
});
final HealthKitRawDataPoint raw;
HealthRawPointFlags flags;
int get startTime => raw.startTime;
int get endTime => raw.endTime;
double? get value => raw.value;
}
enum HealthRawPointContext {
workout,
workoutRecovery,
sleep,
suspectedActivity,
awake,
}
class HealthRawPointFlags {
const HealthRawPointFlags({
required this.isWorkout,
required this.isWorkoutRecovery,
required this.isSleepLikely,
required this.isSuspectedActivity,
});
const HealthRawPointFlags.none()
: isWorkout = false,
isWorkoutRecovery = false,
isSleepLikely = false,
isSuspectedActivity = false;
final bool isWorkout;
final bool isWorkoutRecovery;
final bool isSleepLikely;
final bool isSuspectedActivity;
HealthRawPointContext get context {
if (isWorkout) return HealthRawPointContext.workout;
if (isWorkoutRecovery) return HealthRawPointContext.workoutRecovery;
if (isSleepLikely) return HealthRawPointContext.sleep;
if (isSuspectedActivity) return HealthRawPointContext.suspectedActivity;
return HealthRawPointContext.awake;
}
bool get isAwakeBaselineEligible =>
!isSleepLikely && !isWorkout && !isWorkoutRecovery;
bool get isSleepHrvBaselineEligible =>
isSleepLikely && !isWorkout && !isWorkoutRecovery;
bool get isAwakeHrBaselineEligible =>
!isSleepLikely && !isWorkout && !isWorkoutRecovery;
bool get isSleepHrBaselineEligible =>
isSleepLikely && !isWorkout && !isWorkoutRecovery;
bool get isCurrentAwakeHrEligible => isAwakeHrBaselineEligible;
bool matchesContext(HealthRawPointContext pointContext) {
return context == pointContext;
}
HealthRawPointFlags copyWith({
bool? isWorkout,
bool? isWorkoutRecovery,
bool? isSleepLikely,
bool? isSuspectedActivity,
}) {
return HealthRawPointFlags(
isWorkout: isWorkout ?? this.isWorkout,
isWorkoutRecovery: isWorkoutRecovery ?? this.isWorkoutRecovery,
isSleepLikely: isSleepLikely ?? this.isSleepLikely,
isSuspectedActivity: isSuspectedActivity ?? this.isSuspectedActivity,
);
}
}
enum HealthRawStressState {
overload(1),
attention(2),
normal(3),
excellent(4);
const HealthRawStressState(this.value);
final int value;
}
class HealthRawHrvStressPoint {
const HealthRawHrvStressPoint({
required this.userId,
required this.rawEndTime,
required this.rawHrv,
required this.result,
required this.sourceStartTime,
required this.sourceEndTime,
required this.state,
required this.baselineHrv,
required this.baselineAwakeHrv,
required this.baselineSleepHrv,
required this.baselineRestingHr,
this.flags = const HealthRawPointFlags.none(),
this.uploaded = false,
});
final int userId;
final int rawEndTime;
final double rawHrv;
final double result;
final int sourceStartTime;
final int sourceEndTime;
final HealthRawStressState state;
final double baselineHrv;
final double baselineAwakeHrv;
final double? baselineSleepHrv;
final double baselineRestingHr;
final HealthRawPointFlags flags;
final bool uploaded;
factory HealthRawHrvStressPoint.fromDb(Map<String, Object?> row) {
final result = (row['result'] as num).toDouble();
final rawHrv = _positiveDouble(row['raw_hrv']) ?? result;
final baselineHrv = _positiveDouble(row['baseline_hrv']) ?? result;
final baselineAwakeHrv =
_positiveDouble(row['baseline_awake_hrv']) ?? baselineHrv;
return HealthRawHrvStressPoint(
userId: row['user_id'] as int,
rawEndTime: row['raw_end_time'] as int,
rawHrv: rawHrv,
result: result,
sourceStartTime: row['source_start_time'] as int,
sourceEndTime: row['source_end_time'] as int,
state: _stressStateFromDb(row['state']) ?? HealthRawStressState.normal,
baselineHrv: baselineHrv,
baselineAwakeHrv: baselineAwakeHrv,
baselineSleepHrv: _positiveDouble(row['baseline_sleep_hrv']),
baselineRestingHr: _positiveDouble(row['baseline_resting_hr']) ?? 65,
flags: _flagsFromDb(row),
uploaded: (row['uploaded'] as int) == 1,
);
}
}
class HealthRawRealtimeStressPoint {
const HealthRawRealtimeStressPoint({
required this.userId,
required this.rawEndTime,
required this.rawHr,
required this.result,
required this.sourceStartTime,
required this.sourceEndTime,
this.flags = const HealthRawPointFlags.none(),
this.uploaded = false,
});
final int userId;
final int rawEndTime;
final double rawHr;
final double result;
final int sourceStartTime;
final int sourceEndTime;
final HealthRawPointFlags flags;
final bool uploaded;
HealthRawStressState get state => healthRawRealtimeStressState(result);
bool get isWorkout => flags.isWorkout;
bool get isWorkoutRecovery => flags.isWorkoutRecovery;
bool get isSleepLikely => flags.isSleepLikely;
bool get isSuspectedActivity => flags.isSuspectedActivity;
factory HealthRawRealtimeStressPoint.fromDb(Map<String, Object?> row) {
return HealthRawRealtimeStressPoint(
userId: row['user_id'] as int,
rawEndTime: row['raw_end_time'] as int,
rawHr: (row['raw_hr'] as num?)?.toDouble() ?? 0,
result: (row['result'] as num).toDouble(),
sourceStartTime: row['source_start_time'] as int,
sourceEndTime: row['source_end_time'] as int,
flags: _flagsFromDb(row),
uploaded: (row['uploaded'] as int) == 1,
);
}
}
class HealthRawDailyStressPoint {
const HealthRawDailyStressPoint({
required this.userId,
required this.date,
required this.stressValue,
required this.stressScore,
required this.state,
required this.dataTime,
this.uploaded = false,
});
final int userId;
final int date;
final double stressValue;
final int stressScore;
final HealthRawStressState state;
final int dataTime;
final bool uploaded;
factory HealthRawDailyStressPoint.fromDb(Map<String, Object?> row) {
final stressScore = row['stress_score'] as int;
return HealthRawDailyStressPoint(
userId: row['user_id'] as int,
date: row['date'] as int,
stressValue: (row['stress_value'] as num).toDouble(),
stressScore: stressScore,
state: _stressStateFromDb(row['state']) ??
healthRawRealtimeStressState(stressScore),
dataTime: row['data_time'] as int,
uploaded: (row['uploaded'] as int) == 1,
);
}
}
class HealthRawSleepResult {
const HealthRawSleepResult({
required this.userId,
required this.date,
required this.startDate,
required this.sleepScore,
required this.sleepState,
required this.inBedMinutes,
required this.awakMinutes,
required this.sleepMinutes,
this.uploaded = false,
});
final int userId;
final int date;
final int startDate;
final int sleepScore;
final int sleepState;
final int inBedMinutes;
final int awakMinutes;
final int sleepMinutes;
final bool uploaded;
factory HealthRawSleepResult.fromDb(Map<String, Object?> row) {
return HealthRawSleepResult(
userId: row['user_id'] as int,
date: row['date'] as int,
startDate: row['start_date'] as int,
sleepScore: row['sleep_score'] as int,
sleepState: row['sleep_state'] as int,
inBedMinutes: row['in_bed_minutes'] as int,
awakMinutes: row['awak_minutes'] as int,
sleepMinutes: row['sleep_minutes'] as int,
uploaded: (row['uploaded'] as int? ?? 0) == 1,
);
}
int get sleepStateValue => sleepState;
String? get sleepStateDescription {
for (final state in HealthSleepState.values) {
if (state.value == sleepState) return state.description;
}
return null;
}
}
class HealthRawDailyStressCalculator {
const HealthRawDailyStressCalculator._();
static const int _confirmedActivityBufferSeconds = 12 * 60;
static HealthRawDailyStressPoint? calculate({
required int userId,
required int date,
required List<HealthRawRealtimeStressPoint> realtimePoints,
required int dataTime,
}) {
final validValuePoints =
realtimePoints.where((e) => _isValidStressValue(e.result)).toList();
final activityBuffers = _confirmedActivityBuffers(realtimePoints);
final values = validValuePoints
.where((e) => !_isInTimeRanges(e.rawEndTime, activityBuffers))
.map((e) => e.result)
.toList()
..sort();
if (values.isEmpty) return null;
final stressValue = _integerDouble(_average(values));
final stressScore = _dailyStressScore(values).round().clamp(1, 100).toInt();
return HealthRawDailyStressPoint(
userId: userId,
date: date,
stressValue: stressValue,
stressScore: stressScore,
state: healthRawRealtimeStressState(stressScore),
dataTime: dataTime,
);
}
static double _dailyStressScore(List<double> sortedValues) {
final median = _median(sortedValues);
final p75 = _percentile75(sortedValues);
return _round(_clamp(median * 0.60 + p75 * 0.40, 1, 100));
}
static List<({int start, int end})> _confirmedActivityBuffers(
List<HealthRawRealtimeStressPoint> realtimePoints,
) {
final ranges = realtimePoints
.where((e) =>
_isValidStressValue(e.result) &&
(e.isWorkout || e.isWorkoutRecovery))
.map((e) => (
start: e.rawEndTime - _confirmedActivityBufferSeconds,
end: e.rawEndTime + _confirmedActivityBufferSeconds,
))
.toList()
..sort((a, b) => a.start.compareTo(b.start));
if (ranges.isEmpty) return const <({int start, int end})>[];
final merged = <({int start, int end})>[];
var current = ranges.first;
for (final range in ranges.skip(1)) {
if (range.start <= current.end) {
current = (
start: current.start,
end: math.max(current.end, range.end),
);
} else {
merged.add(current);
current = range;
}
}
merged.add(current);
return merged;
}
static bool _isInTimeRanges(int time, List<({int start, int end})> ranges) {
return ranges.any((e) => time >= e.start && time <= e.end);
}
static bool _isValidStressValue(num value) => value >= 1 && value <= 100;
static double _average(List<double> values) =>
values.reduce((a, b) => a + b) / values.length;
static double _median(List<double> sortedValues) {
final length = sortedValues.length;
final middle = length ~/ 2;
if (length.isOdd) return sortedValues[middle];
return (sortedValues[middle - 1] + sortedValues[middle]) / 2;
}
static double _percentile75(List<double> sortedValues) {
final rank = (sortedValues.length * 0.75).ceil().clamp(
1,
sortedValues.length,
);
return sortedValues[rank - 1];
}
static double _clamp(num value, num min, num max) =>
math.max(min.toDouble(), math.min(max.toDouble(), value.toDouble()));
static double _round(double value) => (value * 10).round() / 10;
static double _integerDouble(num value) => value.toInt().toDouble();
}
HealthRawStressState healthRawRealtimeStressState(num value) {
if (value >= 81) return HealthRawStressState.overload;
if (value >= 61) return HealthRawStressState.attention;
if (value >= 21) return HealthRawStressState.normal;
return HealthRawStressState.excellent;
}
HealthRawPointFlags _flagsFromDb(Map<String, Object?> row) {
return HealthRawPointFlags(
isWorkout: (row['is_workout'] as int? ?? 0) == 1,
isWorkoutRecovery: (row['is_workout_recovery'] as int? ?? 0) == 1,
isSleepLikely: (row['is_sleep_likely'] as int? ?? 0) == 1,
isSuspectedActivity: (row['is_suspected_activity'] as int? ?? 0) == 1,
);
}
double? _positiveDouble(Object? value) {
final result = (value as num?)?.toDouble();
if (result == null || result <= 0) return null;
return result;
}
double _integerDouble(num value) => value.toInt().toDouble();
HealthRawStressState? _stressStateFromDb(Object? value) {
final stateValue = value as int?;
if (stateValue == null) return null;
for (final state in HealthRawStressState.values) {
if (state.value == stateValue) return state;
}
return null;
}
class HealthRawStressDbRows {
const HealthRawStressDbRows({
required this.hrvRows,
... ... @@ -2111,6 +1658,8 @@ class HealthRawStressDbRows {
final List<Map<String, Object?>> sleepRows;
}
double _integerDouble(num value) => value.toInt().toDouble();
class HealthRawStressLocalStore {
HealthRawStressLocalStore({
Directory? rootDirectory,
... ...
... ... @@ -3,9 +3,9 @@ import 'dart:io';
import 'package:path_provider/path_provider.dart';
import '../../l10n/gen/app_localizations.dart';
import '../../pigeon/platform_api.g.dart';
import 'health_raw_data_core_service.dart';
import '../../../../l10n/gen/app_localizations.dart';
import '../../../../pigeon/platform_api.g.dart';
import '../health_raw_models.dart';
const healthRawTodayLink = 'doublefeel://flutter/home?tab=today';
const healthRawHrvChangeLink =
... ...
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import '../logging/app_logger.dart';
... ...
import 'dart:convert';
import '../../pigeon/wear_engine_api.g.dart';
import '../logging/app_logger.dart';
import '../util/platform.dart';
import '../../data/local/user_preferences_storage.dart';
import '../../data/models/enums/app_enums.dart';
import '../network/api/health_api.dart';
import '../network/api/interaction_api.dart';
import '../../data/models/interaction/interaction_models.dart';
import '../result/app_result.dart';
/// Watch sync and P2P messaging.
class WearEngineService {
WearEngineService(
this._userPrefs,
this._healthApi,
this._interactionApi, {
WearEngineHostApi? wearEngineHost,
}) : _wearEngineHost = wearEngineHost ?? WearEngineHostApi();
final WearEngineHostApi _wearEngineHost;
final UserPreferencesStorage _userPrefs;
final HealthApi _healthApi;
final InteractionApi _interactionApi;
Future<bool> hasAvailableDevices() async {
if (!isAndroid) {
return false;
}
return _wearEngineHost.hasAvailableDevices();
}
Future<bool> prepareWatchConnection() async {
final device = await _wearEngineHost.checkConnectedDevice();
if (device == null) {
return false;
}
return _wearEngineHost.registerMessageReceiver();
}
Future<bool> sendInteractionMessage(int actionType) async {
final data = InteractionData(
interactionType: 0,
actionType: actionType,
);
final result = await _interactionApi.sendInteraction(data);
return result is AppSuccess;
}
}
import 'dart:math' as math;
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/health_sleep_calculator.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_sleep_calculator.dart';
import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
import 'package:doublefeel_flutter/data/models/health/activity/activity_burn_statistics_data_v2.dart';
import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
... ...
import 'package:doublefeel_flutter/core/error/app_error.dart';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart';
import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
import 'package:doublefeel_flutter/data/models/health/activity/activity_burn_statistics_data_v2.dart';
import 'package:doublefeel_flutter/data/models/health/health_v2_models.dart';
... ...
import 'dart:convert';
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart';
import 'package:doublefeel_flutter/pigeon/platform_api.g.dart';
import 'package:get/get.dart';
... ...
... ... @@ -8,7 +8,7 @@ import 'package:intl/date_symbol_data_local.dart';
import 'app/bootstrap/app_bootstrap.dart';
import 'app/double_feel_app.dart';
import 'core/services/health_raw_data_core_service.dart';
import 'core/services/raw_data_service/ios/health_raw_data_core_service.dart';
import 'core/theme/app_theme.dart';
import 'data/local/user_preferences_storage.dart';
... ...
import 'dart:async';
import 'package:doublefeel_flutter/core/result/app_result.dart';
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/health_raw_local_notification.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_local_notification.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_local_data_convert.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_local_datasource.dart';
import 'package:doublefeel_flutter/data/models/enums/app_enums.dart';
... ...
import 'dart:io';
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/health_raw_local_notification.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/ios/health_raw_local_notification.dart';
import 'package:doublefeel_flutter/l10n/gen/app_localizations_zh.dart';
import 'package:flutter_test/flutter_test.dart';
... ...
import 'package:doublefeel_flutter/core/services/health_sleep_calculator.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_sleep_calculator.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
... ...
import 'package:doublefeel_flutter/core/services/raw_data_service/ohos/ohos_health_raw_data_sync_service.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('syncRawData starts from latest local data time and stores each page',
() async {
final remote = _FakeOhosHealthRawDataRemoteDataSource([
const OhosHealthRawDataPage(
items: [
OhosHealthRawDataItem(
dataType: 2,
dataTime: 1001,
payload: {'value': 70},
),
],
nextPageToken: 'next',
),
const OhosHealthRawDataPage(
items: [
OhosHealthRawDataItem(
dataType: 2,
dataTime: 1002,
payload: {'value': 71},
),
],
),
]);
final local = _FakeOhosHealthRawDataLocalStore(latestDataTime: 1000);
final service = OhosHealthRawDataSyncService(
remoteDataSource: remote,
localStore: local,
nowProvider: () => DateTime.fromMillisecondsSinceEpoch(2000 * 1000),
);
final result = await service.syncRawData(dataType: 2, endTime: 2000);
expect(result.startTime, 1000);
expect(result.endTime, 2000);
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(local.storedBatches.map((e) => e.length), [1, 1]);
});
test('syncRawData falls back to half-year lookback when local data is empty',
() async {
final now = DateTime(2026, 8, 13, 12);
final remote = _FakeOhosHealthRawDataRemoteDataSource([
const OhosHealthRawDataPage(items: <OhosHealthRawDataItem>[]),
]);
final local = _FakeOhosHealthRawDataLocalStore();
final service = OhosHealthRawDataSyncService(
remoteDataSource: remote,
localStore: local,
nowProvider: () => now,
);
final result = await service.syncRawData(dataType: 7);
final expectedStart = now
.subtract(
const Duration(
days: OhosHealthRawDataSyncService.defaultLookbackDays,
),
)
.millisecondsSinceEpoch ~/
1000;
final expectedEnd = now.millisecondsSinceEpoch ~/ 1000;
expect(result.startTime, expectedStart);
expect(result.endTime, expectedEnd);
expect(remote.calls.single.startTime, expectedStart);
expect(remote.calls.single.endTime, expectedEnd);
expect(local.storedBatches, isEmpty);
});
}
class _FakeOhosHealthRawDataRemoteDataSource
implements OhosHealthRawDataRemoteDataSource {
_FakeOhosHealthRawDataRemoteDataSource(this._pages);
final List<OhosHealthRawDataPage> _pages;
final List<_FetchCall> calls = <_FetchCall>[];
@override
Future<OhosHealthRawDataPage> fetchRawDataPage({
required int dataType,
required int startTime,
required int endTime,
String? pageToken,
}) async {
calls.add(
_FetchCall(
dataType: dataType,
startTime: startTime,
endTime: endTime,
pageToken: pageToken,
),
);
return _pages[calls.length - 1];
}
}
class _FakeOhosHealthRawDataLocalStore implements OhosHealthRawDataLocalStore {
_FakeOhosHealthRawDataLocalStore({int? latestDataTime})
: _latestDataTime = latestDataTime;
final int? _latestDataTime;
final List<List<OhosHealthRawDataItem>> storedBatches =
<List<OhosHealthRawDataItem>>[];
@override
Future<int?> latestDataTime({required int dataType}) async {
return _latestDataTime;
}
@override
Future<void> upsertRawDataBatch({
required int dataType,
required List<OhosHealthRawDataItem> items,
}) async {
storedBatches.add(items);
}
}
class _FetchCall {
const _FetchCall({
required this.dataType,
required this.startTime,
required this.endTime,
required this.pageToken,
});
final int dataType;
final int startTime;
final int endTime;
final String? pageToken;
}
... ...
import 'package:doublefeel_flutter/core/services/health_raw_data_core_service.dart';
import 'package:doublefeel_flutter/core/services/raw_data_service/health_raw_models.dart';
import 'package:doublefeel_flutter/data/datasource/health/health_local_data_convert.dart';
import 'package:doublefeel_flutter/data/models/health/sleep/sleep_statistics_data.dart';
import 'package:doublefeel_flutter/pigeon/health_kit_raw_data_api.g.dart';
... ...