import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:get_it/get_it.dart'; import 'package:logging/logging.dart'; import 'package:moxlib/moxlib.dart'; import 'package:moxplatform/moxplatform.dart'; import 'package:moxplatform_platform_interface/moxplatform_platform_interface.dart'; import 'package:moxxmpp/moxxmpp.dart'; import 'package:moxxyv2/i18n/strings.g.dart'; import 'package:moxxyv2/service/avatars.dart'; import 'package:moxxyv2/service/blocking.dart'; import 'package:moxxyv2/service/connectivity.dart'; import 'package:moxxyv2/service/connectivity_watcher.dart'; import 'package:moxxyv2/service/contacts.dart'; import 'package:moxxyv2/service/conversation.dart'; import 'package:moxxyv2/service/cryptography/cryptography.dart'; import 'package:moxxyv2/service/database/database.dart'; import 'package:moxxyv2/service/events.dart'; import 'package:moxxyv2/service/httpfiletransfer/httpfiletransfer.dart'; import 'package:moxxyv2/service/language.dart'; import 'package:moxxyv2/service/message.dart'; import 'package:moxxyv2/service/moxxmpp/disco.dart'; import 'package:moxxyv2/service/moxxmpp/omemo.dart'; import 'package:moxxyv2/service/moxxmpp/reconnect.dart'; import 'package:moxxyv2/service/moxxmpp/roster.dart'; import 'package:moxxyv2/service/moxxmpp/socket.dart'; import 'package:moxxyv2/service/moxxmpp/stream.dart'; import 'package:moxxyv2/service/notifications.dart'; import 'package:moxxyv2/service/omemo/omemo.dart'; import 'package:moxxyv2/service/preferences.dart'; import 'package:moxxyv2/service/roster.dart'; import 'package:moxxyv2/service/stickers.dart'; import 'package:moxxyv2/service/xmpp.dart'; import 'package:moxxyv2/shared/commands.dart'; import 'package:moxxyv2/shared/eventhandler.dart'; import 'package:moxxyv2/shared/events.dart'; import 'package:moxxyv2/shared/logging.dart'; import 'package:moxxyv2/shared/synchronized_queue.dart'; import 'package:moxxyv2/ui/events.dart' as ui_events; Future initializeServiceIfNeeded() async { final logger = GetIt.I.get(); final handler = MoxplatformPlugin.handler; if (await handler.isRunning()) { if (kDebugMode) { logger.fine('Since kDebugMode is true, waiting 600ms before sending PreStartCommand'); sleep(const Duration(milliseconds: 600)); } logger.info('Attaching to service...'); await handler.attach(ui_events.receiveIsolateEvent); logger.info('Done'); // ignore: cascade_invocations logger.info('Service is running. Sending pre start command'); await handler.getDataSender().sendData( PerformPreStartCommand( systemLocaleCode: WidgetsBinding.instance.platformDispatcher.locale.toLanguageTag(), ), awaitable: false, ); } else { logger.info('Service is not running. Initializing service... '); await handler.start( entrypoint, receiveUIEvent, ui_events.handleIsolateEvent, ); } } /// A middleware for packing an event into a [DataWrapper] and also /// logging what we send. void sendEvent(BackgroundEvent event, { String? id }) { // NOTE: *S*erver to *F*oreground GetIt.I.get().fine('--> ${event.toJson()["type"]}'); GetIt.I.get().sendEvent(event, id: id); } void setupLogging() { Logger.root.level = kDebugMode ? Level.ALL : Level.INFO; Logger.root.onRecord.listen((record) { final logMessageHeader = '[${record.level.name}] (${record.loggerName}) ${record.time}: '; var msg = record.message; do { final tooLong = logMessageHeader.length + msg.length >= 967; final line = tooLong ? msg.substring(0, 967 - logMessageHeader.length) : msg; if (tooLong) { msg = msg.substring(967 - logMessageHeader.length - 2); } else { msg = ''; } final logMessage = logMessageHeader + line; if (GetIt.I.isRegistered()) { final udp = GetIt.I.get(); if (udp.isEnabled()) { udp.sendLog(logMessage, record.time.millisecondsSinceEpoch, record.level.name); } } // ignore: literal_only_boolean_expressions if (/*kDebugMode*/ true) { // ignore: avoid_print print(logMessage); } } while (msg.isNotEmpty); }); } Future initUDPLogger() async { final prefs = await GetIt.I.get().getPreferences(); if (prefs.debugEnabled) { Logger('initUDPLogger').finest('UDPLogger created'); final port = prefs.debugPort; final ip = prefs.debugIp; final passphrase = prefs.debugPassphrase; if (port != 0 && ip.isNotEmpty && passphrase.isNotEmpty) { await GetIt.I.get().init(passphrase, ip, port); } } else { GetIt.I.get().setEnabled(false); } } /// The entrypoint for all platforms after the platform specific initilization is done. @pragma('vm:entry-point') Future entrypoint() async { setupLogging(); setupBackgroundEventHandler(); // Register singletons GetIt.I.registerSingleton(Logger('MoxxyService')); GetIt.I.registerSingleton(UDPLogger()); GetIt.I.registerSingleton(LanguageService()); // Initialize the database GetIt.I.registerSingleton(DatabaseService()); await GetIt.I.get().initialize(); GetIt.I.registerSingleton(PreferencesService()); GetIt.I.registerSingleton(BlocklistService()); GetIt.I.registerSingleton(NotificationsService()); GetIt.I.registerSingleton(HttpFileTransferService()); GetIt.I.registerSingleton(AvatarService()); GetIt.I.registerSingleton(RosterService()); GetIt.I.registerSingleton(ConversationService()); GetIt.I.registerSingleton(MessageService()); GetIt.I.registerSingleton(OmemoService()); GetIt.I.registerSingleton(CryptographyService()); GetIt.I.registerSingleton(ContactsService()); GetIt.I.registerSingleton(StickersService()); final xmpp = XmppService(); GetIt.I.registerSingleton(xmpp); await GetIt.I.get().init(); await GetIt.I.get().init(); if (!kDebugMode) { final enableDebug = (await GetIt.I.get().getPreferences()).debugEnabled; Logger.root.level = enableDebug ? Level.ALL : Level.INFO; } // Init the UDPLogger await initUDPLogger(); GetIt.I.registerSingleton(MoxxyReconnectionPolicy()); final connection = XmppConnection( GetIt.I.get(), MoxxyTCPSocketWrapper(), )..registerManagers([ MoxxyStreamManagementManager(), MoxxyDiscoManager(), MoxxyRosterManager(), MoxxyOmemoManager(), PingManager(), MessageManager(), PresenceManager('http://moxxy.im'), CSIManager(), CarbonsManager(), PubSubManager(), VCardManager(), UserAvatarManager(), StableIdManager(), MessageDeliveryReceiptManager(), ChatMarkerManager(), OOBManager(), SFSManager(), MessageRepliesManager(), BlockingManager(), ChatStateManager(), HttpFileUploadManager(), FileUploadNotificationManager(), EmeManager(), CryptographicHashManager(), DelayedDeliveryManager(), MessageRetractionManager(), LastMessageCorrectionManager(), MessageReactionsManager(), StickersManager(), ]) ..registerFeatureNegotiators([ ResourceBindingNegotiator(), StartTlsNegotiator(), StreamManagementNegotiator(), CSINegotiator(), RosterFeatureNegotiator(), SaslScramNegotiator(10, '', '', ScramHashType.sha512), SaslScramNegotiator(9, '', '', ScramHashType.sha256), SaslScramNegotiator(8, '', '', ScramHashType.sha1), SaslPlainNegotiator(), ]); GetIt.I.registerSingleton(connection); GetIt.I.registerSingleton(ConnectivityWatcherService()); GetIt.I.registerSingleton(ConnectivityService()); await GetIt.I.get().initialize(); GetIt.I.get().finest('Done with xmpp'); final settings = await xmpp.getConnectionSettings(); // Ensure we can access translations here // TODO(Unknown): This does *NOT* allow us to get the system's locale as we have no // window here. WidgetsFlutterBinding.ensureInitialized(); LocaleSettings.useDeviceLocale(); GetIt.I.get().finest('Got settings'); if (settings != null) { unawaited(GetIt.I.get().initializeIfNeeded(settings.jid.toBare().toString())); // The title of the notification will be changed as soon as the connection state // of [XmppConnection] changes. await connection.getManagerById(smManager)!.loadState(); await xmpp.connect(settings, false); } else { GetIt.I.get().setNotification( 'Moxxy', t.notifications.permanent.idle, ); } unawaited(GetIt.I.get?>>().removeQueueLock()); sendEvent(ServiceReadyEvent()); } Future receiveUIEvent(Map? data) async { await GetIt.I.get?>>().add(data); } Future handleUIEvent(Map? data) async { // NOTE: *F*oreground to *S*ervice final log = GetIt.I.get(); if (data == null) { log.warning('Received null from the UI isolate. Ignoring...'); return; } final id = data['id']! as String; final command = getCommandFromJson(data['data']! as Map); if (command == null) { log.severe("Unknown command type ${data['type']}"); return; } if (command is LoginCommand) { final redacted = { 'id': id, 'data': LoginCommand( jid: command.jid, password: '*******', useDirectTLS: command.useDirectTLS, ).toJson() }; log.fine('F2S: $redacted'); } else { log.fine('F2S: $data'); } unawaited(GetIt.I.get().run(command, extra: id)); }