import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:dio/dio.dart'; import '../providers/app_providers.dart'; enum ConnectivityStatus { online, offline, checking } class ConnectivityService { final Dio _dio; final Ref _ref; Timer? _connectivityTimer; final _connectivityController = StreamController.broadcast(); ConnectivityStatus _lastStatus = ConnectivityStatus.checking; int _recentFailures = 0; Duration _interval = const Duration(seconds: 10); int _lastLatencyMs = -1; ConnectivityService(this._dio, this._ref) { _startConnectivityMonitoring(); } Stream get connectivityStream => _connectivityController.stream; ConnectivityStatus get currentStatus => _lastStatus; int get lastLatencyMs => _lastLatencyMs; /// Stream that emits true when connected, false when offline Stream get isConnected => connectivityStream.map((status) => status == ConnectivityStatus.online); /// Check if currently connected bool get isCurrentlyConnected => _lastStatus == ConnectivityStatus.online; void _startConnectivityMonitoring() { // Initial check after a brief delay to avoid showing offline during startup Timer(const Duration(milliseconds: 800), () { _checkConnectivity(); }); // Check periodically; interval adapts to recent failures _connectivityTimer = Timer.periodic(_interval, (_) { _checkConnectivity(); }); } Future _checkConnectivity() async { final serverReachability = await _probeActiveServer(); if (serverReachability != null) { if (serverReachability) { _updateStatus(ConnectivityStatus.online); } else { _lastLatencyMs = -1; _updateStatus(ConnectivityStatus.offline); } return; } final fallbackReachability = await _probeAnyKnownServer(); if (fallbackReachability != null) { if (fallbackReachability) { _updateStatus(ConnectivityStatus.online); } else { _lastLatencyMs = -1; _updateStatus(ConnectivityStatus.offline); } return; } // No configured server to probe; assume usable connectivity so setup flows continue. _lastLatencyMs = -1; _updateStatus(ConnectivityStatus.online); } void _updateStatus(ConnectivityStatus status) { if (_lastStatus != status) { _lastStatus = status; _connectivityController.add(status); } // Adapt polling interval based on recent failures to reduce battery/CPU if (status == ConnectivityStatus.offline) { _recentFailures = (_recentFailures + 1).clamp(0, 10); } else if (status == ConnectivityStatus.online) { _recentFailures = 0; } final newInterval = _recentFailures >= 3 ? const Duration(seconds: 20) : _recentFailures == 2 ? const Duration(seconds: 15) : const Duration(seconds: 10); if (newInterval != _interval) { _interval = newInterval; _connectivityTimer?.cancel(); _connectivityTimer = Timer.periodic( _interval, (_) => _checkConnectivity(), ); } } Future checkConnectivity() async { await _checkConnectivity(); return _lastStatus == ConnectivityStatus.online; } void dispose() { _connectivityTimer?.cancel(); _connectivityController.close(); } Future _probeActiveServer() async { final healthUri = _resolveHealthUri(); if (healthUri == null) return null; return _probeHealthEndpoint(healthUri, updateLatency: true); } Future _probeAnyKnownServer() async { try { final configs = await _ref.read(serverConfigsProvider.future); for (final config in configs) { final uri = _buildHealthUri(config.url); if (uri == null) continue; final result = await _probeHealthEndpoint(uri); if (result != null) { return result; } } } catch (_) {} return null; } Future _probeHealthEndpoint( Uri uri, { bool updateLatency = false, }) async { try { final start = DateTime.now(); final response = await _dio .getUri( uri, options: Options( method: 'GET', sendTimeout: const Duration(seconds: 3), receiveTimeout: const Duration(seconds: 3), followRedirects: false, validateStatus: (status) => status != null && status < 500, ), ) .timeout(const Duration(seconds: 4)); final isHealthy = response.statusCode == 200 && _responseIndicatesHealth(response.data); if (isHealthy && updateLatency) { _lastLatencyMs = DateTime.now().difference(start).inMilliseconds; } return isHealthy; } catch (_) { // Treat as unreachable. return false; } } Uri? _resolveHealthUri() { final api = _ref.read(apiServiceProvider); if (api != null) { return _buildHealthUri(api.baseUrl); } final activeServer = _ref.read(activeServerProvider); return activeServer.maybeWhen( data: (server) => server != null ? _buildHealthUri(server.url) : null, orElse: () => null, ); } Uri? _buildHealthUri(String baseUrl) { if (baseUrl.isEmpty) return null; Uri? parsed = Uri.tryParse(baseUrl.trim()); if (parsed == null) return null; if (!parsed.hasScheme) { parsed = Uri.tryParse('https://$baseUrl') ?? Uri.tryParse('http://$baseUrl'); } if (parsed == null) return null; return parsed.resolve('health'); } bool _responseIndicatesHealth(dynamic data) { if (data is Map) { final dynamic status = data['status']; if (status is bool) return status; if (status is num) return status != 0; } return true; } } // Providers final connectivityServiceProvider = Provider((ref) { // Use a lightweight Dio instance only for connectivity checks final dio = Dio(); final service = ConnectivityService(dio, ref); ref.onDispose(() => service.dispose()); return service; }); final connectivityStatusProvider = StreamProvider((ref) { final service = ref.watch(connectivityServiceProvider); return service.connectivityStream; }); final isOnlineProvider = Provider((ref) { // In reviewer mode, treat app as online to enable flows final reviewerMode = ref.watch(reviewerModeProvider); if (reviewerMode) return true; final status = ref.watch(connectivityStatusProvider); return status.when( data: (status) => status == ConnectivityStatus.online, loading: () => true, // Assume online while checking error: (_, _) => true, // Assume online on error to avoid false offline states ); }); // Dio provider (if not already defined elsewhere) // Removed unused Dio provider to avoid confusion. Use ApiService instead.