feat(android): Improve background service notification and time limit handling

feat: Optimize background streaming and keepalive mechanism
fix(background-streaming): Synchronize stream count between Flutter and Android
This commit is contained in:
cogwheel
2025-12-20 18:21:38 +05:30
parent f8d0911b23
commit 671b953f23
3 changed files with 210 additions and 58 deletions

View File

@@ -12,6 +12,10 @@ class BackgroundStreamingHandler {
'conduit/background_streaming',
);
/// Stream ID used for socket keepalive - not counted as an "active stream"
/// since it's a background task, not user-visible streaming.
static const String socketKeepaliveId = 'socket-keepalive';
static BackgroundStreamingHandler? _instance;
static BackgroundStreamingHandler get instance =>
_instance ??= BackgroundStreamingHandler._();
@@ -23,6 +27,10 @@ class BackgroundStreamingHandler {
final Set<String> _activeStreamIds = <String>{};
final Map<String, StreamState> _streamStates = <String, StreamState>{};
/// Returns count of actual content streams (excludes socket keepalive).
int get _userVisibleStreamCount =>
_activeStreamIds.where((id) => id != socketKeepaliveId).length;
// Callbacks for platform-specific events
void Function(List<String> streamIds)? onStreamsSuspending;
void Function()? onBackgroundTaskExpiring;
@@ -33,6 +41,15 @@ class BackgroundStreamingHandler {
void Function(String error, String errorType, List<String> streamIds)?
onServiceFailed;
/// Called when Android 14's foreground service time limit is reached.
/// The service stops after 5 hours (buffer before Android's 6-hour limit).
/// [remainingMinutes] will be 0 when this is called.
void Function(int remainingMinutes)? onBackgroundTimeLimitApproaching;
/// Called when microphone permission was requested but not granted,
/// causing fallback to dataSync-only foreground service type.
void Function()? onMicrophonePermissionFallback;
void _setupMethodCallHandler() {
_channel.setMethodCallHandler((call) async {
switch (call.method) {
@@ -106,6 +123,29 @@ class BackgroundStreamingHandler {
_streamStates.remove(streamId);
}
break;
case 'timeLimitApproaching':
final Map<String, dynamic> args =
call.arguments as Map<String, dynamic>;
final int remainingMinutes = args['remainingMinutes'] as int? ?? -1;
DebugLogger.stream(
'time-limit-approaching',
scope: 'background',
data: {'remainingMinutes': remainingMinutes},
);
onBackgroundTimeLimitApproaching?.call(remainingMinutes);
break;
case 'microphonePermissionFallback':
DebugLogger.stream(
'mic-permission-fallback',
scope: 'background',
);
onMicrophonePermissionFallback?.call();
break;
}
});
}
@@ -226,14 +266,44 @@ class BackgroundStreamingHandler {
Future<void> keepAlive() async {
if (!Platform.isIOS && !Platform.isAndroid) return;
// Skip keep-alive if no active streams - this ensures Android's count
// stays synchronized with Flutter's actual state
if (_activeStreamIds.isEmpty) return;
try {
await _channel.invokeMethod('keepAlive');
await _channel.invokeMethod('keepAlive', {
// Pass user-visible stream count (excludes socket-keepalive)
// for accurate logging, but service still runs for any background task
'streamCount': _userVisibleStreamCount,
});
DebugLogger.stream('keepalive-success', scope: 'background');
} catch (e) {
DebugLogger.error('keepalive-failed', scope: 'background', error: e);
}
}
/// Check if notification permission is granted (Android 13+ only).
///
/// Returns true on iOS, Android < 13, or if permission is granted.
/// Returns false if Android 13+ and permission is not granted.
Future<bool> checkNotificationPermission() async {
if (!Platform.isAndroid) return true;
try {
final bool? hasPermission = await _channel.invokeMethod<bool>(
'checkNotificationPermission',
);
return hasPermission ?? true;
} catch (e) {
DebugLogger.error(
'check-notification-permission-failed',
scope: 'background',
error: e,
);
return true; // Assume granted on error to not block functionality
}
}
/// Recover stream states from previous app session
Future<List<StreamState>> recoverStreamStates() async {
if (!Platform.isIOS && !Platform.isAndroid) return [];