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:
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user