health_local_datasource.dart 12.7 KB
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/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/data/models/health/hrv/hrv_statistics_data.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';

import 'health_datasource.dart';
import 'health_local_data_convert.dart';

class LocalHealthDataSource implements HealthDataSource {
  const LocalHealthDataSource({required this.coreService});

  final HealthRawDataCoreService coreService;

  @override
  Future<AppResult<SleepStatisticsData>> getSleepStatistics(
    int dateRangeType,
    int startDate, {
    int? queryUserId,
  }) async {
    try {
      final days = LocalHealthDataConvert.rangeDays(dateRangeType, startDate);
      if (days.isEmpty) return AppSuccess(SleepStatisticsData());

      final previousDays = LocalHealthDataConvert.previousRangeDays(
        dateRangeType,
        startDate,
      );
      final allDays = [...previousDays, ...days];
      final queryStart = LocalHealthDataConvert.unixSeconds(
        allDays.first,
      );
      final queryEnd = LocalHealthDataConvert.unixSeconds(
            allDays.last.add(const Duration(days: 1)),
          ) -
          1;
      final sleepResults = await coreService.querySleepResults(
        startTime: queryStart,
        endTime: queryEnd,
      );
      final heartRate = dateRangeType == 3
          ? await _querySleepHeartRate(sleepResults)
          : const <HealthKitRawDataPoint>[];

      return AppSuccess(
        LocalHealthDataConvert.sleepStatisticsFromResults(
          dateRangeType: dateRangeType,
          days: days,
          previousDays: previousDays,
          sleepResults: sleepResults,
          heartRate: heartRate,
        ),
      );
    } catch (error) {
      return AppFailure(AppUnknownError(error));
    }
  }

  @override
  Future<AppResult<ActivityBurnStatisticsDataV2>> getActivityBurnStatistics(
    int dateRangeType,
    int startDate, {
    int? queryUserId,
  }) async {
    try {
      final days = LocalHealthDataConvert.rangeDays(dateRangeType, startDate);
      if (days.isEmpty) return AppSuccess(ActivityBurnStatisticsDataV2());

      final previousDays = LocalHealthDataConvert.previousRangeDays(
        dateRangeType,
        startDate,
      );
      final allDays = [...previousDays, ...days];
      final queryStart = LocalHealthDataConvert.unixSeconds(allDays.first);
      final queryEnd = LocalHealthDataConvert.unixSeconds(
        days.last.add(const Duration(days: 1)),
      );
      final activity = await coreService.queryRawActivitySummaries(
        startTime: queryStart,
        endTime: queryEnd,
      );
      final heartRate = await coreService.queryRawDataPoints(
        dataType: HealthDataUploadType.heartRate.type,
        startTime: queryStart,
        endTime: queryEnd,
      );

      return AppSuccess(
        LocalHealthDataConvert.activityBurnStatistics(
          days: days,
          previousDays: previousDays,
          activity: activity,
          heartRate: heartRate,
          sleepIntervals: [],
        ),
      );
    } catch (error) {
      return AppFailure(AppUnknownError(error));
    }
  }

  @override
  Future<AppResult<HrvStatisticsDataV2>> getHrvStatistics(
    int dateRangeType,
    int startDate, {
    int? queryUserId,
  }) async {
    try {
      final days = LocalHealthDataConvert.rangeDays(dateRangeType, startDate);
      if (days.isEmpty) return AppSuccess(HrvStatisticsDataV2());

      final previousDays = LocalHealthDataConvert.previousRangeDays(
        dateRangeType,
        startDate,
      );
      final hrvPoints = await coreService.queryHrvStressPoints(
        startTime: LocalHealthDataConvert.unixSeconds(days.first),
        endTime: LocalHealthDataConvert.unixSeconds(
          days.last.add(const Duration(days: 1)),
        ),
      );
      final dailyStressPoints = await coreService.queryDailyStressPoints(
        startDate: LocalHealthDataConvert.dateKey(
            previousDays.isEmpty ? days.first : previousDays.first),
        endDate: LocalHealthDataConvert.dateKey(days.last),
      );
      final restingHeartRate = await coreService.queryRawDataPoints(
        dataType: HealthDataUploadType.restingHeartRate.type,
        startTime: LocalHealthDataConvert.unixSeconds(days.first),
        endTime: LocalHealthDataConvert.unixSeconds(
          days.last.add(const Duration(days: 1)),
        ),
      );

      return AppSuccess(
        LocalHealthDataConvert.hrvStatistics(
          dateRangeType: dateRangeType,
          days: days,
          previousDays: previousDays,
          hrvPoints: hrvPoints,
          dailyStressPoints: dailyStressPoints,
          restingHeartRate: restingHeartRate,
        ),
      );
    } catch (error) {
      return AppFailure(AppUnknownError(error));
    }
  }

