fix: guard against post-disposal stream controller access

This commit is contained in:
edde746
2026-02-18 09:20:48 +01:00
parent aae1b1c45c
commit 8d5b5d2e0c
3 changed files with 48 additions and 33 deletions
+7 -1
View File
@@ -36,6 +36,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
int? get textureId => null;
StreamSubscription? _eventSubscription;
StreamSubscription? _logSubscription;
bool _disposed = false;
final _throttleSw = Stopwatch()..start();
int _lastEmitMs = 0;
@@ -63,7 +64,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
PlayerBase() {
_streams = createStreams();
_setupEventListener();
logController.stream.listen(_forwardToAppLogger);
_logSubscription = logController.stream.listen(_forwardToAppLogger);
}
void _forwardToAppLogger(PlayerLog log) {
@@ -89,6 +90,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
_eventSubscription = eventChannel.receiveBroadcastStream().listen(
_handleEvent,
onError: (error) {
if (_disposed) return;
errorController.add(error.toString());
},
);
@@ -108,6 +110,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
}
void _handleEvent(dynamic event) {
if (_disposed) return;
if (event is List && event.length == 2) {
final name = _propIdToName[event[0] as int];
if (name != null) {
@@ -125,6 +128,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
/// Handle a property change event from the platform.
/// Subclasses can override this to handle platform-specific properties.
void handlePropertyChange(String name, dynamic value) {
if (_disposed) return;
switch (name) {
case 'pause':
final playing = value == false;
@@ -258,6 +262,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
/// Handle a player event from the platform.
/// Subclasses can override this to handle platform-specific events.
void handlePlayerEvent(String name, Map? data) {
if (_disposed) return;
switch (name) {
case 'end-file':
final reason = data?['reason'] as String?;
@@ -450,6 +455,7 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
_disposed = true;
await _eventSubscription?.cancel();
await _logSubscription?.cancel();
await methodChannel.invokeMethod('dispose');
await closeStreamControllers();
}
+40 -32
View File
@@ -240,6 +240,7 @@ class DownloadManagerService {
// Prevents concurrent _processQueue calls
bool _isProcessingQueue = false;
bool _disposed = false;
// Debounce timers for DB progress writes (keyed by globalKey).
// UI progress streams are still real-time; only the DB write is debounced.
@@ -626,6 +627,7 @@ class DownloadManagerService {
/// Callback: background_downloader progress update
void _onTaskProgress(TaskProgressUpdate update) {
if (_disposed) return;
final globalKey = update.task.metaData;
if (globalKey.isEmpty || update.progress < 0) return;
@@ -666,44 +668,49 @@ class DownloadManagerService {
/// Callback: background_downloader status change
void _onTaskStatusChanged(TaskStatusUpdate update) {
if (_disposed) return;
final globalKey = update.task.metaData;
if (globalKey.isEmpty) return;
appLogger.d('Background task status: ${update.status} for $globalKey');
switch (update.status) {
case TaskStatus.complete:
_onDownloadComplete(globalKey, update.task);
case TaskStatus.failed:
_onDownloadFailed(globalKey, update.exception?.description ?? 'Download failed');
case TaskStatus.notFound:
_onDownloadFailed(globalKey, 'File not found (404)');
case TaskStatus.canceled:
if (_pausingKeys.contains(globalKey)) {
// Expected cancel from holding-queue promotion during pause — ignore
try {
switch (update.status) {
case TaskStatus.complete:
_onDownloadComplete(globalKey, update.task);
case TaskStatus.failed:
_onDownloadFailed(globalKey, update.exception?.description ?? 'Download failed');
case TaskStatus.notFound:
_onDownloadFailed(globalKey, 'File not found (404)');
case TaskStatus.canceled:
if (_pausingKeys.contains(globalKey)) {
// Expected cancel from holding-queue promotion during pause — ignore
break;
}
final ctx = _pendingDownloadContext.remove(globalKey);
if (ctx != null) {
// Context still present → OS cancelled the task, not user code
// (user-initiated pause/cancel/delete removes context before cancellation completes)
appLogger.w('Download cancelled by system for $globalKey, re-queuing');
_database.updateBgTaskId(globalKey, null);
_transitionStatus(globalKey, DownloadStatus.queued);
_database.addToQueue(mediaGlobalKey: globalKey);
if (_lastClient != null) _processQueue(_lastClient!);
}
case TaskStatus.paused:
appLogger.d('Download paused by system for $globalKey');
case TaskStatus.waitingToRetry:
appLogger.d('Download waiting to retry for $globalKey');
case TaskStatus.enqueued:
case TaskStatus.running:
// If this item is being paused, the holding queue promoted it — cancel it
if (_pausingKeys.contains(globalKey)) {
FileDownloader().cancelTaskWithId(update.task.taskId);
}
break;
}
final ctx = _pendingDownloadContext.remove(globalKey);
if (ctx != null) {
// Context still present → OS cancelled the task, not user code
// (user-initiated pause/cancel/delete removes context before cancellation completes)
appLogger.w('Download cancelled by system for $globalKey, re-queuing');
_database.updateBgTaskId(globalKey, null);
_transitionStatus(globalKey, DownloadStatus.queued);
_database.addToQueue(mediaGlobalKey: globalKey);
if (_lastClient != null) _processQueue(_lastClient!);
}
case TaskStatus.paused:
appLogger.d('Download paused by system for $globalKey');
case TaskStatus.waitingToRetry:
appLogger.d('Download waiting to retry for $globalKey');
case TaskStatus.enqueued:
case TaskStatus.running:
// If this item is being paused, the holding queue promoted it — cancel it
if (_pausingKeys.contains(globalKey)) {
FileDownloader().cancelTaskWithId(update.task.taskId);
}
break;
}
} catch (e) {
appLogger.e('Error handling download status change for $globalKey', error: e);
}
}
@@ -1661,6 +1668,7 @@ class DownloadManagerService {
}
void dispose() {
_disposed = true;
for (final timer in _progressDebounceTimers.values) {
timer.cancel();
}
+1
View File
@@ -101,6 +101,7 @@ class SleepTimerService extends ChangeNotifier {
@override
void dispose() {
_timer?.cancel();
_completedController.close();
super.dispose();
}
}