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 : }
|