  @override
  Future<AppResult<V2HealthEverUploaded>> getHealthDataEverUploaded() async {
    try {
      final startTime = await coreService.queryEarliestHrRawEndTime();
      return AppSuccess(
        V2HealthEverUploaded(
          flag: true,
          startTime: startTime,
        ),
      );
    } catch (error) {
      return AppFailure(AppUnknownError(error));
    }
  }

  @override
  Future<AppResult<V2ActivityTarget>> getV2ActivityTarget(
      int? queryUserId, int intDate) async {
    try {
      final day = LocalHealthDataConvert.dateFromKey(intDate);
      final queryStart = LocalHealthDataConvert.unixSeconds(day);
      final queryEnd = LocalHealthDataConvert.unixSeconds(
        day.add(const Duration(days: 1)),
      );
      final activity = await coreService.queryRawActivitySummaries(
        startTime: queryStart,
        endTime: queryEnd,
      );
      final resolvedActivity = _resolveActivityTarget(activity);
      return AppSuccess(
        LocalHealthDataConvert.v2ActivityTarget(
          userId: queryUserId ?? 0,
          activity: resolvedActivity == null
              ? const <HealthKitRawActivityDataPoint>[]
              : [resolvedActivity],
        ),
      );
    } catch (error) {
      return AppFailure(AppUnknownError(error));
    }
  }

  @override
  Future<AppResult<V2HealthData>> getV2HealthData(
      int? queryUserId, int intDate) async {
    try {
      final day = LocalHealthDataConvert.dateFromKey(intDate);
      final dayStart = LocalHealthDataConvert.unixSeconds(day);
      final dayEnd = LocalHealthDataConvert.unixSeconds(
            day.add(const Duration(days: 1)),
          ) -
          1;
      final hrvPoints = await coreService.queryHrvStressPoints(
        startTime: dayStart,
        endTime: dayEnd,
      );
      final activity = await coreService.queryRawActivitySummaries(
        startTime: dayStart,
        endTime: dayEnd,
      );
      final sleepResults = await coreService.querySleepResults(
        startTime: dayStart,
        endTime: dayEnd,
      );
      final sleepResult = LocalHealthDataConvert.sleepResultForDay(
        day,
        sleepResults,
      );
      final sleepingHeartRate = await _querySleepHeartRate(
        sleepResult == null ? const [] : [sleepResult],
      );
      final sleepStatistics = LocalHealthDataConvert.sleepStatisticsFromResults(
        dateRangeType: 3,
        days: [day],
        previousDays: const [],
        sleepResults: sleepResults,
        heartRate: sleepingHeartRate,
      );
      final sleepTrend = sleepStatistics.sleepTrendList?.isEmpty ?? true
          ? null
          : sleepStatistics.sleepTrendList!.first;
      final restingHeartRate = await coreService.queryRawDataPoints(
        dataType: HealthDataUploadType.restingHeartRate.type,
        startTime: dayStart,
        endTime: dayEnd,
      );
      return AppSuccess(
        LocalHealthDataConvert.v2HealthDataFromSleepResult(
          day: day,
          hrvPoints: hrvPoints,
          activity: activity,
          sleepingHeartRate: sleepingHeartRate,
          restingHeartRate: restingHeartRate,
          sleepResult: sleepResult,
          sleepTrend: sleepTrend,
        ),
      );
    } catch (error) {
      return AppFailure(AppUnknownError(error));
    }
  }

  @override
  Future<AppResult<V2HrvTrendData>> getV2HrvTrend(
      int? queryUserId, int intDate) async {
    try {
      final (startTime, endTime) = _dayRange(intDate);
      final hrvPoints = await coreService.queryHrvStressPoints(
        startTime: startTime,
        endTime: endTime,
      );
      return AppSuccess(LocalHealthDataConvert.v2HrvTrendData(hrvPoints));
    } catch (error) {
      return AppFailure(AppUnknownError(error));
    }
  }

