main.dart 6.52 KB
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:intl/date_symbol_data_local.dart';

import 'app/bootstrap/app_bootstrap.dart';
import 'app/double_feel_app.dart';
import 'core/config/app_environment_config.dart';
import 'core/services/raw_data_service/health_raw_data_core_service.dart';
import 'core/services/raw_data_service/health_raw_models.dart';
import 'core/theme/app_theme.dart';
import 'data/local/user_preferences_storage.dart';
import 'pigeon/health_kit_raw_data_api.g.dart';

Future<void> main() async {
  runZonedGuarded<Future<Null>>(
    () async {
      WidgetsFlutterBinding.ensureInitialized();
      FlutterError.onError = (errorDetails) {};
      PlatformDispatcher.instance.onError = (error, stack) {
        return true;
      };

      SystemChrome.setSystemUIOverlayStyle(AppTheme.systemUiOverlayStyle);

      await AppBootstrap.init();
      await initializeDateFormatting();
      runApp(const DoubleFeelApp());
    },
    (error, stackTrace) async {
      if (kDebugMode) {}
    },
  );
}

@pragma('vm:entry-point')
Future<void> healthBackgroundMain() async {
  WidgetsFlutterBinding.ensureInitialized();
  const channel = MethodChannel('doublefeel_flutter_health_background_channel');

  await _healthBackgroundLog(channel, 'entry.start');
  try {
    await _healthBackgroundLog(channel, 'bootstrap.start');
    await AppBootstrap.initForBackground();
    await _healthBackgroundLog(channel, 'bootstrap.success');
  } catch (error, stackTrace) {
    await _healthBackgroundLog(
      channel,
      'bootstrap.failed error=$error stack=$stackTrace',
    );
    rethrow;
  }

  channel.setMethodCallHandler((call) async {
    switch (call.method) {
      case 'runPendingHealthTasks':
        await _runPendingHealthTasks(channel, call.arguments);
        return true;
      default:
        return null;
    }
  });

  await channel.invokeMethod('healthBackgroundTaskReady', {
    'timestamp': DateTime.now().millisecondsSinceEpoch ~/ 1000,
  });
  await _healthBackgroundLog(channel, 'entry.readySent');
}

Future<void> _runPendingHealthTasks(
  MethodChannel channel,
  Object? arguments,
) async {
  final tasks = _backgroundHealthTasksFromArguments(arguments);
  await _healthBackgroundLog(
    channel,
    'tasks.received count=${tasks.length} ids=${tasks.map((e) => e.taskId).join(',')}',
  );
  await _recordTemporaryHealthTaskReceived(tasks);
  if (tasks.isEmpty) return;

  for (final task in tasks) {
    await channel.invokeMethod('healthBackgroundTaskReceived', {
      'taskId': task.taskId,
    });
  }

  final userPrefs = Get.find<UserPreferencesStorage>();
  final userId = userPrefs.preferences.value.meUserInfo?.id ?? 0;
  final token = userPrefs.accessToken;
  await _healthBackgroundLog(
    channel,
    'login.check userId=$userId hasToken=${token.isNotEmpty}',
  );
  if (userId <= 0 || token.isEmpty) {
    for (final task in tasks) {
      await channel.invokeMethod('healthBackgroundTaskFinished', {
        'taskId': task.taskId,
        'success': false,
        'retryable': true,
        'error': 'missingLogin',
      });
    }
    return;
  }

  try {
    await _healthBackgroundLog(channel, 'calculate.start');
    final calculation =
        await Get.find<HealthRawDataCoreService>().startCoreCaculate();
    final hasNewResult = _hasNewHealthCalculationResult(calculation);
    await _healthBackgroundLog(
      channel,
      'calculate.success hasNewResult=$hasNewResult',
    );
    for (final task in tasks) {
      await channel.invokeMethod('healthBackgroundTaskFinished', {
        'taskId': task.taskId,
        'success': true,
        'calculated': hasNewResult,
      });
    }
  } catch (error) {
    await _healthBackgroundLog(channel, 'calculate.failed error=$error');
    for (final task in tasks) {
      await channel.invokeMethod('healthBackgroundTaskFinished', {
        'taskId': task.taskId,
        'success': false,
        'retryable': true,
        'error': error.toString(),
      });
    }
  }
}

Future<void> _recordTemporaryHealthTaskReceived(
  List<_BackgroundHealthTask> tasks,
) async {
  if (!_isHealthPipelineDebugEnabled) {
    return;
  }
  try {
    await HealthKitRawDataHostApi().saveLocalNotificationDebugEvent(
      event: <String, Object?>{
        'event': 'flutterHealthDataReceived',
        'identifier': 'flutter.health_data.received',
        'engine_type': 'temporary',
        'task_ids': tasks.map((task) => task.taskId).toList(growable: false),
        'data_types': tasks.expand((task) => task.dataTypes).toSet().toList()
          ..sort(),
      },
    ).timeout(const Duration(seconds: 1));
  } catch (_) {
    // Pipeline telemetry must never delay a HealthKit background task.
  }
}

bool _hasNewHealthCalculationResult(HealthRawStressCalculationResult result) {
  return result.hrvStressPoints.isNotEmpty ||
      result.realtimeStressPoints.isNotEmpty ||
      result.dailyStressPoints.isNotEmpty ||
      result.sleepResults.isNotEmpty;
}

Future<void> _healthBackgroundLog(MethodChannel channel, String message) async {
  if (!_isHealthPipelineDebugEnabled) {
    return;
  }
  try {
    await channel.invokeMethod('healthBackgroundLog', {
      'message': message,
      'timestamp': DateTime.now().millisecondsSinceEpoch ~/ 1000,
    });
  } catch (_) {}
}

bool get _isHealthPipelineDebugEnabled {
  if (!Get.isRegistered<AppEnvironmentConfig>()) {
    return false;
  }
  return Get.find<AppEnvironmentConfig>().isDebug;
}

List<_BackgroundHealthTask> _backgroundHealthTasksFromArguments(
  Object? arguments,
) {
  final rawTasksValue =
      arguments is Map<Object?, Object?> ? arguments['tasks'] : null;
  final rawTasks =
      rawTasksValue is List ? rawTasksValue.cast<Object?>() : const <Object?>[];
  return rawTasks
      .whereType<Map<Object?, Object?>>()
      .map(_BackgroundHealthTask.fromMap)
      .whereType<_BackgroundHealthTask>()
      .toList(growable: false);
}

class _BackgroundHealthTask {
  const _BackgroundHealthTask({
    required this.taskId,
    required this.dataTypes,
  });

  final String taskId;
  final List<int> dataTypes;

  static _BackgroundHealthTask? fromMap(Map<Object?, Object?> map) {
    final taskId = map['taskId'];
    if (taskId is! String || taskId.isEmpty) return null;
    final dataTypes = (map['dataTypes'] as Iterable<Object?>?)
            ?.whereType<num>()
            .map((value) => value.toInt())
            .toList(growable: false) ??
        const <int>[];
    return _BackgroundHealthTask(taskId: taskId, dataTypes: dataTypes);
  }
}