781flyingdutchman / background_downloader

Flutter plugin for file downloads and uploads
Other
160 stars 76 forks source link

Android: java.util.concurrent.CancellationException when calling cancelTaskWithId (background_downloader 8.4.3) #289

Closed robert-virkus closed 5 months ago

robert-virkus commented 6 months ago

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:

  1. Track tasks, e.g. final downloader = await FileDownloader().trackTasks();
  2. Enqueue a download, e.g. await FileDownloader().enqueue(task);
  3. Cancel the task after a while, e.g. await downloader.cancelTaskWithId(download._taskId);
  4. See error

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:

V/BackgroundDownloader(14291): Canceling taskIds [4243623334]
I/flutter (14291): Task update: TaskProgressUpdate{progress: -2.0, expectedFileSize: -1, networkSpeed: -1.0, timeRemaining: -0:00:01.000000} with download: null
I/WM-WorkerWrapper(14291): Work [ id=6b027e2a-8cbc-47c5-9776-dc11ec0b4b4c, tags={ com.bbflight.background_downloader.DownloadTaskWorker, BackgroundDownloader, taskId=4243623334, group=default } ] was cancelled
I/WM-WorkerWrapper(14291): java.util.concurrent.CancellationException: Task was cancelled.
I/WM-WorkerWrapper(14291):  at androidx.work.impl.utils.futures.AbstractFuture.cancellationExceptionWithCause(AbstractFuture.java:1183)
I/WM-WorkerWrapper(14291):  at androidx.work.impl.utils.futures.AbstractFuture.getDoneValue(AbstractFuture.java:513)
I/WM-WorkerWrapper(14291):  at androidx.work.impl.utils.futures.AbstractFuture.get(AbstractFuture.java:474)
I/WM-WorkerWrapper(14291):  at androidx.work.impl.WorkerWrapper$2.run(WorkerWrapper.java:316)
I/WM-WorkerWrapper(14291):  at androidx.work.impl.utils.SerialExecutorImpl$Task.run(SerialExecutorImpl.java:96)
I/WM-WorkerWrapper(14291):  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1137)
I/WM-WorkerWrapper(14291):  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:637)
I/WM-WorkerWrapper(14291):  at java.lang.Thread.run(Thread.java:1012)
I/TaskWorker(14291): Exception for taskId 4243623334: kotlinx.coroutines.JobCancellationException: Job was cancelled; job=JobImpl{Cancelling}@882a7ed
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

781flyingdutchman commented 6 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.

robert-virkus commented 6 months ago

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!

781flyingdutchman commented 6 months ago

Thanks, a few thoughts:

github-actions[bot] commented 6 months ago

This issue is stale because it has been open for 14 days with no activity.

github-actions[bot] commented 5 months ago

This issue was closed because it has been inactive for 7 days since being marked as stale.