  @override
  Future<AppResult<V2LatestHrvData>> getV2LatestHrv(
      int? queryUserId, int intDate) async {
    try {
      final (startTime, endTime) = _dayRange(intDate);
      final hrvPoints = await coreService.queryHrvStressPoints(
        startTime: startTime,
        endTime: endTime,
      );
      return AppSuccess(
        LocalHealthDataConvert.v2LatestHrvData(hrvPoints),
      );
    } catch (error) {
      return AppFailure(AppUnknownError(error));
    }
  }

  @override
  Future<AppResult<V2RealtimeStressData>> getV2RealtimeStress(
      int? queryUserId, int intDate) async {
    try {
      final (startTime, endTime) = _dayRange(intDate);
      final realtimePoints = await coreService.queryRealtimeStressPoints(
        startTime: startTime,
        endTime: endTime,
      );
      return AppSuccess(
        LocalHealthDataConvert.v2RealtimeStressData(realtimePoints),
      );
    } catch (error) {
      return AppFailure(AppUnknownError(error));
    }
  }

  @override
  Future<AppResult<V2StressScore>> getV2StressScore(
      int? queryUserId, int intDate) async {
    try {
      final dailyStressPoints = await coreService.queryDailyStressPoints(
        startDate: intDate,
        endDate: intDate,
      );
      return AppSuccess(
        LocalHealthDataConvert.v2StressScore(
          dailyStressPoints.isEmpty ? null : dailyStressPoints.last,
        ),
      );
    } catch (error) {
      return AppFailure(AppUnknownError(error));
    }
  }

  (int startTime, int endTime) _dayRange(int intDate) {
    final day = LocalHealthDataConvert.dateFromKey(intDate);
    return (
      LocalHealthDataConvert.unixSeconds(day),
      LocalHealthDataConvert.unixSeconds(day.add(const Duration(days: 1))) - 1,
    );
  }

  Future<List<HealthKitRawDataPoint>> _querySleepHeartRate(
    List<HealthRawSleepResult> sleepResults,
  ) async {
    if (sleepResults.isEmpty) return const [];
    final startTime =
        sleepResults.map((e) => e.startDate).reduce((a, b) => a < b ? a : b);
    final endTime =
        sleepResults.map((e) => e.date).reduce((a, b) => a > b ? a : b);
    return coreService.queryRawDataPoints(
      dataType: HealthDataUploadType.heartRate.type,
      startTime: startTime,
      endTime: endTime,
    );
  }

  HealthKitRawActivityDataPoint? _resolveActivityTarget(
    List<HealthKitRawActivityDataPoint> activity,
  ) {
    double? activeEnergyBurnedGoal;
    double? exerciseTimeGoal;
    double? standHoursGoal;
    var resolvedEndTime = 0;
    final reversed = [...activity]
      ..sort((a, b) => b.endTime.compareTo(a.endTime));

    for (final item in reversed) {
      if (activeEnergyBurnedGoal == null &&
          _isPositive(item.activeEnergyBurnedGoal)) {
        activeEnergyBurnedGoal = item.activeEnergyBurnedGoal?.ceilToDouble();
        resolvedEndTime =
            resolvedEndTime < item.endTime ? item.endTime : resolvedEndTime;
      }
      if (exerciseTimeGoal == null && _isPositive(item.exerciseTimeGoal)) {
        exerciseTimeGoal = item.exerciseTimeGoal;
        resolvedEndTime =
            resolvedEndTime < item.endTime ? item.endTime : resolvedEndTime;
      }
      if (standHoursGoal == null && _isPositive(item.standHoursGoal)) {
        standHoursGoal = item.standHoursGoal;
        resolvedEndTime =
            resolvedEndTime < item.endTime ? item.endTime : resolvedEndTime;
      }
      if (activeEnergyBurnedGoal != null &&
          exerciseTimeGoal != null &&
          standHoursGoal != null) {
        break;
      }
    }

    if (activeEnergyBurnedGoal == null &&
        exerciseTimeGoal == null &&
        standHoursGoal == null) {
      return null;
    }
    return HealthKitRawActivityDataPoint(
      endTime: resolvedEndTime,
      activeEnergyBurnedGoal: activeEnergyBurnedGoal,
      exerciseTimeGoal: exerciseTimeGoal,
      standHoursGoal: standHoursGoal,
    );
  }

  bool _isPositive(num? value) => value != null && value > 0;
}