import 'dart:io'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:provider/provider.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/components/connection_state.dart'; import 'package:twonly/src/providers/contacts_change_provider.dart'; import 'package:twonly/src/providers/download_change_provider.dart'; import 'package:twonly/src/providers/messages_change_provider.dart'; import 'package:twonly/src/providers/settings_change_provider.dart'; import 'package:twonly/src/tasks/websocket_foreground_task.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/onboarding/onboarding_view.dart'; import 'package:twonly/src/views/home_view.dart'; import 'package:twonly/src/views/onboarding/register_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'dart:async'; // these global function can be called from anywhere to update // the ui when something changed. The callbacks will be set by // MyApp widget. // this callback is called by the apiProvider Function(bool) globalCallbackConnectionState = (a) {}; bool globalIsAppInBackground = true; // these two callbacks are called on updated to the corresponding database Function globalCallBackOnContactChange = () {}; Future Function(int) globalCallBackOnMessageChange = (a) async {}; Function(List, bool) globalCallBackOnDownloadChange = (a, b) {}; /// The Widget that configures your application. class MyApp extends StatefulWidget { const MyApp({super.key}); @override State createState() => _MyAppState(); } class _MyAppState extends State with WidgetsBindingObserver { bool _isConnected = false; bool wasPaused = false; @override void initState() { super.initState(); globalIsAppInBackground = false; WidgetsBinding.instance.addObserver(this); // init change provider to load data from the database context.read().update(); context.read().init(); // register global callbacks to the widget tree globalCallbackConnectionState = (isConnected) { setState(() { _isConnected = isConnected; }); }; globalCallBackOnContactChange = () { context.read().update(); }; globalCallBackOnDownloadChange = (token, add) { context.read().update(token, add); }; globalCallBackOnMessageChange = (userId) async { await context.read().updateLastMessageFor(userId); }; WidgetsBinding.instance.addPostFrameCallback((_) { _requestPermissions(); _initService(); }); initAsync(); } Future initAsync() async { // make sure the front end service will be killed FlutterForegroundTask.sendDataToTask(""); await FlutterForegroundTask.stopService(); // connect async to the backend api apiProvider.connect(); } Future _requestPermissions() async { // Android 13+, you need to allow notification permission to display foreground service notification. // // iOS: If you need notification, ask for permission. final NotificationPermission notificationPermission = await FlutterForegroundTask.checkNotificationPermission(); if (notificationPermission != NotificationPermission.granted) { await FlutterForegroundTask.requestNotificationPermission(); } if (Platform.isAndroid) { // Android 12+, there are restrictions on starting a foreground service. // // To restart the service on device reboot or unexpected problem, you need to allow below permission. if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) { // This function requires `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission. await FlutterForegroundTask.requestIgnoreBatteryOptimization(); } // Use this utility only if you provide services that require long-term survival, // such as exact alarm service, healthcare service, or Bluetooth communication. // // This utility requires the "android.permission.SCHEDULE_EXACT_ALARM" permission. // Using this permission may make app distribution difficult due to Google policy. // if (!await FlutterForegroundTask.canScheduleExactAlarms) { // When you call this function, will be gone to the settings page. // So you need to explain to the user why set it. // await FlutterForegroundTask.openAlarmsAndRemindersSettings(); // } } } void _initService() { FlutterForegroundTask.init( androidNotificationOptions: AndroidNotificationOptions( channelId: 'foreground_service', channelName: 'Foreground Service Notification', channelDescription: 'This notification appears when the foreground service is running.', onlyAlertOnce: true, ), iosNotificationOptions: const IOSNotificationOptions( showNotification: false, playSound: false, ), foregroundTaskOptions: ForegroundTaskOptions( eventAction: ForegroundTaskEventAction.repeat(5000), autoRunOnBoot: true, autoRunOnMyPackageReplaced: true, allowWakeLock: true, allowWifiLock: true, ), ); } Future _startService() async { if (await FlutterForegroundTask.isRunningService) { return FlutterForegroundTask.restartService(); } else { return FlutterForegroundTask.startService( serviceId: 256, notificationTitle: 'Staying connected to the server.', notificationText: 'Tap to return to the app', notificationIcon: NotificationIcon(metaDataName: "eu.twonly.service.TWONLY_LOGO"), notificationInitialRoute: '/chats', callback: startCallback, ); } } Future _stopService() async { if (context.mounted) { context.read().init(afterPaused: true); context.read().update(); } FlutterForegroundTask.sendDataToTask(""); await FlutterForegroundTask.stopService(); if (!apiProvider.isAuthenticated) { apiProvider.connect(); } } @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); if (state == AppLifecycleState.resumed) { if (wasPaused) { globalIsAppInBackground = false; _stopService(); } } else if (state == AppLifecycleState.paused) { wasPaused = true; globalIsAppInBackground = true; apiProvider.close(() { _startService(); }); } } @override void dispose() { // apiProvider.close(() {}); WidgetsBinding.instance.removeObserver(this); // disable globalCallbacks to the flutter tree globalCallbackConnectionState = (a) {}; globalCallBackOnDownloadChange = (a, b) {}; globalCallBackOnContactChange = () {}; globalCallBackOnMessageChange = (a) async {}; super.dispose(); } @override Widget build(BuildContext context) { // var isConnected = context.watch().isConnected; // Glue the SettingsController to the MaterialApp. // // The ListenableBuilder Widget listens to the SettingsController for changes. // Whenever the user updates their settings, the MaterialApp is rebuilt. return ListenableBuilder( listenable: context.watch(), builder: (BuildContext context, Widget? child) { return MaterialApp( restorationScopeId: 'app', localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], supportedLocales: const [ Locale('en', ''), Locale('de', ''), ], onGenerateTitle: (BuildContext context) => "twonly", theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF57CC99)), inputDecorationTheme: const InputDecorationTheme(border: OutlineInputBorder())), darkTheme: ThemeData.dark().copyWith( colorScheme: ColorScheme.fromSeed( brightness: Brightness.dark, // <-- the only line added seedColor: const Color(0xFF57CC99)), inputDecorationTheme: const InputDecorationTheme(border: OutlineInputBorder()), ), themeMode: context.watch().themeMode, initialRoute: '/', routes: { "/": (context) => MyAppMainWidget(isConnected: _isConnected, initialPage: 0), "/chats": (context) => MyAppMainWidget(isConnected: _isConnected, initialPage: 1) // home: MyAppMainWidget(isConnected: _isConnected, initialPage: 0), }, ); }, ); } } class MyAppMainWidget extends StatefulWidget { const MyAppMainWidget( {super.key, required this.isConnected, required this.initialPage}); final bool isConnected; final int initialPage; @override State createState() => _MyAppMainWidgetState(); } class _MyAppMainWidgetState extends State { Future _isUserCreated = isUserCreated(); // Assume this is a function that checks if the user is created bool _showOnboarding = true; // Initial state for onboarding @override Widget build(BuildContext context) { return Stack( children: [ FutureBuilder( future: _isUserCreated, builder: (context, snapshot) { if (snapshot.hasData) { if (snapshot.data!) { return HomeView(initialPage: widget.initialPage); } if (_showOnboarding) { return OnboardingView( callbackOnSuccess: () { setState(() { _showOnboarding = false; }); }, ); } return RegisterView( callbackOnSuccess: () { setState(() { _isUserCreated = isUserCreated(); }); }, ); } else { return Center(child: Container()); } }, ), if (!widget.isConnected) ConnectionInfo() ], ); } }