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