fix(downloads): recover queue ownership after setup failures

This commit is contained in:
zarzet committed 2026-10-06 21:59:55 +07:00
1 parent 8f021b01c7
commit 4489136d38
2 files changed
+121 -1

No files matched your search

+30 -1
View File
@@ -426,8 +426,14 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
Future.microtask(() async {
try {
updateSettings(ref.read(settingsProvider));
await _initOutputDir();
try {
await _initOutputDir();
} catch (error, stack) {
_log.e('Failed to initialize download folder: $error', error, stack);
}
await _loadQueueFromStorage();
} catch (error, stack) {
_log.e('Failed to initialize download queue: $error', error, stack);
} finally {
if (!_queueRestored.isCompleted) _queueRestored.complete();
}
@@ -1498,6 +1504,29 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
try {
if (state.isProcessing) return;
await _processQueueSingleFlight();
} catch (error, stack) {
_log.e('Download queue processing failed: $error', error, stack);
_stopProgressPolling();
_metadataEmbedding.clearCoverCache();
_stopConnectivityMonitoring();
if (ref.mounted) {
state = state.copyWith(isProcessing: false, currentDownload: null);
}
// Setup and finalization can fail outside the per-item pipeline. The
// queue owner still releases the execution window before another run.
try {
if (Platform.isAndroid) {
await PlatformBridge.stopDownloadService();
} else if (Platform.isIOS) {
await PlatformBridge.endBackgroundDownloadTask();
}
} catch (cleanupError, cleanupStack) {
_log.e(
'Failed to release download execution window: $cleanupError',
cleanupError,
cleanupStack,
);
}
} finally {
if (_queueProcessingGate.leave()) {
Future.microtask(_processQueue);
+91
View File
@@ -0,0 +1,91 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:spotiflac_android/models/download_item.dart';
import 'package:spotiflac_android/models/settings.dart';
import 'package:spotiflac_android/models/track.dart';
import 'package:spotiflac_android/providers/download_queue_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
class _Settings extends SettingsNotifier {
@override
AppSettings build() => const AppSettings();
}
class _Queue extends DownloadQueueNotifier {
@override
DownloadQueueState build() => const DownloadQueueState().copyWith(
items: [
DownloadItem(
id: 'queued',
track: const Track(
id: 'example:track',
name: 'Song',
artistName: 'Artist',
albumName: 'Album',
duration: 180,
),
service: 'example',
createdAt: DateTime.utc(2026),
),
],
);
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
const paths = MethodChannel('plugins.flutter.io/path_provider');
const connectivity = MethodChannel(
'dev.fluttercommunity.plus/connectivity_status',
);
tearDown(() {
messenger.setMockMethodCallHandler(paths, null);
messenger.setMockMethodCallHandler(connectivity, null);
});
test('a failed queue setup releases processing and permits retry', () async {
var pathAttempts = 0;
messenger.setMockMethodCallHandler(paths, (_) async {
pathAttempts++;
throw PlatformException(code: 'folder_unavailable');
});
messenger.setMockMethodCallHandler(connectivity, (_) async => null);
final queue = _Queue();
final container = ProviderContainer(
overrides: [
downloadQueueProvider.overrideWith(() => queue),
settingsProvider.overrideWith(_Settings.new),
],
);
addTearDown(container.dispose);
var started = false;
var finished = Completer<void>();
container.listen(downloadQueueProvider, (previous, next) {
if (next.isProcessing) started = true;
if (started && !next.isProcessing && !finished.isCompleted) {
finished.complete();
}
});
queue.resumePendingDownloadsOnForeground();
await finished.future;
final state = container.read(downloadQueueProvider);
expect(state.isProcessing, isFalse);
expect(state.currentDownload, isNull);
expect(state.items.single.status, DownloadStatus.queued);
expect(pathAttempts, greaterThan(0));
final firstAttempts = pathAttempts;
started = false;
finished = Completer<void>();
queue.resumePendingDownloadsOnForeground();
await finished.future;
expect(pathAttempts, greaterThan(firstAttempts));
expect(container.read(downloadQueueProvider).isProcessing, isFalse);
});
}