developer_options_controller.dart 11.4 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_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 '../../../../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 {
  shareHealthRawDataDatabase,
  shareHealthRawUploadLog,
  shareNativeAppleHealthObserverRecord,
  shareFlutterObserverRecord,
  shareUploadTaskRecord,
  shareLocalNotificationDebugRecord,
  sendTestHrvLocalNotification,
  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();

  final List<DeveloperOptionsItem> 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: 'Share HealthRawData Database',
      action: DeveloperOptionsAction.shareHealthRawDataDatabase,
    ),
    DeveloperOptionsItem(
      title: '本地上传日志',
      action: DeveloperOptionsAction.shareHealthRawUploadLog,
    ),
    DeveloperOptionsItem(
      title: '分享原生AppleHealthObserver记录',
      action: DeveloperOptionsAction.shareNativeAppleHealthObserverRecord,
    ),
    DeveloperOptionsItem(
      title: '分享Flutter Observer-Caculate记录',
      action: DeveloperOptionsAction.shareFlutterObserverRecord,
    ),
    DeveloperOptionsItem(
      title: '分享上传Task日志',
      action: DeveloperOptionsAction.shareUploadTaskRecord,
    ),
    DeveloperOptionsItem(
      title: '分享本地推送诊断日志',
      action: DeveloperOptionsAction.shareLocalNotificationDebugRecord,
    ),
    DeveloperOptionsItem(
      title: '测试本地推送 HRV',
      action: DeveloperOptionsAction.sendTestHrvLocalNotification,
    ),
    DeveloperOptionsItem(
      title: '清除本地数据库、上传日志',
      action: DeveloperOptionsAction.clearHealthRawDataDatabaseAndUploadLog,
    ),
    DeveloperOptionsItem(
      title: 'Global Env',
      action: DeveloperOptionsAction.globalEnv,
    ),
  ];

  @override
  void onInit() {
    super.onInit();
    tokenController.text = _userPreferencesStorage.accessToken;
  }

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

    switch (item.action) {
      case DeveloperOptionsAction.shareHealthRawDataDatabase:
        await shareHealthRawDataDatabase();
        break;
      case DeveloperOptionsAction.shareHealthRawUploadLog:
        await shareHealthRawUploadLog();
        break;
      case DeveloperOptionsAction.shareNativeAppleHealthObserverRecord:
        await shareNativeAppleHealthObserverRecord();
        break;
      case DeveloperOptionsAction.shareFlutterObserverRecord:
        await shareFlutterObserverRecord();
        break;
      case DeveloperOptionsAction.shareUploadTaskRecord:
        await shareUploadTaskRecord();
        break;
      case DeveloperOptionsAction.shareLocalNotificationDebugRecord:
        await shareLocalNotificationDebugRecord();
        break;
      case DeveloperOptionsAction.sendTestHrvLocalNotification:
        await sendTestHrvLocalNotification();
        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> 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> shareNativeAppleHealthObserverRecord() async {
    try {
      await _healthRawDataCoreService.shareNativeAppleHealthObserverRecord();
    } catch (error, stackTrace) {
      AppLogger.e(
        'Share native AppleHealthObserver record failed',
        error,
        stackTrace,
      );
      Get.snackbar('Share failed', error.toString());
    }
  }

  Future<void> shareFlutterObserverRecord() async {
    try {
      await _healthRawDataCoreService.shareFlutterObserverRecord();
    } catch (error, stackTrace) {
      AppLogger.e(
        'Share Flutter Observer-Caculate record 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> shareLocalNotificationDebugRecord() async {
    try {
      final sharedByNative =
          await _healthRawDataCoreService.shareLocalNotificationDebugRecord();
      if (sharedByNative) return;
      final text =
          await _healthRawDataCoreService.readLocalNotificationDebugLogText();
      await _platformHostApi.shareText(text);
    } catch (error, stackTrace) {
      AppLogger.e(
        'Share local notification debug 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> 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();
  }
}