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

Generated by: LCOV version 1.14