developer_options_controller.dart 13.1 KB
import 'package:doublefeel_flutter/app/actions/dialog_action.dart';
import 'package:doublefeel_flutter/app/models/dialog_meta_data.dart';
import 'package:doublefeel_flutter/app/utils/dialog_utils.dart';
import 'package:doublefeel_flutter/core/config/app_environment_config.dart';
import 'package:doublefeel_flutter/core/services/loading_service.dart';
import 'package:doublefeel_flutter/core/services/thinking_data_service.dart';
import 'package:doublefeel_flutter/core/services/user_state_service.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';

import '../../../../core/logging/app_diagnostic_log_store.dart';
import '../../../../core/logging/app_logger.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/services/raw_data_service/platform_ios/apple_health_raw_local_notification.dart';
import '../../../../core/services/raw_data_service/platform_ohos/ohos_harmony_health_raw_data_sync_service_factory.dart';
import '../../../../core/services/raw_data_service/platform_ohos/ohos_health_raw_data_sync_service.dart';
import '../../../../data/local/user_preferences_storage.dart';
import '../../../../l10n/l10n_extensions.dart';
import '../../../../core/platform/pigeon_api_facade.dart';
import '../../../routes/app_pages.dart';

class DeveloperOptionsItem {
  const DeveloperOptionsItem({
    required this.title,
    this.routeName,
    this.action,
  });

  final String title;
  final String? routeName;
  final DeveloperOptionsAction? action;
}

enum DeveloperOptionsAction {
  shareAppDiagnosticLog,
  shareHealthRawDataDatabase,
  shareHealthPipelineRecord,
  sendTestHrvLocalNotification,
  pullOhosRawData,
  clearHealthRawDataDatabaseAndUploadLog,
  globalEnv,
}

class DeveloperOptionsController extends GetxController {
  DeveloperOptionsController(
    this._userPreferencesStorage,
    this._healthRawDataCoreService,
  );

  final UserPreferencesStorage _userPreferencesStorage;
  final HealthRawDataCoreService _healthRawDataCoreService;
  final AppPlatformHostApi _platformHostApi = AppPlatformHostApi();
  final TextEditingController tokenController = TextEditingController();

  late final List<DeveloperOptionsItem> options;

  @override
  void onInit() {
    super.onInit();
    tokenController.text = _userPreferencesStorage.accessToken;
    final config = Get.find<AppEnvironmentConfig>();
    if (config.isDebug) {
      options = const [
        DeveloperOptionsItem(
          title: 'User Information',
          routeName: Routes.USER_INFORMATION,
        ),
        DeveloperOptionsItem(
          title: 'Route List',
          routeName: Routes.ROUTE_LIST,
        ),
        DeveloperOptionsItem(
          title: 'Token Login',
          routeName: Routes.TOKEN_LOGIN,
        ),
        DeveloperOptionsItem(
          title: '分享启动及登录态诊断日志',
          action: DeveloperOptionsAction.shareAppDiagnosticLog,
        ),
        DeveloperOptionsItem(
          title: '分享本地计算数据库文件',
          action: DeveloperOptionsAction.shareHealthRawDataDatabase,
        ),
        DeveloperOptionsItem(
          title: '分享 Health 数据流程日志',
          action: DeveloperOptionsAction.shareHealthPipelineRecord,
        ),
        DeveloperOptionsItem(
          title: '测试本地推送 HRV',
          action: DeveloperOptionsAction.sendTestHrvLocalNotification,
        ),
        DeveloperOptionsItem(
          title: '主动拉取 OHOS 原始数据',
          action: DeveloperOptionsAction.pullOhosRawData,
        ),
        DeveloperOptionsItem(
          title: '清除本地数据库、上传日志',
          action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog,
        ),
        DeveloperOptionsItem(
          title: 'Global Env',
          action: DeveloperOptionsAction.globalEnv,
        ),
      ];
    } else {
      options = const [
        DeveloperOptionsItem(
          title: 'User Information',
          routeName: Routes.USER_INFORMATION,
        ),
        DeveloperOptionsItem(
          title: '分享本地计算数据库文件',
          action: DeveloperOptionsAction.shareHealthRawDataDatabase,
        ),
        DeveloperOptionsItem(
          title: '分享 Health 数据流程日志',
          action: DeveloperOptionsAction.shareHealthPipelineRecord,
        ),
        DeveloperOptionsItem(
          title: '主动拉取 OHOS 原始数据',
          action: DeveloperOptionsAction.pullOhosRawData,
        ),
        DeveloperOptionsItem(
          title: '清除本地数据库、上传日志',
          action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog,
        ),
      ];
    }
  }

