Closed robert-virkus closed 5 months ago
If you cancel a task it will remain in the database with it's status canceled
until you remove it. Tasks don't get removed from the database until you call delete
, so if you want cancelled tasks removed you'll have to do that yourself after cancellation completes.
If you're saying that the task is not cancelled after you cancel it, then that's an issue but from your description I don't think that's what you're saying, right? The Kotlin error you're seeing is unavoidable but normal when you cancel a task.
Thanks so much for your quick reply and help! For some reason the task keeps ending up in the database either as failed or canceled even when I remove it manually from the database. I tried both first removing it and then cancelling it and the other way round.
final downloader = await FileDownloader().trackTasks();
final downloads = state.value ?? [];
final taskId = download._taskId;
/// ....
await downloader.database.deleteRecordWithId(taskId);
await downloader.cancelTaskWithId(taskId);
I have now worked around this by manually cleaning up failed and canceled records during start up.
Thanks for this great library and keep up the good work!
Thanks, a few thoughts:
resumeFromBackground
after you have started your listeners and after you have called trackTasks
. This is to flush the buffer of 'undelivered' updates that may have happened while your app was suspended, and process those. The order is 1) register listeners, 2) call trackTasks
and 3) call resumeFromBackground
await
the cancelTaskWithId
call, that does not mean the task is cancelled when the call returns. So, deleting that task from the database immediately before or after that call likely won't work, as the 'canceled' status update will arrive a bit later and put the task right back in the database, marked as 'canceled'. Perhaps that is what you are experiencing?database
as your app's database to store downloads, unless you build a proper PersistentStorage
implementation (as some have done successfully). The default database
is a simple database to keep track of the activities of the downloader plugin. So, if you wanted to for example show your users what files have been downloaded you should wrap the database
in an object with persistent storagte that you define yourself, and then it is easy to for example filter out 'canceled' tasks.This issue is stale because it has been open for 14 days with no activity.
This issue was closed because it has been inactive for 7 days since being marked as stale.
Describe the bug I try to cancel an ongoing download via cancelTaskWithId. A java.util.concurrent.CancellationException is thrown and afterwards the cancelled download is still listed when querying allRecords.
To Reproduce Steps to reproduce the behavior:
final downloader = await FileDownloader().trackTasks();
await FileDownloader().enqueue(task);
await downloader.cancelTaskWithId(download._taskId);
Expected behavior The
allRecords()
method should not return a cancelled task after is has been cancelled.Logs If possible, include logs that capture the issue:
Code
Here's the code I used to interact with background_downloader: ```dart /// Manages downloading and downloaded podcasts @Riverpod(keepAlive: true) class DownloadsNotifier extends _$DownloadsNotifier { @override Future> build() async { DownloadProgress toProgress(TaskRecord record) { final episode = PodcastEpisode.fromJson( jsonDecode(record.task.metaData), ); return DownloadProgress( episode: episode, progress: record.progress, localUrl: record.task.filename, taskId: record.taskId, ); } final downloader = await FileDownloader().trackTasks(); downloader.updates.listen(_updateTaskProgress); await downloader.ready; final allRecords = await downloader.database.allRecords(); final episodes = allRecords.map(toProgress).toList(); return episodes; } /// Enqueues the given episode for download Future enqueue(PodcastEpisode episode) async {
final task = DownloadTask(
url: episode.playbackUrl,
metaData: jsonEncode(episode.toJson()),
updates: Updates.statusAndProgress,
);
final episodes = state.value ?? [];
final result = await FileDownloader().enqueue(task);
state = AsyncData(
[
DownloadProgress(
episode: episode,
progress: 0,
localUrl: task.filename,
taskId: task.taskId,
),
...episodes,
],
);
return result;
}
/// Retrieves the download progress for the given episode
DownloadProgress? getDownloadProgress(PodcastEpisode episode) =>
state.value?.firstWhereOrNull(
(element) => element.episode.playbackUrl == episode.playbackUrl,
);
/// Checks if the episode is currently being downloaded or downloaded
bool isDownloading(PodcastEpisode episode) =>
state.value?.any(
(element) => element.episode.playbackUrl == episode.playbackUrl) ??
false;
/// Removes the download for the given episode
Future removeDownload(DownloadProgress download) async {
final downloader = await FileDownloader().trackTasks();
final downloads = state.value ?? [];
state = AsyncData(
downloads.where((e) => e._taskId != download._taskId).toList(),
);
download.updateProgress(0);
await downloader.database.deleteRecordWithId(download._taskId);
}
/// Cancels the download for the given episode
Future cancelDownload(DownloadProgress download) async {
final downloader = await FileDownloader().trackTasks();
final downloads = state.value ?? [];
state = AsyncData(
downloads.where((e) => e._taskId != download._taskId).toList(),
);
download.updateProgress(0);
await downloader.cancelTaskWithId(download._taskId);
}
void _updateTaskProgress(TaskUpdate event) {
final downloads = state.value ?? [];
final download = downloads.firstWhereOrNull(
(element) => element._taskId == event.task.taskId,
);
print(
'Task update: $event with download: ${download?.episode.playbackUrl}');
if (download != null) {
switch (event) {
case TaskStatusUpdate():
print('status: ${event.status}');
switch (event.status) {
case TaskStatus.complete:
download.updateProgress(1.0);
break;
case TaskStatus.failed:
download.updateProgress(0.0);
break;
case TaskStatus.enqueued:
download.updateProgress(0.0);
break;
case TaskStatus.running:
download.updateProgress(0.0);
break;
case TaskStatus.notFound:
download.updateProgress(0.0);
break;
case TaskStatus.canceled:
download.updateProgress(0.0);
break;
case TaskStatus.waitingToRetry:
// TODO: Handle this case.
break;
case TaskStatus.paused:
// TODO: Handle this case.
break;
}
break;
case TaskProgressUpdate():
print('progress: ${event.progress}');
download.updateProgress(event.progress);
break;
}
}
}
}
/// The download progress for a given podcast episode
class DownloadProgress extends ChangeNotifier {
/// Creates a new [DownloadProgress]
DownloadProgress({
required this.episode,
required double progress,
required this.localUrl,
required String taskId,
}) : _progress = progress,
_taskId = taskId;
final String _taskId;
/// The episode being downloaded
final PodcastEpisode episode;
double _progress;
/// The progress of the download
double get progress => _progress;
/// The local URL of the downloaded file
final String localUrl;
/// Whether the download is completed
bool get isDownloaded => progress >= 1.0;
/// Retrieves the current playback url of the episode
String get url => isDownloaded ? localUrl : episode.playbackUrl;
/// Updates the download progress
void updateProgress(double progress) {
_progress = progress;
notifyListeners();
}
}
```
Additional context Android simulator, with Flutter 3.19.5 and background_downloader 8.4.3