import 'dart:async'; import 'dart:convert'; import 'package:cwtch/config.dart'; import 'package:cwtch/notification_manager.dart'; import 'package:cwtch/views/doublecolview.dart'; import 'package:cwtch/views/messageview.dart'; import 'package:flutter/foundation.dart'; import 'package:cwtch/cwtch/ffi.dart'; import 'package:cwtch/cwtch/gomobile.dart'; import 'package:flutter/material.dart'; import 'package:cwtch/errorHandler.dart'; import 'package:cwtch/settings.dart'; import 'package:cwtch/torstatus.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; import 'package:window_manager/window_manager.dart'; import 'cwtch/cwtch.dart'; import 'cwtch/cwtchNotifier.dart'; import 'l10n/custom_material_delegate.dart'; import 'licenses.dart'; import 'models/appstate.dart'; import 'models/contactlist.dart'; import 'models/profile.dart'; import 'models/profilelist.dart'; import 'models/servers.dart'; import 'views/profilemgrview.dart'; import 'views/splashView.dart'; import 'dart:io' show Platform, exit; import 'themes/opaque.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; var globalSettings = Settings(Locale("en", '')); var globalErrorHandler = ErrorHandler(); var globalTorStatus = TorStatus(); var globalAppState = AppState(); var globalServersList = ServerListState(); Future main() async { print("Cwtch version: ${EnvironmentConfig.BUILD_VER} built on: ${EnvironmentConfig.BUILD_DATE}"); LicenseRegistry.addLicense(() => licenses()); WidgetsFlutterBinding.ensureInitialized(); // window_manager requires (await recommended but probably not required if not using immediately) windowManager.ensureInitialized(); print("runApp()"); return runApp(Flwtch()); } class Flwtch extends StatefulWidget { final Key flwtch = GlobalKey(); @override FlwtchState createState() => FlwtchState(); } enum ConnectivityState { assumed_online, confirmed_offline, confirmed_online } class FlwtchState extends State with WindowListener { late Cwtch cwtch; late ProfileListState profs; final MethodChannel notificationClickChannel = MethodChannel('im.cwtch.flwtch/notificationClickHandler'); final MethodChannel shutdownMethodChannel = MethodChannel('im.cwtch.flwtch/shutdownClickHandler'); final MethodChannel shutdownLinuxMethodChannel = MethodChannel('im.cwtch.linux.shutdown'); late StreamSubscription? connectivityStream; ConnectivityState connectivityState = ConnectivityState.assumed_online; final GlobalKey navKey = GlobalKey(); Future shutdownDirect(MethodCall call) async { EnvironmentConfig.debugLog("$call"); await cwtch.Shutdown(); return Future.value({}); } @override initState() { print("initState() started, setting up handlers"); globalSettings = Settings(Locale("en", '')); globalErrorHandler = ErrorHandler(); globalTorStatus = TorStatus(); globalAppState = AppState(); globalServersList = ServerListState(); print("initState: running..."); windowManager.addListener(this); print("initState: registering notification, shutdown handlers..."); profs = ProfileListState(); notificationClickChannel.setMethodCallHandler(_externalNotificationClicked); shutdownMethodChannel.setMethodCallHandler(modalShutdown); shutdownLinuxMethodChannel.setMethodCallHandler(shutdownDirect); print("initState: creating cwtchnotifier, ffi"); if (Platform.isAndroid) { var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState, globalServersList, this); cwtch = CwtchGomobile(cwtchNotifier); } else if (Platform.isLinux) { var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(_notificationSelectConvo), globalAppState, globalServersList, this); cwtch = CwtchFfi(cwtchNotifier); } else { var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(_notificationSelectConvo), globalAppState, globalServersList, this); cwtch = CwtchFfi(cwtchNotifier); } // Cwtch.start can take time, we don't want it blocking first splash screen draw, so postpone a smidge to let splash render Future.delayed(const Duration(milliseconds: 100), () { print("initState delayed: invoking cwtch.Start()"); cwtch.Start().then((v) { cwtch.getCwtchDir().then((dir) { globalSettings.themeloader.LoadThemes(dir); }); }); }); print("initState: starting connectivityListener"); if (EnvironmentConfig.TEST_MODE == false) { startConnectivityListener(); } else { connectivityStream = null; } print("initState: done!"); super.initState(); } // connectivity listening is an optional enhancement feature that tries to listen for OS events about the network // and if it detects coming back online, restarts the ACN/tor // gracefully fails and NOPs, as it's not a required functionality startConnectivityListener() async { try { connectivityStream = Connectivity().onConnectivityChanged.listen((ConnectivityResult result) { // Got a new connectivity status! if (result == ConnectivityResult.none) { connectivityState = ConnectivityState.confirmed_offline; } else { // were we offline? if (connectivityState == ConnectivityState.confirmed_offline) { EnvironmentConfig.debugLog("Network appears to have come back online, restarting Tor"); cwtch.ResetTor(); } connectivityState = ConnectivityState.confirmed_online; } }, onError: (Object error) { print("Error listening to connectivity for network state: {$error}"); return null; }, cancelOnError: true); } catch (e) { print("Warning: Unable to open connectivity for listening to network state: {$e}"); connectivityStream = null; } } ChangeNotifierProvider getTorStatusProvider() => ChangeNotifierProvider.value(value: globalTorStatus); ChangeNotifierProvider getErrorHandlerProvider() => ChangeNotifierProvider.value(value: globalErrorHandler); ChangeNotifierProvider getSettingsProvider() => ChangeNotifierProvider.value(value: globalSettings); ChangeNotifierProvider getAppStateProvider() => ChangeNotifierProvider.value(value: globalAppState); Provider getFlwtchStateProvider() => Provider(create: (_) => this); ChangeNotifierProvider getProfileListProvider() => ChangeNotifierProvider(create: (context) => profs); ChangeNotifierProvider getServerListStateProvider() => ChangeNotifierProvider.value(value: globalServersList); @override Widget build(BuildContext context) { globalSettings.initPackageInfo(); return MultiProvider( providers: [ getFlwtchStateProvider(), getProfileListProvider(), getSettingsProvider(), getErrorHandlerProvider(), getTorStatusProvider(), getAppStateProvider(), getServerListStateProvider(), ], builder: (context, widget) { return Consumer2( builder: (context, settings, appState, child) => MaterialApp( key: Key('app'), navigatorKey: navKey, locale: settings.locale, showPerformanceOverlay: settings.profileMode, localizationsDelegates: >[ AppLocalizations.delegate, MaterialLocalizationDelegate(), GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, GlobalWidgetsLocalizations.delegate, ], supportedLocales: AppLocalizations.supportedLocales, title: 'Cwtch', showSemanticsDebugger: settings.useSemanticDebugger, theme: mkThemeData(settings), home: (!appState.loaded) ? SplashView() : ProfileMgrView(), ), ); }, ); } // invoked from either ProfileManagerView's appbar close button, or a ShutdownClicked event on // the MyBroadcastReceiver method channel Future modalShutdown(MethodCall mc) async { // set up the buttons Widget cancelButton = ElevatedButton( child: Text(AppLocalizations.of(navKey.currentContext!)!.cancel), onPressed: () { Navigator.of(navKey.currentContext!).pop(); // dismiss dialog }, ); Widget continueButton = ElevatedButton( child: Text(AppLocalizations.of(navKey.currentContext!)!.shutdownCwtchAction), onPressed: () { Provider.of(navKey.currentContext!, listen: false).cwtchIsClosing = true; Navigator.of(navKey.currentContext!).pop(); // dismiss dialog }); // set up the AlertDialog AlertDialog alert = AlertDialog( title: Text(AppLocalizations.of(navKey.currentContext!)!.shutdownCwtchDialogTitle), content: Text(AppLocalizations.of(navKey.currentContext!)!.shutdownCwtchDialog), actions: [ cancelButton, continueButton, ], ); // show the dialog showDialog( context: navKey.currentContext!, barrierDismissible: false, builder: (BuildContext context) { return alert; }, ).then((val) { if (Provider.of(navKey.currentContext!, listen: false).cwtchIsClosing) { globalAppState.SetModalState(ModalState.shutdown); // Directly call the shutdown command, Android will do this for us... Provider.of(navKey.currentContext!, listen: false).shutdown(); } }); } Future shutdown() async { globalAppState.SetModalState(ModalState.shutdown); EnvironmentConfig.debugLog("shutting down"); await cwtch.Shutdown(); // Wait a few seconds as shutting down things takes a little time.. { if (Platform.isAndroid) { SystemNavigator.pop(); } else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { print("Exiting..."); exit(0); } } } // Invoked via notificationClickChannel by MyBroadcastReceiver in MainActivity.kt // coder beware: args["RemotePeer"] is actually a handle, and could be eg a groupID Future _externalNotificationClicked(MethodCall call) async { var args = jsonDecode(call.arguments); _notificationSelectConvo(args["ProfileOnion"], args["Handle"]); } Future _notificationSelectConvo(String profileOnion, int convoId) async { var profile = profs.getProfile(profileOnion)!; var convo = profile.contactList.getContact(convoId)!; if (profileOnion.isEmpty) { return; } Provider.of(navKey.currentContext!, listen: false).initialScrollIndex = convo.unreadMessages; convo.unreadMessages = 0; // Clear nav path back to root while (navKey.currentState!.canPop()) { navKey.currentState!.pop(); } Provider.of(navKey.currentContext!, listen: false).selectedConversation = null; Provider.of(navKey.currentContext!, listen: false).selectedProfile = profileOnion; Provider.of(navKey.currentContext!, listen: false).selectedConversation = convoId; Navigator.of(navKey.currentContext!).push( PageRouteBuilder( settings: RouteSettings(name: "conversations"), pageBuilder: (c, a1, a2) { return OrientationBuilder(builder: (orientationBuilderContext, orientation) { return MultiProvider( providers: [ChangeNotifierProvider.value(value: profile), ChangeNotifierProvider.value(value: profile.contactList)], builder: (innercontext, widget) { var appState = Provider.of(navKey.currentContext!); var settings = Provider.of(navKey.currentContext!); return settings.uiColumns(appState.isLandscape(innercontext)).length > 1 ? DoubleColumnView() : MessageView(); }); }); }, transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child), transitionDuration: Duration(milliseconds: 200), ), ); // On Gnome follows up a clicked notification with a "Cwtch is ready" notification that takes you to the app. AFAICT just because Gnome is bad // https://askubuntu.com/questions/1286206/how-to-skip-the-is-ready-notification-and-directly-open-apps-in-ubuntu-20-4 await windowManager.show(); await windowManager.focus(); } // using windowManager flutter plugin until proper lifecycle management lands in desktop @override void onWindowFocus() { globalAppState.focus = true; } @override void onWindowBlur() { globalAppState.focus = false; } void onWindowClose() {} @override void dispose() { globalAppState.SetModalState(ModalState.shutdown); cwtch.Shutdown(); windowManager.removeListener(this); cwtch.dispose(); connectivityStream?.cancel(); super.dispose(); } }