From 052a68e3b60871f87f511976a999e3f66679b732 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Thu, 9 Oct 2025 00:45:00 +0530 Subject: [PATCH] chore: update dependencies and enhance connectivity service - Added `connectivity_plus` dependency to manage network connectivity status. - Updated `pubspec.yaml` to include the new dependency version. - Enhanced `ConnectivityService` to utilize `connectivity_plus` for improved connectivity monitoring and handling. - Refactored connectivity checks and state management for better performance and reliability. --- ios/Podfile.lock | 6 + ios/Runner/AppDelegate.swift | 21 +- lib/core/services/connectivity_service.dart | 211 +++++++++++++++----- pubspec.lock | 24 +++ pubspec.yaml | 1 + 5 files changed, 210 insertions(+), 53 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b53fdb4..09307a7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -34,6 +34,8 @@ PODS: - DKImagePickerController/PhotoGallery - Flutter - Flutter (1.0.0) + - flutter_local_notifications (0.0.1): + - Flutter - flutter_native_splash (2.4.3): - Flutter - flutter_secure_storage (6.0.0): @@ -82,6 +84,7 @@ PODS: DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_tts (from `.symlinks/plugins/flutter_tts/ios`) @@ -111,6 +114,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: Flutter + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_secure_storage: @@ -149,6 +154,7 @@ SPEC CHECKSUMS: DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 flutter_tts: b88dbc8655d3dc961bc4a796e4e16a4cc1795833 diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 42ece4c..059279d 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -122,11 +122,26 @@ class BackgroundStreamingHandler: NSObject { } private func saveStreamStates(_ states: [[String: Any]]) { - UserDefaults.standard.set(states, forKey: "ConduitActiveStreams") + do { + let jsonData = try JSONSerialization.data(withJSONObject: states, options: []) + UserDefaults.standard.set(jsonData, forKey: "ConduitActiveStreams") + } catch { + print("BackgroundStreamingHandler: Failed to serialize stream states: \(error)") + } } - + private func recoverStreamStates() -> [[String: Any]] { - return UserDefaults.standard.array(forKey: "ConduitActiveStreams") as? [[String: Any]] ?? [] + guard let jsonData = UserDefaults.standard.data(forKey: "ConduitActiveStreams") else { + return [] + } + do { + if let states = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [[String: Any]] { + return states + } + } catch { + print("BackgroundStreamingHandler: Failed to deserialize stream states: \(error)") + } + return [] } deinit { diff --git a/lib/core/services/connectivity_service.dart b/lib/core/services/connectivity_service.dart index b5519be..9be0e24 100644 --- a/lib/core/services/connectivity_service.dart +++ b/lib/core/services/connectivity_service.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -11,46 +12,171 @@ part 'connectivity_service.g.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) { + ConnectivityService(this._dio, this._ref, [Connectivity? connectivity]) + : _connectivity = connectivity ?? Connectivity() { _startConnectivityMonitoring(); } + final Dio _dio; + final Ref _ref; + final Connectivity _connectivity; + + final _connectivityController = + StreamController.broadcast(); + + Timer? _initialCheckTimer; + Timer? _pollTimer; + StreamSubscription>? _connectivitySubscription; + Completer? _activeCheck; + List? _lastConnectivityResults; + + ConnectivityStatus _lastStatus = ConnectivityStatus.checking; + Duration _interval = const Duration(seconds: 10); + int _recentFailures = 0; + int _lastLatencyMs = -1; + bool _hasNetwork = true; + bool _queuedImmediateCheck = false; + 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(); + _initialCheckTimer = Timer(const Duration(milliseconds: 800), () { + unawaited(_runConnectivityCheck(force: true)); }); - // Check periodically; interval adapts to recent failures - _connectivityTimer = Timer.periodic(_interval, (_) { - _checkConnectivity(); + _connectivitySubscription = _connectivity.onConnectivityChanged.listen(( + results, + ) { + unawaited(_handleConnectivityChange(results)); + }); + + unawaited( + _connectivity.checkConnectivity().then( + (results) => _handleConnectivityChange(results), + ), + ); + } + + Future _runConnectivityCheck({bool force = false}) async { + if (_connectivityController.isClosed) return; + + if (!_hasNetwork) { + _lastLatencyMs = -1; + _updateStatus(ConnectivityStatus.offline); + return; + } + + _initialCheckTimer?.cancel(); + _initialCheckTimer = null; + _cancelScheduledPoll(); + + final existingCheck = _activeCheck; + if (existingCheck != null) { + if (force) { + _queuedImmediateCheck = true; + } + await existingCheck.future; + if (force && _queuedImmediateCheck) { + _queuedImmediateCheck = false; + await _runConnectivityCheck(force: false); + } + return; + } + + final completer = Completer(); + _activeCheck = completer; + + if (_lastStatus != ConnectivityStatus.checking) { + _updateStatus(ConnectivityStatus.checking); + } + + try { + await _checkConnectivity(); + } finally { + completer.complete(); + _activeCheck = null; + } + + if (_queuedImmediateCheck) { + _queuedImmediateCheck = false; + await _runConnectivityCheck(force: false); + return; + } + + _scheduleNextPoll(); + } + + void _scheduleNextPoll() { + if (_connectivityController.isClosed || !_hasNetwork) { + return; + } + + _pollTimer = Timer(_interval, () { + _pollTimer = null; + unawaited(_runConnectivityCheck()); }); } + void _cancelScheduledPoll() { + _pollTimer?.cancel(); + _pollTimer = null; + } + + bool _haveSameConnectivity( + List previous, + List current, + ) { + if (identical(previous, current)) return true; + if (previous.length != current.length) return false; + final previousSet = previous.toSet(); + final currentSet = current.toSet(); + if (previousSet.length != currentSet.length) return false; + for (final value in previousSet) { + if (!currentSet.contains(value)) return false; + } + return true; + } + + Future _handleConnectivityChange( + List results, + ) async { + if (_connectivityController.isClosed) return; + + final previousResults = _lastConnectivityResults; + _lastConnectivityResults = results; + final hadNetwork = _hasNetwork; + _hasNetwork = results.any((result) => result != ConnectivityResult.none); + + if (!_hasNetwork) { + _lastLatencyMs = -1; + _queuedImmediateCheck = false; + _cancelScheduledPoll(); + _initialCheckTimer?.cancel(); + _initialCheckTimer = null; + _updateStatus(ConnectivityStatus.offline); + return; + } + + final networkTypeChanged = previousResults == null + ? true + : !_haveSameConnectivity(previousResults, results); + + if (!hadNetwork || + _lastStatus == ConnectivityStatus.offline || + networkTypeChanged) { + unawaited(_runConnectivityCheck(force: true)); + } + } + Future _checkConnectivity() async { - // Don't check connectivity if service is disposed if (_connectivityController.isClosed) return; final serverReachability = await _probeActiveServer(); @@ -75,7 +201,6 @@ class ConnectivityService { return; } - // No configured server to probe; assume usable connectivity so setup flows continue. _lastLatencyMs = -1; _updateStatus(ConnectivityStatus.online); } @@ -83,13 +208,11 @@ class ConnectivityService { void _updateStatus(ConnectivityStatus status) { if (_lastStatus != status) { _lastStatus = status; - // Only add to stream if controller is not closed if (!_connectivityController.isClosed) { _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) { @@ -104,25 +227,25 @@ class ConnectivityService { if (newInterval != _interval) { _interval = newInterval; - _connectivityTimer?.cancel(); - // Only create new timer if service is not disposed - if (!_connectivityController.isClosed) { - _connectivityTimer = Timer.periodic( - _interval, - (_) => _checkConnectivity(), - ); + _cancelScheduledPoll(); + if (_lastStatus != ConnectivityStatus.offline && _hasNetwork) { + _scheduleNextPoll(); } } } Future checkConnectivity() async { - await _checkConnectivity(); + await _runConnectivityCheck(force: true); return _lastStatus == ConnectivityStatus.online; } void dispose() { - _connectivityTimer?.cancel(); - _connectivityTimer = null; + _initialCheckTimer?.cancel(); + _initialCheckTimer = null; + _cancelScheduledPoll(); + _connectivitySubscription?.cancel(); + _connectivitySubscription = null; + _activeCheck = null; if (!_connectivityController.isClosed) { _connectivityController.close(); } @@ -151,14 +274,15 @@ class ConnectivityService { } Future _probeBaseEndpoint( - Uri uri, { + Uri baseUri, { bool updateLatency = false, }) async { try { final start = DateTime.now(); + final healthUri = baseUri.resolve('/health'); final response = await _dio .getUri( - uri, + healthUri, options: Options( method: 'GET', sendTimeout: const Duration(seconds: 3), @@ -175,7 +299,6 @@ class ConnectivityService { } return isHealthy; } catch (_) { - // Treat as unreachable. return false; } } @@ -205,27 +328,22 @@ class ConnectivityService { } if (parsed == null) return null; - // Return the base URL directly instead of resolving to /health return parsed; } } -// Providers final connectivityServiceProvider = Provider((ref) { - // Create a Dio instance with custom headers from the active server config final activeServer = ref.watch(activeServerProvider); return activeServer.maybeWhen( data: (server) { if (server == null) { - // No server configured, use lightweight Dio final dio = Dio(); final service = ConnectivityService(dio, ref); ref.onDispose(() => service.dispose()); return service; } - // Create Dio with custom headers from server config final dio = Dio( BaseOptions( baseUrl: server.url, @@ -234,7 +352,6 @@ final connectivityServiceProvider = Provider((ref) { followRedirects: true, maxRedirects: 5, validateStatus: (status) => status != null && status < 400, - // Add custom headers from server config headers: server.customHeaders.isNotEmpty ? Map.from(server.customHeaders) : null, @@ -246,7 +363,6 @@ final connectivityServiceProvider = Provider((ref) { return service; }, orElse: () { - // No server data available, use lightweight Dio final dio = Dio(); final service = ConnectivityService(dio, ref); ref.onDispose(() => service.dispose()); @@ -280,17 +396,12 @@ class ConnectivityStatusNotifier extends _$ConnectivityStatusNotifier { } 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.offline, - loading: () => true, // Assume online while checking - error: (_, _) => - true, // Assume online on error to avoid false offline states + loading: () => true, + error: (error, _) => true, ); }); - -// Dio provider (if not already defined elsewhere) -// Removed unused Dio provider to avoid confusion. Use ApiService instead. diff --git a/pubspec.lock b/pubspec.lock index b75a579..9b612c1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -225,6 +225,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: "33bae12a398f841c6cda09d1064212957265869104c478e5ad51e2fb26c3973c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + url: "https://pub.dev" + source: hosted + version: "2.0.1" convert: dependency: transitive description: @@ -917,6 +933,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" node_preamble: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 51bce66..34371b0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -68,6 +68,7 @@ dependencies: share_handler: ^0.0.19 riverpod_annotation: ^3.0.0 flutter_local_notifications: ^19.4.2 + connectivity_plus: ^7.0.0 # Clipboard functionality is available through flutter/services (part of Flutter SDK)