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