health_local_datasource.dart 10.1 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),
      );

      return AppSuccess(
        LocalHealthDataConvert.hrvStatistics(
          dateRangeType: dateRangeType,
          days: days,
          previousDays: previousDays,
          hrvPoints: hrvPoints,
          dailyStressPoints: dailyStressPoints,
        ),
      );
    } 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) async {
    try {
      final now = DateTime.now();
      final queryStart = LocalHealthDataConvert.unixSeconds(
        now.subtract(
            const Duration(days: HealthRawDataCoreService.defaultLookbackDays)),
      );
      final queryEnd = LocalHealthDataConvert.unixSeconds(now);
      final activity = await coreService.queryRawActivitySummaries(
        startTime: queryStart,
        endTime: queryEnd,
      );
      return AppSuccess(
        LocalHealthDataConvert.v2ActivityTarget(
          userId: 0,
          activity: activity,
        ),
      );
    } 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 = sleepResults.isEmpty ? null : sleepResults.last;
      final sleepingHeartRate = await _querySleepHeartRate(
        sleepResult == null ? const [] : [sleepResult],
      );
      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,
        ),
      );
    } 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,
    );
  }
}