// SPDX-FileCopyrightText: 2019-Present Christian Kußowski // SPDX-FileCopyrightText: 2019-Present Contributors to FluffyChat // // SPDX-License-Identifier: AGPL-3.0-or-later import 'dart:async'; import 'dart:isolate'; import 'dart:ui'; import 'package:collection/collection.dart'; import 'package:tatchat/config/app_config.dart'; import 'package:tatchat/utils/client_manager.dart'; import 'package:tatchat/utils/error_reporter.dart'; import 'package:tatchat/utils/notification_background_handler.dart'; import 'package:tatchat/utils/platform_infos.dart'; import 'package:tatchat/utils/start_push_foreground_service.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_vodozemac/flutter_vodozemac.dart' as vod; import 'package:just_audio_media_kit/just_audio_media_kit.dart'; import 'package:material_ui/material_ui.dart'; import 'package:matrix/matrix.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:universal_html/universal_html.dart' as web; import 'config/setting_keys.dart'; import 'utils/background_push.dart'; import 'widgets/tatu_chat_app.dart'; ReceivePort? mainIsolateReceivePort; bool _vodozemacInitialized = false; bool isIntegrationTest = false; void main(List args) => runZonedGuarded(() async { // Forward Flutter errors to global error reporter FlutterError.onError = (details) => Zone.current.handleUncaughtError( details.exception, details.stack ?? StackTrace.current, ); isIntegrationTest = args.singleOrNull == 'integration_test'; if (PlatformInfos.isAndroid) { final port = mainIsolateReceivePort = ReceivePort(); IsolateNameServer.removePortNameMapping(AppConfig.mainIsolatePortName); IsolateNameServer.registerPortWithName( port.sendPort, AppConfig.mainIsolatePortName, ); await waitForPushIsolateDone(); } // Sanitize hash for OIDC: if (kIsWeb) { final hash = web.window.location.hash; if (hash.isNotEmpty && !hash.startsWith('/')) { web.window.location.hash = hash.replaceFirst('#', '#?'); } } // Our background push shared isolate accesses flutter-internal things very early in the startup proccess // To make sure that the parts of flutter needed are started up already, we need to ensure that the // widget bindings are initialized already. WidgetsFlutterBinding.ensureInitialized(); final store = await AppSettings.init(); Logs().i('Welcome to ${AppSettings.applicationName.value} <3'); kEnableMatrixSdkBenchmarks = AppSettings.benchmarksInLogs.value; if (!_vodozemacInitialized) { await vod.init( wasmPath: './assets/assets/vodozemac/${AppConfig.vodozemacVersion}/', ); _vodozemacInitialized = true; } Logs().nativeColors = !PlatformInfos.isIOS; // If the app starts in detached mode, we assume that it is in // background fetch mode for processing push notifications. This is // currently only supported on Android. if (PlatformInfos.isAndroid && AppLifecycleState.detached == WidgetsBinding.instance.lifecycleState) { await ForegroundServices.startService('background_push'); final clients = await ClientManager.getClients(store: store); // Do not send online presences when app is in background fetch mode. for (final client in clients) { client.backgroundSync = false; client.syncPresence = PresenceType.offline; } // In the background fetch mode we do not want to waste ressources with // starting the Flutter engine but process incoming push notifications. BackgroundPush.clientOnly(clients); // To start the flutter engine afterwards we add an custom observer. WidgetsBinding.instance.addObserver(AppStarter(clients, store)); Logs().i( '${AppSettings.applicationName.value} started in background-fetch mode. No GUI will be created unless the app is no longer detached.', ); return; } final clients = await ClientManager.getClients(store: store); // Started in foreground mode. Logs().i( '${AppSettings.applicationName.value} started in foreground mode. Rendering GUI...', ); await startGui(clients, store); }, ErrorReporter.onFlutterError); /// Fetch the pincode for the applock and start the flutter engine. Future startGui(List clients, SharedPreferences store) async { // Fetch the pin for the applock if existing for mobile applications. String? pin; var useBiometrics = false; if (PlatformInfos.supportsAppLock) { try { pin = await const FlutterSecureStorage().read( key: 'chat.fluffy.app_lock', ); useBiometrics = (await const FlutterSecureStorage().read( key: 'chat.fluffy.use_biometrics', )) == 'true'; } catch (e, s) { Logs().d('Unable to read PIN from Secure storage', e, s); } } if (PlatformInfos.isLinux || PlatformInfos.isWindows) { JustAudioMediaKit.ensureInitialized(); } // Preload first client final firstClient = clients.firstOrNull; await firstClient?.roomsLoading; await firstClient?.accountDataLoading; runApp( TatuChatApp( clients: clients, appLockSettings: (pincode: pin, useBiometrics: useBiometrics), store: store, ), ); } /// Watches the lifecycle changes to start the application when it /// is no longer detached. class AppStarter with WidgetsBindingObserver { final List clients; final SharedPreferences store; bool guiStarted = false; AppStarter(this.clients, this.store); @override void didChangeAppLifecycleState(AppLifecycleState state) { if (guiStarted) return; if (state == AppLifecycleState.detached) return; Logs().i( '${AppSettings.applicationName.value} switches from the detached background-fetch mode to ${state.name} mode. Rendering GUI...', ); // Switching to foreground mode needs to reenable send online sync presence. for (final client in clients) { client.backgroundSync = true; client.syncPresence = PresenceType.online; } startGui(clients, store); // We must make sure that the GUI is only started once. guiStarted = true; } }