// lib/main.dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'dart:async'; // Import Timer import 'package:provider/single_child_widget.dart'; import 'package:environment_monitoring_app/services/api_service.dart'; import 'package:environment_monitoring_app/services/local_storage_service.dart'; import 'package:environment_monitoring_app/services/river_in_situ_sampling_service.dart'; import 'package:environment_monitoring_app/services/river_manual_triennial_sampling_service.dart'; import 'package:environment_monitoring_app/services/air_sampling_service.dart'; import 'package:environment_monitoring_app/services/telegram_service.dart'; import 'package:environment_monitoring_app/services/server_config_service.dart'; import 'package:environment_monitoring_app/services/retry_service.dart'; import 'package:environment_monitoring_app/services/marine_in_situ_sampling_service.dart'; import 'package:environment_monitoring_app/services/marine_npe_report_service.dart'; import 'package:environment_monitoring_app/services/marine_tarball_sampling_service.dart'; import 'package:environment_monitoring_app/services/marine_manual_pre_departure_service.dart'; import 'package:environment_monitoring_app/services/marine_manual_sonde_calibration_service.dart'; import 'package:environment_monitoring_app/services/marine_manual_equipment_maintenance_service.dart'; import 'package:environment_monitoring_app/theme.dart'; import 'package:environment_monitoring_app/auth_provider.dart'; // Core Screens import 'package:environment_monitoring_app/screens/login.dart'; import 'package:environment_monitoring_app/screens/register.dart'; import 'package:environment_monitoring_app/screens/forgot_password.dart'; import 'package:environment_monitoring_app/screens/logout.dart'; import 'package:environment_monitoring_app/home_page.dart'; import 'package:environment_monitoring_app/screens/profile.dart'; import 'package:environment_monitoring_app/screens/settings.dart'; // Department Home Pages import 'package:environment_monitoring_app/screens/air/air_home_page.dart'; import 'package:environment_monitoring_app/screens/river/river_home_page.dart'; import 'package:environment_monitoring_app/screens/marine/marine_home_page.dart'; // Air Screens import 'package:environment_monitoring_app/screens/air/manual/air_manual_info_centre_document.dart'; import 'package:environment_monitoring_app/screens/air/manual/air_manual_installation_screen.dart'; import 'package:environment_monitoring_app/screens/air/manual/air_manual_collection_screen.dart'; import 'package:environment_monitoring_app/screens/air/manual/air_manual_report.dart' as airManualReport; import 'package:environment_monitoring_app/screens/air/manual/air_manual_data_status_log.dart' as airManualDataStatusLog; import 'package:environment_monitoring_app/screens/air/manual/air_manual_image_request.dart' as airManualImageRequest; import 'package:environment_monitoring_app/screens/air/continuous/air_continuous_info_centre_document.dart'; import 'package:environment_monitoring_app/screens/air/continuous/overview.dart' as airContinuousOverview; import 'package:environment_monitoring_app/screens/air/continuous/entry.dart' as airContinuousEntry; import 'package:environment_monitoring_app/screens/air/continuous/report.dart' as airContinuousReport; import 'package:environment_monitoring_app/screens/air/investigative/air_investigative_info_centre_document.dart'; import 'package:environment_monitoring_app/screens/air/investigative/overview.dart' as airInvestigativeOverview; import 'package:environment_monitoring_app/screens/air/investigative/entry.dart' as airInvestigativeEntry; import 'package:environment_monitoring_app/screens/air/investigative/report.dart' as airInvestigativeReport; // River Screens import 'package:environment_monitoring_app/screens/river/manual/river_manual_info_centre_document.dart'; import 'package:environment_monitoring_app/screens/river/manual/in_situ_sampling.dart' as riverManualInSituSampling; import 'package:environment_monitoring_app/screens/river/manual/river_manual_data_status_log.dart' as riverManualDataStatusLog; import 'package:environment_monitoring_app/screens/river/manual/river_manual_report.dart' as riverManualReport; import 'package:environment_monitoring_app/screens/river/manual/triennial/river_manual_triennial_sampling.dart' as riverManualTriennialSampling; import 'package:environment_monitoring_app/screens/river/manual/river_manual_image_request.dart' as riverManualImageRequest; import 'package:environment_monitoring_app/screens/river/continuous/river_continuous_info_centre_document.dart'; import 'package:environment_monitoring_app/screens/river/continuous/overview.dart' as riverContinuousOverview; import 'package:environment_monitoring_app/screens/river/continuous/entry.dart' as riverContinuousEntry; import 'package:environment_monitoring_app/screens/river/continuous/report.dart' as riverContinuousReport; import 'package:environment_monitoring_app/screens/river/investigative/river_investigative_info_centre_document.dart'; import 'package:environment_monitoring_app/screens/river/investigative/overview.dart' as riverInvestigativeOverview; import 'package:environment_monitoring_app/screens/river/investigative/entry.dart' as riverInvestigativeEntry; import 'package:environment_monitoring_app/screens/river/investigative/report.dart' as riverInvestigativeReport; // Marine Screens import 'package:environment_monitoring_app/screens/marine/manual/info_centre_document.dart' as marineManualInfoCentreDocument; import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_pre_sampling.dart' as marineManualPreSampling; import 'package:environment_monitoring_app/screens/marine/manual/in_situ_sampling.dart' as marineManualInSituSampling; import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_report.dart' as marineManualReport; import 'package:environment_monitoring_app/screens/marine/manual/reports/marine_manual_npe_report_hub.dart'; import 'package:environment_monitoring_app/screens/marine/manual/reports/marine_manual_pre_departure_checklist_screen.dart' as marineManualPreDepartureChecklist; import 'package:environment_monitoring_app/screens/marine/manual/reports/marine_manual_sonde_calibration_screen.dart' as marineManualSondeCalibration; import 'package:environment_monitoring_app/screens/marine/manual/reports/marine_manual_equipment_maintenance_screen.dart' as marineManualEquipmentMaintenance; import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_data_status_log.dart' as marineManualDataStatusLog; import 'package:environment_monitoring_app/screens/marine/manual/marine_image_request.dart' as marineManualImageRequest; import 'package:environment_monitoring_app/screens/marine/continuous/marine_continuous_info_centre_document.dart'; import 'package:environment_monitoring_app/screens/marine/continuous/overview.dart' as marineContinuousOverview; import 'package:environment_monitoring_app/screens/marine/continuous/entry.dart' as marineContinuousEntry; import 'package:environment_monitoring_app/screens/marine/continuous/report.dart' as marineContinuousReport; import 'package:environment_monitoring_app/screens/marine/investigative/marine_investigative_info_centre_document.dart'; import 'package:environment_monitoring_app/screens/marine/investigative/overview.dart' as marineInvestigativeOverview; import 'package:environment_monitoring_app/screens/marine/investigative/entry.dart' as marineInvestigativeEntry; import 'package:environment_monitoring_app/screens/marine/investigative/report.dart' as marineInvestigativeReport; import 'package:environment_monitoring_app/models/tarball_data.dart'; import 'package:environment_monitoring_app/screens/marine/manual/tarball_sampling_step1.dart'; import 'package:environment_monitoring_app/screens/marine/manual/tarball_sampling_step2.dart'; import 'package:environment_monitoring_app/screens/marine/manual/tarball_sampling_step3_summary.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // Create singleton instances of core services before running the app final DatabaseHelper databaseHelper = DatabaseHelper(); final TelegramService telegramService = TelegramService(); final ApiService apiService = ApiService(telegramService: telegramService); final RetryService retryService = RetryService(); final MarineInSituSamplingService marineInSituService = MarineInSituSamplingService(telegramService); final RiverInSituSamplingService riverInSituService = RiverInSituSamplingService(telegramService); telegramService.setApiService(apiService); // The AuthProvider needs to be created here so it can be passed to the retry service. final authProvider = AuthProvider( apiService: apiService, dbHelper: databaseHelper, serverConfigService: ServerConfigService(), retryService: retryService, ); // Initialize the retry service with all its dependencies. retryService.initialize( marineInSituService: marineInSituService, riverInSituService: riverInSituService, authProvider: authProvider, ); setupPeriodicServices(telegramService, retryService); runApp( MultiProvider( providers: [ ChangeNotifierProvider.value(value: authProvider), Provider(create: (_) => apiService), Provider(create: (_) => databaseHelper), Provider(create: (_) => telegramService), Provider(create: (_) => LocalStorageService()), Provider.value(value: retryService), Provider.value(value: marineInSituService), Provider.value(value: riverInSituService), Provider(create: (context) => RiverManualTriennialSamplingService(telegramService)), Provider(create: (context) => AirSamplingService(databaseHelper, telegramService)), Provider(create: (context) => MarineTarballSamplingService(telegramService)), Provider(create: (context) => MarineNpeReportService(Provider.of(context, listen: false))), // --- UPDATED: Inject ApiService into the service constructors --- Provider(create: (context) => MarineManualPreDepartureService( Provider.of(context, listen: false) )), Provider(create: (context) => MarineManualSondeCalibrationService( Provider.of(context, listen: false) )), Provider(create: (context) => MarineManualEquipmentMaintenanceService( Provider.of(context, listen: false) )), ], child: const RootApp(), ), ); } void setupPeriodicServices(TelegramService telegramService, RetryService retryService) { // Initial processing on startup (delayed) Future.delayed(const Duration(seconds: 5), () { debugPrint("[Main] Performing initial alert queue processing on app start."); telegramService.processAlertQueue(); debugPrint("[Main] Performing initial retry queue processing on app start."); retryService.processRetryQueue(); }); // Start recurring timers to process both queues every 5 minutes. Timer.periodic(const Duration(minutes: 5), (timer) { debugPrint("[Main] Periodic check: Processing Telegram alert queue..."); telegramService.processAlertQueue(); debugPrint("[Main] Periodic check: Processing main retry queue..."); retryService.processRetryQueue(); }); } class RootApp extends StatefulWidget { const RootApp({super.key}); @override State createState() => _RootAppState(); } class _RootAppState extends State { @override void initState() { super.initState(); _initializeConnectivityListener(); _performInitialSessionCheck(); } /// Initial check when app loads to see if we need to transition from offline to online. void _performInitialSessionCheck() async { // Wait a moment for providers to be fully available. await Future.delayed(const Duration(milliseconds: 100)); if (mounted) { final authProvider = Provider.of(context, listen: false); // Perform proactive token refresh on app start await authProvider.proactiveTokenRefresh(); // First, try to transition from an offline placeholder token to an online one. final didTransition = await authProvider.checkAndTransitionToOnlineSession(); // If no transition happened (i.e., we were already supposed to be online), validate the session. if (!didTransition) { authProvider.validateAndRefreshSession(); } } } /// Listens for connectivity changes to trigger auto-relogin or queue processing. void _initializeConnectivityListener() { Connectivity().onConnectivityChanged.listen((List results) { if (!results.contains(ConnectivityResult.none)) { debugPrint("[Main] Internet connection detected."); if (mounted) { // Access services from provider context final authProvider = Provider.of(context, listen: false); final telegramService = Provider.of(context, listen: false); final retryService = Provider.of(context, listen: false); // When connection is restored, always try to transition/validate the session. authProvider.checkAndTransitionToOnlineSession().then((didTransition) { if (!didTransition) { authProvider.validateAndRefreshSession(); } }); // Process queues telegramService.processAlertQueue(); retryService.processRetryQueue(); } } else { debugPrint("[Main] Internet connection lost."); } }); } @override Widget build(BuildContext context) { return Consumer( builder: (context, auth, child) { Widget homeWidget; if (auth.isLoading) { homeWidget = const SplashScreen(); } else if (auth.isLoggedIn) { homeWidget = const SessionAwareWrapper(child: HomePage()); } else { homeWidget = const LoginScreen(); } return MaterialApp( title: 'Environment Monitoring App', theme: AppTheme.darkBlueTheme, debugShowCheckedModeBanner: false, home: homeWidget, onGenerateRoute: (settings) { if (settings.name == '/marine/manual/tarball/step2') { final args = settings.arguments as TarballSamplingData; return MaterialPageRoute(builder: (context) { return TarballSamplingStep2(data: args); }); } if (settings.name == '/marine/manual/tarball/step3') { final args = settings.arguments as TarballSamplingData; return MaterialPageRoute(builder: (context) { return TarballSamplingStep3Summary(data: args); }); } if (settings.name == '/marine/manual/data-log') { return MaterialPageRoute(builder: (context) { return const marineManualDataStatusLog.MarineManualDataStatusLog(); }); } return null; }, routes: { // Auth Routes '/register': (context) => const RegisterScreen(), '/forgot-password': (context) => ForgotPasswordScreen(), '/logout': (context) => const LogoutScreen(), '/home': (context) => const HomePage(), '/profile': (context) => const ProfileScreen(), '/settings': (context) => const SettingsScreen(), // Department Home Pages '/air/home': (context) => const AirHomePage(), '/river/home': (context) => const RiverHomePage(), '/marine/home': (context) => const MarineHomePage(), // Air Manual '/air/manual/info': (context) => const AirManualInfoCentreDocument(), '/air/manual/installation': (context) => const AirManualInstallationScreen(), '/air/manual/collection': (context) => const AirManualCollectionScreen(), '/air/manual/report': (context) => airManualReport.AirManualReport(), '/air/manual/data-log': (context) => airManualDataStatusLog.AirManualDataStatusLog(), '/air/manual/image-request': (context) => airManualImageRequest.AirManualImageRequest(), // Air Continuous '/air/continuous/info': (context) => const AirContinuousInfoCentreDocument(), '/air/continuous/overview': (context) => airContinuousOverview.OverviewScreen(), '/air/continuous/entry': (context) => airContinuousEntry.EntryScreen(), '/air/continuous/report': (context) => airContinuousReport.ReportScreen(), // Air Investigative '/air/investigative/info': (context) => const AirInvestigativeInfoCentreDocument(), '/air/investigative/overview': (context) => airInvestigativeOverview.OverviewScreen(), '/air/investigative/entry': (context) => airInvestigativeEntry.EntryScreen(), '/air/investigative/report': (context) => airInvestigativeReport.ReportScreen(), // River Manual '/river/manual/info': (context) => const RiverManualInfoCentreDocument(), '/river/manual/in-situ': (context) => riverManualInSituSampling.RiverInSituSamplingScreen(), '/river/manual/report': (context) => riverManualReport.RiverManualReport(), '/river/manual/triennial': (context) => riverManualTriennialSampling.RiverManualTriennialSamplingScreen(), '/river/manual/data-log': (context) => riverManualDataStatusLog.RiverManualDataStatusLog(), '/river/manual/image-request': (context) => riverManualImageRequest.RiverManualImageRequest(), // River Continuous '/river/continuous/info': (context) => const RiverContinuousInfoCentreDocument(), '/river/continuous/overview': (context) => riverContinuousOverview.OverviewScreen(), '/river/continuous/entry': (context) => riverContinuousEntry.EntryScreen(), '/river/continuous/report': (context) => riverContinuousReport.ReportScreen(), // River Investigative '/river/investigative/info': (context) => const RiverInvestigativeInfoCentreDocument(), '/river/investigative/overview': (context) => riverInvestigativeOverview.OverviewScreen(), '/river/investigative/entry': (context) => riverInvestigativeEntry.EntryScreen(), '/river/investigative/report': (context) => riverInvestigativeReport.ReportScreen(), // Marine Manual '/marine/manual/info': (context) => marineManualInfoCentreDocument.MarineInfoCentreDocument(), '/marine/manual/pre-sampling': (context) => marineManualPreSampling.MarineManualPreSampling(), '/marine/manual/in-situ': (context) => marineManualInSituSampling.MarineInSituSampling(), '/marine/manual/tarball': (context) => const TarballSamplingStep1(), '/marine/manual/report': (context) => const marineManualReport.MarineManualReportHomePage(), '/marine/manual/report/npe': (context) => const MarineManualNPEReportHub(), '/marine/manual/report/pre-departure': (context) => const marineManualPreDepartureChecklist.MarineManualPreDepartureChecklistScreen(), '/marine/manual/report/calibration': (context) => const marineManualSondeCalibration.MarineManualSondeCalibrationScreen(), '/marine/manual/report/maintenance': (context) => const marineManualEquipmentMaintenance.MarineManualEquipmentMaintenanceScreen(), '/marine/manual/image-request': (context) => const marineManualImageRequest.MarineImageRequestScreen(), // Marine Continuous '/marine/continuous/info': (context) => const MarineContinuousInfoCentreDocument(), '/marine/continuous/overview': (context) => marineContinuousOverview.OverviewScreen(), '/marine/continuous/entry': (context) => marineContinuousEntry.EntryScreen(), '/marine/continuous/report': (context) => marineContinuousReport.ReportScreen(), // Marine Investigative '/marine/investigative/info': (context) => const MarineInvestigativeInfoCentreDocument(), '/marine/investigative/overview': (context) => marineInvestigativeOverview.OverviewScreen(), '/marine/investigative/entry': (context) => marineInvestigativeEntry.EntryScreen(), '/marine/investigative/report': (context) => marineInvestigativeReport.ReportScreen(), }, ); }, ); } } class SessionAwareWrapper extends StatefulWidget { final Widget child; const SessionAwareWrapper({super.key, required this.child}); @override State createState() => _SessionAwareWrapperState(); } class _SessionAwareWrapperState extends State { bool _isDialogShowing = false; @override void didChangeDependencies() { super.didChangeDependencies(); final auth = Provider.of(context); if (auth.isSessionExpired && !_isDialogShowing) { // Use addPostFrameCallback to show dialog after the build phase. WidgetsBinding.instance.addPostFrameCallback((_) { _showSessionExpiredDialog(); }); } } Future _showSessionExpiredDialog() async { setState(() => _isDialogShowing = true); await showDialog( context: context, barrierDismissible: false, // User must make a choice builder: (BuildContext dialogContext) { final auth = Provider.of(context, listen: false); return AlertDialog( title: const Text("Session Expired"), content: const Text( "Your online session has expired. You can continue working offline, but you will not be able to sync data until you log in again."), actions: [ TextButton( child: const Text("Continue Offline"), onPressed: () { Navigator.of(dialogContext).pop(); // Just close the dialog }, ), ElevatedButton( child: const Text("Login Now"), onPressed: () { // Logout clears all state and pushes to login screen via the RootApp builder auth.logout(); Navigator.of(dialogContext).pop(); }, ), ], ); }, ); // Once the dialog is dismissed, reset the flag. if (mounted) { setState(() => _isDialogShowing = false); } } @override Widget build(BuildContext context) { // This widget just returns its child, its only job is to show the dialog. return widget.child; } } class SplashScreen extends StatelessWidget { const SplashScreen({super.key}); @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( 'assets/icon4.png', // Ensure this asset exists height: 360, width: 360, ), const SizedBox(height: 24), const CircularProgressIndicator(), const SizedBox(height: 20), const Text('Loading MMS data...', style: TextStyle(fontSize: 16)), ], ), ), ); } }