feat: enhance background streaming service with foreground handling

- Improved BackgroundStreamingService to manage foreground service notifications more effectively, ensuring compliance with Android requirements.
- Implemented dynamic foreground service type resolution based on microphone permission, enhancing service behavior based on app state.
- Added checks for app foreground status in connectivity management, improving responsiveness to network changes.
- Refactored notification handling to streamline service lifecycle management and improve code maintainability.
This commit is contained in:
cogwheel0
2025-10-09 15:47:27 +05:30
parent 162a5e0781
commit c073d71363
4 changed files with 161 additions and 9 deletions

View File

@@ -58,13 +58,18 @@ void _scheduleConversationWarmup(Ref ref, {bool force = false}) {
return;
}
final connectivity = ref.read(connectivityServiceProvider);
if (!connectivity.isAppForeground) {
return;
}
final isOnline = ref.read(isOnlineProvider);
if (!isOnline) {
return;
}
// If network latency is high, delay warmup further to reduce contention
final latency = ref.read(connectivityServiceProvider).lastLatencyMs;
final latency = connectivity.lastLatencyMs;
final extraDelay = latency > 800
? 400
: latency > 400
@@ -99,6 +104,11 @@ void _scheduleConversationWarmup(Ref ref, {bool force = false}) {
await Future.delayed(Duration(milliseconds: extraDelay));
}
try {
if (!ref.read(connectivityServiceProvider).isAppForeground) {
statusController.set(_ConversationWarmupStatus.idle);
return;
}
final existing = ref.read(conversationsProvider);
if (existing.hasValue) {
statusController.set(_ConversationWarmupStatus.complete);

View File

@@ -85,6 +85,7 @@ class RouterNotifier extends ChangeNotifier {
}
final authState = ref.read(authNavigationStateProvider);
final connectivityService = ref.read(connectivityServiceProvider);
if (location == Routes.serverConnection) {
return authState == AuthNavigationState.authenticated
@@ -102,7 +103,9 @@ class RouterNotifier extends ChangeNotifier {
final shouldShowConnectionIssue =
!reviewerMode &&
connectivity == ConnectivityStatus.offline &&
authState == AuthNavigationState.authenticated;
authState == AuthNavigationState.authenticated &&
connectivityService.isAppForeground &&
!connectivityService.isOfflineSuppressed;
if (shouldShowConnectionIssue) {
return location == Routes.connectionIssue ? null : Routes.connectionIssue;

View File

@@ -5,6 +5,7 @@ import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -24,7 +25,7 @@ enum ConnectivityStatus { online, offline }
/// - Assumes online by default (optimistic)
/// - Only shows offline when explicitly confirmed
/// - Minimal state changes during startup
class ConnectivityService {
class ConnectivityService with WidgetsBindingObserver {
ConnectivityService(this._dio, this._ref, [Connectivity? connectivity])
: _connectivity = connectivity ?? Connectivity() {
_initialize();
@@ -38,6 +39,8 @@ class ConnectivityService {
StreamSubscription<List<ConnectivityResult>>? _connectivitySubscription;
Timer? _pollTimer;
Timer? _noNetworkGraceTimer;
DateTime? _offlineSuppressedUntil;
bool _isAppForeground = true;
// Start optimistically as online to prevent flash
ConnectivityStatus _currentStatus = ConnectivityStatus.online;
@@ -51,6 +54,8 @@ class ConnectivityService {
ConnectivityStatus get currentStatus => _currentStatus;
int get lastLatencyMs => _lastLatencyMs;
bool get isOnline => _currentStatus == ConnectivityStatus.online;
bool get isAppForeground => _isAppForeground;
bool get isOfflineSuppressed => _isOfflineSuppressed;
void _initialize() {
// Listen to network interface changes
@@ -64,6 +69,9 @@ class ConnectivityService {
// Start periodic health checks
_scheduleNextCheck();
WidgetsBinding.instance.addObserver(this);
_extendOfflineSuppression(const Duration(seconds: 3));
}
void _handleNetworkChange(List<ConnectivityResult> results) {
@@ -105,6 +113,10 @@ class ConnectivityService {
void _scheduleNextCheck({Duration? delay}) {
_stopPolling();
if (!_isAppForeground) {
return;
}
// Adaptive polling based on failure count
final interval =
delay ??
@@ -222,6 +234,12 @@ class ConnectivityService {
if (_currentStatus != newStatus && !_statusController.isClosed) {
_currentStatus = newStatus;
_statusController.add(newStatus);
} else {
_currentStatus = newStatus;
}
if (newStatus == ConnectivityStatus.online) {
_offlineSuppressedUntil = null;
}
}
@@ -230,6 +248,27 @@ class ConnectivityService {
_noNetworkGraceTimer = null;
}
bool get _isOfflineSuppressed {
final until = _offlineSuppressedUntil;
if (until == null) {
return false;
}
if (DateTime.now().isBefore(until)) {
return true;
}
_offlineSuppressedUntil = null;
return false;
}
void _extendOfflineSuppression(Duration duration) {
final base = DateTime.now();
final proposed = base.add(duration);
if (_offlineSuppressedUntil == null ||
proposed.isAfter(_offlineSuppressedUntil!)) {
_offlineSuppressedUntil = proposed;
}
}
/// Manually trigger a connectivity check.
Future<bool> checkNow() async {
await _checkServerHealth();
@@ -241,6 +280,7 @@ class ConnectivityService {
_connectivitySubscription?.cancel();
_connectivitySubscription = null;
_cancelNoNetworkGrace();
WidgetsBinding.instance.removeObserver(this);
if (!_statusController.isClosed) {
_statusController.close();
@@ -287,6 +327,29 @@ class ConnectivityService {
return parsed;
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
_isAppForeground = true;
_extendOfflineSuppression(const Duration(seconds: 4));
// Give networking stack a short window to settle
_scheduleNextCheck(delay: const Duration(milliseconds: 500));
break;
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
case AppLifecycleState.hidden:
_isAppForeground = false;
_extendOfflineSuppression(const Duration(seconds: 6));
_stopPolling();
break;
case AppLifecycleState.detached:
_isAppForeground = false;
_stopPolling();
break;
}
}
}
// Provider for the connectivity service