  Future<void> optionItemOnTap(DeveloperOptionsItem item) async {
    final routeName = item.routeName;
    if (routeName != null) {
      Get.toNamed(routeName);
      return;
    }

    switch (item.action) {
      case DeveloperOptionsAction.shareAppDiagnosticLog:
        await shareAppDiagnosticLog();
        break;
      case DeveloperOptionsAction.shareHealthRawDataDatabase:
        await shareHealthRawDataDatabase();
        break;
      case DeveloperOptionsAction.shareHealthPipelineRecord:
        await shareHealthPipelineRecord();
        break;
      case DeveloperOptionsAction.sendTestHrvLocalNotification:
        await sendTestHrvLocalNotification();
        break;
      case DeveloperOptionsAction.pullOhosRawData:
        await pullOhosRawData();
        break;
      case DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog:
        await clearHealthRawDataDatabaseAndUploadLog();
        break;
      case DeveloperOptionsAction.globalEnv:
        {
          try {
            final confirmResult = await DialogUtils.showCommonDialog(
                DialogMetaData(
                    title: '选择服务器环境',
                    message: '',
                    cancelText: '国内版',
                    confirmText: '国际版',
                    iconAsset: null));
            if (confirmResult.action == DialogAction.confirm ||
                confirmResult.action == DialogAction.cancel) {
              final environmentConfig = Get.find<AppEnvironmentConfig>();
              final region = confirmResult.action == DialogAction.cancel
                  ? AppRegion.china
                  : AppRegion.global;
              if (environmentConfig.region.value == region) return;
              await environmentConfig.setRegion(region);
              ta.setSuperProperties({
                'app_region': environmentConfig.region.value == AppRegion.china
                    ? 'mainland'
                    : 'overseas'
              });

              await LoadingService.instance.run(() async {
                await Get.find<UserStateService>().onLogout();
              });
              Get.offAllNamed(AppRoutes.login);
            }
          } catch (error, stackTrace) {
            AppLogger.e('Set global env failed', error, stackTrace);
          }
        }
        break;
      case null:
        break;
    }
  }

  Future<void> shareAppDiagnosticLog() async {
    try {
      final bytes = await AppDiagnosticLogStore.instance.readBytes();
      await _platformHostApi.shareFileData(bytes);
    } catch (error, stackTrace) {
      AppLogger.e('Share AppDiagnosticLog failed', error, stackTrace);
      Get.snackbar('Share failed', error.toString());
    }
  }

  Future<void> shareHealthRawDataDatabase() async {
    try {
      final bytes = await _healthRawDataCoreService.readDatabaseFileBytes();
      await _platformHostApi.shareFileData(bytes);
    } catch (error, stackTrace) {
      AppLogger.e('Share HealthRawData database failed', error, stackTrace);
      Get.snackbar('Share failed', error.toString());
    }
  }

  Future<void> shareHealthRawUploadLog() async {
    try {
      final text = await _healthRawDataCoreService.readUploadApiLogText();
      await _platformHostApi.shareText(text);
    } catch (error, stackTrace) {
      AppLogger.e('Share HealthRawData upload log failed', error, stackTrace);
      Get.snackbar('Share failed', error.toString());
    }
  }

  Future<void> shareUploadTaskRecord() async {
    try {
      await _healthRawDataCoreService.shareUploadTaskRecord();
    } catch (error, stackTrace) {
      AppLogger.e(
        'Share upload task record failed',
        error,
        stackTrace,
      );
      Get.snackbar('Share failed', error.toString());
    }
  }

  Future<void> shareHealthPipelineRecord() async {
    try {
      final sharedByNative =
          await _healthRawDataCoreService.shareLocalNotificationDebugRecord();
      if (sharedByNative) return;
      Get.snackbar('Share failed', 'Health data pipeline log is unavailable');
    } catch (error, stackTrace) {
      AppLogger.e(
        'Share health pipeline record failed',
        error,
        stackTrace,
      );
      Get.snackbar('Share failed', error.toString());
    }
  }

