mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-21 15:02:21 +02:00
fix(downloads): recover queue ownership after setup failures
This commit is contained in:
1 parent
8f021b01c7
commit
4489136d38
2 files changed
+121
-1
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user