LCOV - code coverage report
Current view: top level - lib - main.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 186 0.0 %
Date: 2026-07-15 19:28:30 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:async';
       2             : import 'dart:convert';
       3             : import 'package:cwtch/config.dart';
       4             : import 'package:cwtch/notification_manager.dart';
       5             : import 'package:cwtch/views/doublecolview.dart';
       6             : import 'package:cwtch/views/messageview.dart';
       7             : import 'package:flutter/foundation.dart';
       8             : import 'package:cwtch/cwtch/ffi.dart';
       9             : import 'package:cwtch/cwtch/gomobile.dart';
      10             : import 'package:flutter/material.dart';
      11             : import 'package:cwtch/errorHandler.dart';
      12             : import 'package:cwtch/settings.dart';
      13             : import 'package:cwtch/torstatus.dart';
      14             : import 'package:flutter/services.dart';
      15             : import 'package:flutter_localizations/flutter_localizations.dart';
      16             : import 'package:provider/provider.dart';
      17             : import 'package:window_manager/window_manager.dart';
      18             : import 'cwtch/cwtch.dart';
      19             : import 'cwtch/cwtchNotifier.dart';
      20             : import 'l10n/custom_material_delegate.dart';
      21             : import 'licenses.dart';
      22             : import 'models/appstate.dart';
      23             : import 'models/contactlist.dart';
      24             : import 'models/profile.dart';
      25             : import 'models/profilelist.dart';
      26             : import 'models/servers.dart';
      27             : import 'views/profilemgrview.dart';
      28             : import 'views/splashView.dart';
      29             : import 'dart:io' show Platform, exit;
      30             : import 'themes/opaque.dart';
      31             : import 'l10n/app_localizations.dart';
      32             : import 'package:connectivity_plus/connectivity_plus.dart';
      33             : 
      34           0 : var globalSettings = Settings(Locale("en", ''));
      35           0 : var globalErrorHandler = ErrorHandler();
      36           0 : var globalTorStatus = TorStatus();
      37           0 : var globalAppState = AppState();
      38           0 : var globalServersList = ServerListState();
      39             : 
      40           0 : Future<void> main() async {
      41           0 :   print("Cwtch version: ${EnvironmentConfig.BUILD_VER} built on: ${EnvironmentConfig.BUILD_DATE}");
      42           0 :   LicenseRegistry.addLicense(() => licenses());
      43           0 :   WidgetsFlutterBinding.ensureInitialized();
      44             :   // window_manager requires (await recommended but probably not required if not using immediately)
      45           0 :   windowManager.ensureInitialized();
      46           0 :   print("runApp()");
      47           0 :   return runApp(Flwtch());
      48             : }
      49             : 
      50             : class Flwtch extends StatefulWidget {
      51             :   final Key flwtch = GlobalKey();
      52             : 
      53           0 :   @override
      54           0 :   FlwtchState createState() => FlwtchState();
      55             : }
      56             : 
      57             : enum ConnectivityState { assumed_online, confirmed_offline, confirmed_online }
      58             : 
      59             : class FlwtchState extends State<Flwtch> with WindowListener, WidgetsBindingObserver {
      60             :   late Cwtch cwtch;
      61             :   late ProfileListState profs;
      62             :   final MethodChannel notificationClickChannel = MethodChannel('im.cwtch.flwtch/notificationClickHandler');
      63             :   final MethodChannel shutdownMethodChannel = MethodChannel('im.cwtch.flwtch/shutdownClickHandler');
      64             :   final MethodChannel shutdownLinuxMethodChannel = MethodChannel('im.cwtch.linux.shutdown');
      65             :   late StreamSubscription? connectivityStream;
      66             :   ConnectivityState connectivityState = ConnectivityState.assumed_online;
      67             : 
      68             :   final GlobalKey<NavigatorState> navKey = GlobalKey<NavigatorState>();
      69             : 
      70           0 :   Future<dynamic> shutdownDirect(MethodCall call) async {
      71           0 :     EnvironmentConfig.debugLog("$call");
      72           0 :     await cwtch.Shutdown();
      73           0 :     return Future.value({});
      74             :   }
      75             : 
      76           0 :   @override
      77             :   initState() {
      78           0 :     print("initState() started, setting up handlers");
      79           0 :     globalSettings = Settings(Locale("en", ''));
      80           0 :     globalErrorHandler = ErrorHandler();
      81           0 :     globalTorStatus = TorStatus();
      82           0 :     globalAppState = AppState();
      83           0 :     globalServersList = ServerListState();
      84             : 
      85           0 :     print("initState: running...");
      86           0 :     windowManager.addListener(this);
      87             : 
      88           0 :     print("initState: registering notification, shutdown handlers...");
      89           0 :     profs = ProfileListState();
      90           0 :     notificationClickChannel.setMethodCallHandler(_externalNotificationClicked);
      91           0 :     shutdownMethodChannel.setMethodCallHandler(modalShutdown);
      92           0 :     shutdownLinuxMethodChannel.setMethodCallHandler(shutdownDirect);
      93           0 :     print("initState: creating cwtchnotifier, ffi");
      94           0 :     if (Platform.isAndroid) {
      95           0 :       var cwtchNotifier = new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, NullNotificationsManager(), globalAppState, globalServersList, this);
      96           0 :       cwtch = CwtchGomobile(cwtchNotifier);
      97           0 :     } else if (Platform.isLinux) {
      98           0 :       var cwtchNotifier = new CwtchNotifier(
      99           0 :         profs,
     100           0 :         globalSettings,
     101           0 :         globalErrorHandler,
     102           0 :         globalTorStatus,
     103           0 :         newDesktopNotificationsManager(_notificationSelectConvo),
     104           0 :         globalAppState,
     105           0 :         globalServersList,
     106             :         this,
     107             :       );
     108           0 :       cwtch = CwtchFfi(cwtchNotifier);
     109             :     } else {
     110           0 :       var cwtchNotifier = new CwtchNotifier(
     111           0 :         profs,
     112           0 :         globalSettings,
     113           0 :         globalErrorHandler,
     114           0 :         globalTorStatus,
     115           0 :         newDesktopNotificationsManager(_notificationSelectConvo),
     116           0 :         globalAppState,
     117           0 :         globalServersList,
     118             :         this,
     119             :       );
     120           0 :       cwtch = CwtchFfi(cwtchNotifier);
     121             :     }
     122             :     // Cwtch.start can take time, we don't want it blocking first splash screen draw, so postpone a smidge to let splash render
     123           0 :     Future.delayed(const Duration(milliseconds: 100), () {
     124           0 :       print("initState delayed: invoking cwtch.Start()");
     125           0 :       cwtch.Start().then((v) {
     126           0 :         cwtch.getCwtchDir().then((dir) {
     127           0 :           globalSettings.themeloader.LoadThemes(dir);
     128             :         });
     129             :       });
     130             :     });
     131           0 :     print("initState: starting connectivityListener");
     132           0 :     if (EnvironmentConfig.TEST_MODE == false) {
     133           0 :       startConnectivityListener();
     134             :     } else {
     135           0 :       connectivityStream = null;
     136             :     }
     137           0 :     print("initState: done!");
     138           0 :     super.initState();
     139           0 :     WidgetsBinding.instance.addObserver(this);
     140             :   }
     141             : 
     142             :   // connectivity listening is an optional enhancement feature that tries to listen for OS events about the network
     143             :   // and if it detects coming back online, restarts the ACN/tor
     144             :   // gracefully fails and NOPs, as it's not a required functionality
     145           0 :   startConnectivityListener() async {
     146             :     try {
     147           0 :       connectivityStream = Connectivity().onConnectivityChanged.listen(
     148           0 :         (List<ConnectivityResult> result) {
     149             :           // Got a new connectivity status!
     150           0 :           if (result[0] == ConnectivityResult.none) {
     151           0 :             connectivityState = ConnectivityState.confirmed_offline;
     152             :           } else {
     153             :             // were we offline?
     154           0 :             if (connectivityState == ConnectivityState.confirmed_offline) {
     155           0 :               EnvironmentConfig.debugLog("Network appears to have come back online, restarting Tor");
     156           0 :               cwtch.ResetTor();
     157             :             }
     158           0 :             connectivityState = ConnectivityState.confirmed_online;
     159             :           }
     160             :         },
     161           0 :         onError: (Object error) {
     162           0 :           print("Error listening to connectivity for network state: {$error}");
     163             :           return null;
     164             :         },
     165             :         cancelOnError: true,
     166             :       );
     167             :     } catch (e) {
     168           0 :       print("Warning: Unable to open connectivity for listening to network state: {$e}");
     169           0 :       connectivityStream = null;
     170             :     }
     171             :   }
     172             : 
     173           0 :   ChangeNotifierProvider<TorStatus> getTorStatusProvider() => ChangeNotifierProvider.value(value: globalTorStatus);
     174           0 :   ChangeNotifierProvider<ErrorHandler> getErrorHandlerProvider() => ChangeNotifierProvider.value(value: globalErrorHandler);
     175           0 :   ChangeNotifierProvider<Settings> getSettingsProvider() => ChangeNotifierProvider.value(value: globalSettings);
     176           0 :   ChangeNotifierProvider<AppState> getAppStateProvider() => ChangeNotifierProvider.value(value: globalAppState);
     177           0 :   Provider<FlwtchState> getFlwtchStateProvider() => Provider<FlwtchState>(create: (_) => this);
     178           0 :   ChangeNotifierProvider<ProfileListState> getProfileListProvider() => ChangeNotifierProvider(create: (context) => profs);
     179           0 :   ChangeNotifierProvider<ServerListState> getServerListStateProvider() => ChangeNotifierProvider.value(value: globalServersList);
     180             : 
     181           0 :   @override
     182             :   Widget build(BuildContext context) {
     183           0 :     globalSettings.initPackageInfo();
     184             : 
     185           0 :     return MultiProvider(
     186           0 :       providers: [getFlwtchStateProvider(), getProfileListProvider(), getSettingsProvider(), getErrorHandlerProvider(), getTorStatusProvider(), getAppStateProvider(), getServerListStateProvider()],
     187           0 :       builder: (context, widget) {
     188           0 :         return Consumer2<Settings, AppState>(
     189           0 :           builder: (context, settings, appState, child) => MaterialApp(
     190           0 :             key: Key('app'),
     191           0 :             navigatorKey: navKey,
     192           0 :             locale: settings.locale,
     193           0 :             showPerformanceOverlay: settings.profileMode,
     194           0 :             localizationsDelegates: <LocalizationsDelegate<dynamic>>[
     195             :               AppLocalizations.delegate,
     196           0 :               MaterialLocalizationDelegate(),
     197             :               GlobalMaterialLocalizations.delegate,
     198             :               GlobalCupertinoLocalizations.delegate,
     199             :               GlobalWidgetsLocalizations.delegate,
     200             :             ],
     201             :             supportedLocales: AppLocalizations.supportedLocales,
     202             :             title: 'Cwtch',
     203           0 :             showSemanticsDebugger: settings.useSemanticDebugger,
     204           0 :             theme: mkThemeData(settings),
     205           0 :             home: (!appState.loaded) ? SplashView() : ProfileMgrView(),
     206             :           ),
     207             :         );
     208             :       },
     209             :     );
     210             :   }
     211             : 
     212             :   // invoked from either ProfileManagerView's appbar close button, or a ShutdownClicked event on
     213             :   // the MyBroadcastReceiver method channel
     214           0 :   Future<void> modalShutdown(MethodCall mc) async {
     215             :     // set up the buttons
     216           0 :     Widget cancelButton = ElevatedButton(
     217           0 :       child: Text(AppLocalizations.of(navKey.currentContext!)!.cancel),
     218           0 :       onPressed: () {
     219           0 :         Navigator.of(navKey.currentContext!).pop(); // dismiss dialog
     220             :       },
     221             :     );
     222           0 :     Widget continueButton = ElevatedButton(
     223           0 :       child: Text(key: Key("confirmShutdown"), AppLocalizations.of(navKey.currentContext!)!.shutdownCwtchAction),
     224           0 :       onPressed: () {
     225           0 :         Provider.of<AppState>(navKey.currentContext!, listen: false).cwtchIsClosing = true;
     226           0 :         Navigator.of(navKey.currentContext!).pop(); // dismiss dialog
     227             :       },
     228             :     );
     229             : 
     230             :     // set up the AlertDialog
     231           0 :     AlertDialog alert = AlertDialog(
     232           0 :       key: Key("modalShutdown"),
     233           0 :       title: Text(AppLocalizations.of(navKey.currentContext!)!.shutdownCwtchDialogTitle),
     234           0 :       content: Text(AppLocalizations.of(navKey.currentContext!)!.shutdownCwtchDialog),
     235           0 :       actions: [cancelButton, continueButton],
     236             :     );
     237             : 
     238             :     // show the dialog
     239           0 :     showDialog(
     240           0 :       context: navKey.currentContext!,
     241             :       barrierDismissible: false,
     242           0 :       builder: (BuildContext context) {
     243             :         return alert;
     244             :       },
     245           0 :     ).then((val) {
     246           0 :       if (Provider.of<AppState>(navKey.currentContext!, listen: false).cwtchIsClosing) {
     247             :         // Directly call the shutdown command, Android will do this for us...
     248           0 :         Provider.of<FlwtchState>(navKey.currentContext!, listen: false).shutdown();
     249             :       }
     250             :     });
     251             :   }
     252             : 
     253           0 :   Future<void> shutdown() async {
     254             :     if (!EnvironmentConfig.TEST_MODE) {
     255           0 :       globalAppState.SetModalState(ModalState.shutdown);
     256             :     }
     257           0 :     EnvironmentConfig.debugLog("shutting down");
     258           0 :     await cwtch.Shutdown();
     259             :     // Wait a few seconds as shutting down things takes a little time..
     260             :     {
     261           0 :       if (Platform.isAndroid) {
     262           0 :         SystemNavigator.pop();
     263           0 :       } else if (!EnvironmentConfig.TEST_MODE && (Platform.isLinux || Platform.isWindows || Platform.isMacOS)) {
     264           0 :         print("Exiting...");
     265           0 :         exit(0);
     266             :       }
     267             :     }
     268             :   }
     269             : 
     270             :   // Invoked via notificationClickChannel by MyBroadcastReceiver in MainActivity.kt
     271             :   // coder beware: args["RemotePeer"] is actually a handle, and could be eg a groupID
     272           0 :   Future<void> _externalNotificationClicked(MethodCall call) async {
     273           0 :     var args = jsonDecode(call.arguments);
     274           0 :     _notificationSelectConvo(args["ProfileOnion"], args["Handle"]);
     275             :   }
     276             : 
     277           0 :   Future<void> _notificationSelectConvo(String profileOnion, int convoId) async {
     278           0 :     var profile = profs.getProfile(profileOnion)!;
     279           0 :     var convo = profile.contactList.getContact(convoId)!;
     280           0 :     if (profileOnion.isEmpty) {
     281             :       return;
     282             :     }
     283           0 :     Provider.of<AppState>(navKey.currentContext!, listen: false).initialScrollIndex = convo.unreadMessages;
     284           0 :     convo.unreadMessages = 0;
     285             : 
     286             :     // Clear nav path back to root
     287           0 :     while (navKey.currentState!.canPop()) {
     288           0 :       navKey.currentState!.pop();
     289             :     }
     290             : 
     291           0 :     Provider.of<AppState>(navKey.currentContext!, listen: false).selectedConversation = null;
     292           0 :     Provider.of<AppState>(navKey.currentContext!, listen: false).selectedProfile = profileOnion;
     293           0 :     Provider.of<AppState>(navKey.currentContext!, listen: false).selectedConversation = convoId;
     294             : 
     295           0 :     Navigator.of(navKey.currentContext!).push(
     296           0 :       PageRouteBuilder(
     297           0 :         settings: RouteSettings(name: "conversations"),
     298           0 :         pageBuilder: (c, a1, a2) {
     299           0 :           return OrientationBuilder(
     300           0 :             builder: (orientationBuilderContext, orientation) {
     301           0 :               return MultiProvider(
     302           0 :                 providers: [
     303           0 :                   ChangeNotifierProvider<ProfileInfoState>.value(value: profile),
     304           0 :                   ChangeNotifierProvider<ContactListState>.value(value: profile.contactList),
     305             :                 ],
     306           0 :                 builder: (innercontext, widget) {
     307           0 :                   var appState = Provider.of<AppState>(navKey.currentContext!);
     308           0 :                   var settings = Provider.of<Settings>(navKey.currentContext!);
     309           0 :                   return settings.uiColumns(appState.isLandscape(innercontext)).length > 1 ? DoubleColumnView() : MessageView();
     310             :                 },
     311             :               );
     312             :             },
     313             :           );
     314             :         },
     315           0 :         transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
     316           0 :         transitionDuration: Duration(milliseconds: 200),
     317             :       ),
     318             :     );
     319             :     // 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
     320             :     // https://askubuntu.com/questions/1286206/how-to-skip-the-is-ready-notification-and-directly-open-apps-in-ubuntu-20-4
     321           0 :     await windowManager.show();
     322           0 :     await windowManager.focus();
     323             :   }
     324             : 
     325             :   // using windowManager flutter plugin until proper lifecycle management lands in desktop
     326             : 
     327           0 :   @override
     328             :   void onWindowFocus() {
     329           0 :     globalAppState.focus = true;
     330             :   }
     331             : 
     332           0 :   @override
     333             :   void onWindowBlur() {
     334           0 :     globalAppState.focus = false;
     335             :   }
     336             : 
     337           0 :   void onWindowClose() {
     338           0 :     Provider.of<FlwtchState>(navKey.currentContext!, listen: false).shutdown();
     339             :   }
     340             : 
     341             :   // TODO: wire into logic around being backgrounded and resuming
     342           0 :   @override
     343             :   void didChangeAppLifecycleState(AppLifecycleState state) {
     344           0 :     super.didChangeAppLifecycleState(state);
     345             : 
     346             :     switch (state) {
     347           0 :       case AppLifecycleState.inactive:
     348             :         break;
     349           0 :       case AppLifecycleState.paused:
     350             :         break;
     351           0 :       case AppLifecycleState.resumed:
     352             :         break;
     353           0 :       case AppLifecycleState.detached:
     354             :         break;
     355           0 :       case AppLifecycleState.hidden:
     356             :         break;
     357             :     }
     358             :   }
     359             : 
     360           0 :   @override
     361             :   void dispose() {
     362           0 :     WidgetsBinding.instance.removeObserver(this);
     363           0 :     globalAppState.SetModalState(ModalState.shutdown);
     364           0 :     cwtch.Shutdown();
     365           0 :     windowManager.removeListener(this);
     366           0 :     cwtch.dispose();
     367           0 :     connectivityStream?.cancel();
     368           0 :     super.dispose();
     369             :   }
     370             : }

Generated by: LCOV version 1.14