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 'package:flutter_gen/gen_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 {
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 : var cwtchNotifier =
99 0 : new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(_notificationSelectConvo), globalAppState, globalServersList, this);
100 0 : cwtch = CwtchFfi(cwtchNotifier);
101 : } else {
102 : var cwtchNotifier =
103 0 : new CwtchNotifier(profs, globalSettings, globalErrorHandler, globalTorStatus, newDesktopNotificationsManager(_notificationSelectConvo), globalAppState, globalServersList, this);
104 0 : cwtch = CwtchFfi(cwtchNotifier);
105 : }
106 : // Cwtch.start can take time, we don't want it blocking first splash screen draw, so postpone a smidge to let splash render
107 0 : Future.delayed(const Duration(milliseconds: 100), () {
108 0 : print("initState delayed: invoking cwtch.Start()");
109 0 : cwtch.Start().then((v) {
110 0 : cwtch.getCwtchDir().then((dir) {
111 0 : globalSettings.themeloader.LoadThemes(dir);
112 : });
113 : });
114 : });
115 0 : print("initState: starting connectivityListener");
116 0 : if (EnvironmentConfig.TEST_MODE == false) {
117 0 : startConnectivityListener();
118 : } else {
119 0 : connectivityStream = null;
120 : }
121 0 : print("initState: done!");
122 0 : super.initState();
123 : }
124 :
125 : // connectivity listening is an optional enhancement feature that tries to listen for OS events about the network
126 : // and if it detects coming back online, restarts the ACN/tor
127 : // gracefully fails and NOPs, as it's not a required functionality
128 0 : startConnectivityListener() async {
129 : try {
130 0 : connectivityStream = Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
131 : // Got a new connectivity status!
132 0 : if (result == ConnectivityResult.none) {
133 0 : connectivityState = ConnectivityState.confirmed_offline;
134 : } else {
135 : // were we offline?
136 0 : if (connectivityState == ConnectivityState.confirmed_offline) {
137 0 : EnvironmentConfig.debugLog("Network appears to have come back online, restarting Tor");
138 0 : cwtch.ResetTor();
139 : }
140 0 : connectivityState = ConnectivityState.confirmed_online;
141 : }
142 0 : }, onError: (Object error) {
143 0 : print("Error listening to connectivity for network state: {$error}");
144 : return null;
145 : }, cancelOnError: true);
146 : } catch (e) {
147 0 : print("Warning: Unable to open connectivity for listening to network state: {$e}");
148 0 : connectivityStream = null;
149 : }
150 : }
151 :
152 0 : ChangeNotifierProvider<TorStatus> getTorStatusProvider() => ChangeNotifierProvider.value(value: globalTorStatus);
153 0 : ChangeNotifierProvider<ErrorHandler> getErrorHandlerProvider() => ChangeNotifierProvider.value(value: globalErrorHandler);
154 0 : ChangeNotifierProvider<Settings> getSettingsProvider() => ChangeNotifierProvider.value(value: globalSettings);
155 0 : ChangeNotifierProvider<AppState> getAppStateProvider() => ChangeNotifierProvider.value(value: globalAppState);
156 0 : Provider<FlwtchState> getFlwtchStateProvider() => Provider<FlwtchState>(create: (_) => this);
157 0 : ChangeNotifierProvider<ProfileListState> getProfileListProvider() => ChangeNotifierProvider(create: (context) => profs);
158 0 : ChangeNotifierProvider<ServerListState> getServerListStateProvider() => ChangeNotifierProvider.value(value: globalServersList);
159 :
160 0 : @override
161 : Widget build(BuildContext context) {
162 0 : globalSettings.initPackageInfo();
163 :
164 0 : return MultiProvider(
165 0 : providers: [
166 0 : getFlwtchStateProvider(),
167 0 : getProfileListProvider(),
168 0 : getSettingsProvider(),
169 0 : getErrorHandlerProvider(),
170 0 : getTorStatusProvider(),
171 0 : getAppStateProvider(),
172 0 : getServerListStateProvider(),
173 : ],
174 0 : builder: (context, widget) {
175 0 : return Consumer2<Settings, AppState>(
176 0 : builder: (context, settings, appState, child) => MaterialApp(
177 0 : key: Key('app'),
178 0 : navigatorKey: navKey,
179 0 : locale: settings.locale,
180 0 : showPerformanceOverlay: settings.profileMode,
181 0 : localizationsDelegates: <LocalizationsDelegate<dynamic>>[
182 : AppLocalizations.delegate,
183 0 : MaterialLocalizationDelegate(),
184 : GlobalMaterialLocalizations.delegate,
185 : GlobalCupertinoLocalizations.delegate,
186 : GlobalWidgetsLocalizations.delegate,
187 : ],
188 : supportedLocales: AppLocalizations.supportedLocales,
189 : title: 'Cwtch',
190 0 : showSemanticsDebugger: settings.useSemanticDebugger,
191 0 : theme: mkThemeData(settings),
192 0 : home: (!appState.loaded) ? SplashView() : ProfileMgrView(),
193 : ),
194 : );
195 : },
196 : );
197 : }
198 :
199 : // invoked from either ProfileManagerView's appbar close button, or a ShutdownClicked event on
200 : // the MyBroadcastReceiver method channel
201 0 : Future<void> modalShutdown(MethodCall mc) async {
202 : // set up the buttons
203 0 : Widget cancelButton = ElevatedButton(
204 0 : child: Text(AppLocalizations.of(navKey.currentContext!)!.cancel),
205 0 : onPressed: () {
206 0 : Navigator.of(navKey.currentContext!).pop(); // dismiss dialog
207 : },
208 : );
209 0 : Widget continueButton = ElevatedButton(
210 0 : child: Text(AppLocalizations.of(navKey.currentContext!)!.shutdownCwtchAction),
211 0 : onPressed: () {
212 0 : Provider.of<AppState>(navKey.currentContext!, listen: false).cwtchIsClosing = true;
213 0 : Navigator.of(navKey.currentContext!).pop(); // dismiss dialog
214 : });
215 :
216 : // set up the AlertDialog
217 0 : AlertDialog alert = AlertDialog(
218 0 : title: Text(AppLocalizations.of(navKey.currentContext!)!.shutdownCwtchDialogTitle),
219 0 : content: Text(AppLocalizations.of(navKey.currentContext!)!.shutdownCwtchDialog),
220 0 : actions: [
221 : cancelButton,
222 : continueButton,
223 : ],
224 : );
225 :
226 : // show the dialog
227 0 : showDialog(
228 0 : context: navKey.currentContext!,
229 : barrierDismissible: false,
230 0 : builder: (BuildContext context) {
231 : return alert;
232 : },
233 0 : ).then((val) {
234 0 : if (Provider.of<AppState>(navKey.currentContext!, listen: false).cwtchIsClosing) {
235 0 : globalAppState.SetModalState(ModalState.shutdown);
236 : // Directly call the shutdown command, Android will do this for us...
237 0 : Provider.of<FlwtchState>(navKey.currentContext!, listen: false).shutdown();
238 : }
239 : });
240 : }
241 :
242 0 : Future<void> shutdown() async {
243 0 : globalAppState.SetModalState(ModalState.shutdown);
244 0 : EnvironmentConfig.debugLog("shutting down");
245 0 : await cwtch.Shutdown();
246 : // Wait a few seconds as shutting down things takes a little time..
247 : {
248 0 : if (Platform.isAndroid) {
249 0 : SystemNavigator.pop();
250 0 : } else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) {
251 0 : print("Exiting...");
252 0 : exit(0);
253 : }
254 : }
255 : }
256 :
257 : // Invoked via notificationClickChannel by MyBroadcastReceiver in MainActivity.kt
258 : // coder beware: args["RemotePeer"] is actually a handle, and could be eg a groupID
259 0 : Future<void> _externalNotificationClicked(MethodCall call) async {
260 0 : var args = jsonDecode(call.arguments);
261 0 : _notificationSelectConvo(args["ProfileOnion"], args["Handle"]);
262 : }
263 :
264 0 : Future<void> _notificationSelectConvo(String profileOnion, int convoId) async {
265 0 : var profile = profs.getProfile(profileOnion)!;
266 0 : var convo = profile.contactList.getContact(convoId)!;
267 0 : if (profileOnion.isEmpty) {
268 : return;
269 : }
270 0 : Provider.of<AppState>(navKey.currentContext!, listen: false).initialScrollIndex = convo.unreadMessages;
271 0 : convo.unreadMessages = 0;
272 :
273 : // Clear nav path back to root
274 0 : while (navKey.currentState!.canPop()) {
275 0 : navKey.currentState!.pop();
276 : }
277 :
278 0 : Provider.of<AppState>(navKey.currentContext!, listen: false).selectedConversation = null;
279 0 : Provider.of<AppState>(navKey.currentContext!, listen: false).selectedProfile = profileOnion;
280 0 : Provider.of<AppState>(navKey.currentContext!, listen: false).selectedConversation = convoId;
281 :
282 0 : Navigator.of(navKey.currentContext!).push(
283 0 : PageRouteBuilder(
284 0 : settings: RouteSettings(name: "conversations"),
285 0 : pageBuilder: (c, a1, a2) {
286 0 : return OrientationBuilder(builder: (orientationBuilderContext, orientation) {
287 0 : return MultiProvider(
288 0 : providers: [ChangeNotifierProvider<ProfileInfoState>.value(value: profile), ChangeNotifierProvider<ContactListState>.value(value: profile.contactList)],
289 0 : builder: (innercontext, widget) {
290 0 : var appState = Provider.of<AppState>(navKey.currentContext!);
291 0 : var settings = Provider.of<Settings>(navKey.currentContext!);
292 0 : return settings.uiColumns(appState.isLandscape(innercontext)).length > 1 ? DoubleColumnView() : MessageView();
293 : });
294 : });
295 : },
296 0 : transitionsBuilder: (c, anim, a2, child) => FadeTransition(opacity: anim, child: child),
297 0 : transitionDuration: Duration(milliseconds: 200),
298 : ),
299 : );
300 : // 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
301 : // https://askubuntu.com/questions/1286206/how-to-skip-the-is-ready-notification-and-directly-open-apps-in-ubuntu-20-4
302 0 : await windowManager.show();
303 0 : await windowManager.focus();
304 : }
305 :
306 : // using windowManager flutter plugin until proper lifecycle management lands in desktop
307 :
308 0 : @override
309 : void onWindowFocus() {
310 0 : globalAppState.focus = true;
311 : }
312 :
313 0 : @override
314 : void onWindowBlur() {
315 0 : globalAppState.focus = false;
316 : }
317 :
318 0 : void onWindowClose() {
319 0 : Provider.of<FlwtchState>(navKey.currentContext!, listen: false).shutdown();
320 : }
321 :
322 0 : @override
323 : void dispose() {
324 0 : globalAppState.SetModalState(ModalState.shutdown);
325 0 : cwtch.Shutdown();
326 0 : windowManager.removeListener(this);
327 0 : cwtch.dispose();
328 0 : connectivityStream?.cancel();
329 0 : super.dispose();
330 : }
331 : }
|