Files
FlClash/lib/bootstrap.dart
chen08209 bc72bba981 feat(android): drop Firebase analytics and crash reporting
Changelog: No more Firebase analytics or crash reporting
2026-10-17 17:21:43 +08:00

263 lines
8.8 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:fl_clash/common/boot_guard.dart';
import 'package:fl_clash/common/boot_record.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/common/launch.dart';
import 'package:fl_clash/common/migration.dart';
import 'package:fl_clash/common/tray.dart';
import 'package:fl_clash/common/window.dart';
import 'package:fl_clash/database/database.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/l10n/l10n.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/views/disclaimer.dart';
import 'package:fl_clash/views/navigation.dart';
import 'package:material_ui/material_ui.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:material_color_utilities/palettes/tonal_palette.dart';
import 'package:package_info_plus/package_info_plus.dart';
class Bootstrap {
static Bootstrap? _instance;
Bootstrap._internal();
factory Bootstrap() {
_instance ??= Bootstrap._internal();
return _instance!;
}
BootDecision _bootDecision = const BootDecision();
Future<ProviderContainer> init(int version) async {
globalState.appEnv = const String.fromEnvironment(
'APP_ENV',
defaultValue: 'pre',
);
windowPort = window;
trayPort = appTray;
navigationPort = navigation;
final dynamicColor = await _initDynamicColor();
return _initData(version, dynamicColor);
}
Future<DynamicColorSeeds> _initDynamicColor() async {
TonalPalette? primaryPalette;
Color? accentColor;
try {
primaryPalette = await _getSystemPrimaryPalette();
} catch (error) {
commonPrint.log(
'Failed to get core palette: $error',
logLevel: LogLevel.warning,
);
}
try {
accentColor = await DynamicColorPlugin.getAccentColor();
} catch (error) {
commonPrint.log(
'Failed to get accent color: $error',
logLevel: LogLevel.warning,
);
}
return (
lightSeed: primaryPalette != null ? Color(primaryPalette.get(40)) : null,
darkSeed: primaryPalette != null ? Color(primaryPalette.get(80)) : null,
accentColor: accentColor ?? const Color(defaultPrimaryColor),
);
}
/// Runs before migration and the database: a copy started from another
/// install path gets past the platform's single-instance hand-off.
Future<void> _claimDataDirectory() async {
if (!system.isDesktop || await singleInstanceLock.acquire()) {
return;
}
commonPrint.log('another instance owns the data directory, exiting');
exit(0);
}
Future<TonalPalette?> _getSystemPrimaryPalette() async {
final raw = await DynamicColorPlugin.channel.invokeMethod<List<dynamic>>(
DynamicColorPlugin.methodName,
);
if (raw == null || raw.length < TonalPalette.commonSize) {
return null;
}
return TonalPalette.fromList(
raw.sublist(0, TonalPalette.commonSize).cast<int>(),
);
}
Future<ProviderContainer> _initData(
int version,
DynamicColorSeeds dynamicColor,
) async {
await _claimDataDirectory();
globalState.packageInfo = await PackageInfo.fromPlatform();
var config = await migration.run();
_bootDecision = await bootGuard.evaluate(
profileId: config.currentProfileId,
);
if (_bootDecision.recovery == BootRecovery.clearProfile) {
config = config.copyWith(currentProfileId: null);
await preferences.saveConfig(config);
}
final patchClashConfig = config.patchClashConfig.ensureControllerSecret();
if (patchClashConfig != config.patchClashConfig) {
config = config.copyWith(patchClashConfig: patchClashConfig);
await preferences.saveConfig(config);
}
final appState = AppState(
brightness: WidgetsBinding.instance.platformDispatcher.platformBrightness,
version: version,
viewSize: Size.zero,
requests: FixedList(maxRequestsLength),
logs: FixedList(maxLogsLength),
traffics: FixedList(trafficSampleLength),
totalTraffic: const Traffic(),
systemUiOverlayStyle: const SystemUiOverlayStyle(),
);
final appStateOverrides = buildAppStateOverrides(appState);
final configOverrides = buildConfigOverrides(config);
final container = ProviderContainer(
overrides: [...appStateOverrides, ...configOverrides],
);
globalState.container = container;
container
.read(dynamicColorProvider.notifier)
.seed(
lightSeed: dynamicColor.lightSeed,
darkSeed: dynamicColor.darkSeed,
accentColor: dynamicColor.accentColor,
);
final profiles = await database.profilesDao.query().get();
container.read(profilesProvider.notifier).reorder(profiles);
await AppLocalizations.load(
getLocaleForString(config.appSettingProps.locale) ??
WidgetsBinding.instance.platformDispatcher.locale,
);
await window?.init(version, config.windowProps);
if (system.isAndroid) {
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
}
return container;
}
Future<void> attach() async {
if (globalState.isAttach == true) {
return;
}
await _initApp();
globalState.isAttach = true;
}
ProviderContainer get _container => globalState.container;
Future<void> _initApp() async {
unawaited(_container.read(systemActionProvider.notifier).updateTray());
unawaited(
_container.read(profilesActionProvider.notifier).autoUpdateProfiles(),
);
unawaited(_container.read(commonActionProvider.notifier).autoCheckUpdate());
unawaited(
autoLaunch?.updateStatus(_container.read(appSettingProvider).autoLaunch),
);
if (!_container.read(appSettingProvider).silentLaunch) {
unawaited(window?.show());
} else {
unawaited(window?.hide());
}
await _handleFailedPreference();
if (!await _handlerDisclaimer()) {
return;
}
await _showCrashRecoveryTip();
await _container.read(coreActionProvider.notifier).startCore();
if (!_bootDecision.isDegraded) {
await _container.read(setupActionProvider.notifier).initStatus();
}
_container.read(initProvider.notifier).value = true;
unawaited(_container.read(resourcesActionProvider.notifier).autoUpdate());
await bootGuard.markRunning();
unawaited(_container.read(locationPermissionsProvider.notifier).refresh());
}
Future<void> _showCrashRecoveryTip() async {
switch (_bootDecision.recovery) {
case BootRecovery.none:
return;
case BootRecovery.skipAutoSetup:
await dialogs.showMessage(
title: currentAppLocalizations.launchInterrupted,
cancelable: false,
dismissible: false,
message: TextSpan(text: currentAppLocalizations.launchInterruptedTip),
);
case BootRecovery.clearProfile:
await dialogs.showMessage(
title: currentAppLocalizations.crashDetected,
cancelable: false,
dismissible: false,
message: TextSpan(
text: currentAppLocalizations.crashDetectedTip(_failedProfileLabel),
),
);
}
}
String get _failedProfileLabel {
final profileId = _bootDecision.failedProfileId;
if (profileId == null) {
return '';
}
final profile = _container.read(profilesProvider).getProfile(profileId);
return profile?.label.takeFirstValid(['$profileId']) ?? '$profileId';
}
Future<void> _handleFailedPreference() async {
if (await preferences.isInit) return;
final res = await dialogs.showMessage(
title: currentAppLocalizations.tip,
message: TextSpan(text: currentAppLocalizations.cacheCorrupt),
);
if (res == true) {
final file = File(await appPath.sharedPreferencesPath);
await file.safeDelete();
}
// Saving here would rewrite the preferences file the user just chose to delete.
await _container.read(systemActionProvider.notifier).handleExit(false);
}
/// Android's exit only finishes the activity, so Dart keeps running after
/// [SystemAction.handleExit] returns and must not go on to start the app.
Future<bool> _handlerDisclaimer() async {
final acceptedVersion = _container.read(
appSettingProvider.select((state) => state.acceptedDisclaimerVersion),
);
if (acceptedVersion >= disclaimerVersion) {
return true;
}
final isDisclaimerAccepted = await requestDisclaimerConsent();
if (!isDisclaimerAccepted) {
await _container.read(systemActionProvider.notifier).handleExit();
return false;
}
_container
.read(appSettingProvider.notifier)
.update(
(state) =>
state.copyWith(acceptedDisclaimerVersion: disclaimerVersion),
);
return true;
}
}
final bootstrap = Bootstrap();