  Future<void> sendTestHrvLocalNotification() async {
    try {
      final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
      final notifications = HealthRawLocalNotificationBuilder(l10n).build(
        result: HealthRawStressCalculationResult(
          userId: _userPreferencesStorage.preferences.value.meUserInfo?.id ?? 0,
          hrvStressPoints: [
            HealthRawHrvStressPoint(
              userId:
                  _userPreferencesStorage.preferences.value.meUserInfo?.id ?? 0,
              rawEndTime: now,
              rawHrv: 35,
              result: 32,
              sourceStartTime: now,
              sourceEndTime: now,
              state: HealthRawStressState.excellent,
              baselineHrv: 30,
              baselineAwakeHrv: 30,
              baselineSleepHrv: null,
              baselineRestingHr: 60,
            ),
          ],
          realtimeStressPoints: const [],
          dailyStressPoints: const [],
          sleepResults: const [],
        ),
        hasExistingHrv: true,
        hasExistingSleep: false,
        realtimeWindow: const [],
        record: const HealthRawLocalNotificationRecord(),
      );
      final notification = notifications.firstWhereOrNull(
        (item) => item.recordType == HealthRawLocalNotificationRecordType.hrv,
      );
      if (notification == null) {
        Get.snackbar('Send failed', 'No HRV notification was built');
        return;
      }

      final userId =
          _userPreferencesStorage.preferences.value.meUserInfo?.id ?? 0;
      final sent = await HealthRawLocalNotificationDispatcher().sendOne(
        userId: userId,
        notification: notification,
      );
      Get.snackbar(
        sent ? '已触发 HRV 本地推送' : 'HRV 本地推送发送失败',
        '${notification.title}\n${notification.content}',
      );
    } catch (error, stackTrace) {
      AppLogger.e(
        'Send test HRV local notification failed',
        error,
        stackTrace,
      );
      Get.snackbar('Send failed', error.toString());
    }
  }

  Future<void> pullOhosRawData() async {
    try {
      final userId =
          _userPreferencesStorage.preferences.value.meUserInfo?.id ?? 0;
      if (userId <= 0) {
        Get.snackbar('OHOS 拉取失败', '当前用户 id 无效');
        return;
      }
      final syncService = createDefaultOhosHealthRawDataSyncService(
        userIdProvider: () => userId,
      );
      final now = DateTime.now();
      final endTime = now.millisecondsSinceEpoch ~/ 1000;
      final startTime =
          now.subtract(const Duration(days: 7)).millisecondsSinceEpoch ~/ 1000;
      await LoadingService.instance.run(() async {
        AppLogger.i(
          '${OhosHealthRawDataSyncService.logMarker} '
          'developer_options_pull_calculation_start '
          'startTime=$startTime endTime=$endTime',
        );
        final results = await syncService.syncCalculationRawData(
          startTime: startTime,
          endTime: endTime,
        );

        final storedCount = results.fold<int>(
          0,
          (sum, result) => sum + result.storedCount,
        );
        final pageCount = results.fold<int>(
          0,
          (sum, result) => sum + result.pageCount,
        );
        Get.snackbar(
          'OHOS 原始数据拉取完成',
          '类型 ${results.length} 个,页数 $pageCount,写入 $storedCount 条',
        );
      });
    } catch (error, stackTrace) {
      AppLogger.e('Pull OHOS raw data failed', error, stackTrace);
      Get.snackbar('OHOS 拉取失败', error.toString());
    }
  }

  Future<void> clearHealthRawDataDatabaseAndUploadLog() async {
    try {
      await _healthRawDataCoreService.clearLocalDatabaseAndUploadLog();
      Get.snackbar('已清除', '本地数据库和上传日志已清除');
    } catch (error, stackTrace) {
      AppLogger.e(
        'Clear HealthRawData database and upload log failed',
        error,
        stackTrace,
      );
      Get.snackbar('Clear failed', error.toString());
    }
  }

  Future<void> saveAccessToken() async {
    final token = tokenController.text.trim();
    await _userPreferencesStorage.updateAccessToken(token);
    Get.offAllNamed(AppRoutes.splash);
  }

  @override
  void onClose() {
    tokenController.dispose();
    super.onClose();
  }
}