Skip to content

Commit

Permalink
Added rescheduleMissingTasks
Browse files Browse the repository at this point in the history
Reschedules tasks that are present in the database but missing in the downloader. The assumption is that they were lost when the app was killed

Refined dependency declarations in pubspec.yaml
  • Loading branch information
781flyingdutchman committed Jan 16, 2025
1 parent 6a6f1ea commit ed73407
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 10 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## next
* Upgraded minimum Dart SDK to 3.3.0
## 8.9.2
* Upgraded minimum Dart SDK to 3.5.0 / Flutter SDK 3.24.0 to stay in sync with dependency updates
*

## 8.9.1
* [iOS] Adds Privacy Manifest
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,8 +357,9 @@ To ensure your callbacks or listener capture events that may have happened when
In summary, to track your tasks persistently, follow these steps in order, immediately after app startup:
1. If using a non-default `PersistentStorage` backend, initialize with `FileDownloader(persistentStorage: MyPersistentStorage())` and wait for the initialization to complete by calling `await FileDownloader().ready` (see [using the database](#using-the-database-to-track-tasks) for details on `PersistentStorage`).
2. Register an event listener or callback(s) to process status and progress updates
3. call `await FileDownloader().trackTasks()` if you want to track the tasks in a persistent database
4. call `await FileDownloader().resumeFromBackground()` to ensure events that happened while your app was in the background are processed
3. Call `await FileDownloader().trackTasks()` if you want to track the tasks in a persistent database
4. Call `await FileDownloader().resumeFromBackground()` to ensure events that happened while your app was in the background are processed
5. If you are tracking tasks in the database, after ~5 seconds, call `await FileDownloader().rescheduleMissingTasks()` to reschedule tasks that are in the database as `enqueued` or `running` yet are not enqueued or running on the native side. These tasks have been "lost", most likely because the user killed your app (which kills tasks on the native side without warning)

The rest of this section details [event listeners](#using-an-event-listener), [callbacks](#using-callbacks) and the [database](#using-the-database-to-track-tasks) in detail.

Expand Down Expand Up @@ -440,14 +441,16 @@ final successfullyEnqueued = await FileDownloader().enqueue(task);
// somewhere else: query the task status by getting a `TaskRecord`
// from the database
final record = await FileDownloader().database.recordForId(task.taskId);
print('Taskid ${record.taskId} with task ${record.task} has '
print('TaskId ${record.taskId} with task ${record.task} has '
'status ${record.status} and progress ${record.progress} '
'with an expected file size of ${record.expectedFileSize} bytes'
```

You can interact with the `database` using `allRecords`, `allRecordsOlderThan`, `recordForId`,`deleteAllRecords`,
`deleteRecordWithId` etc. If you only want to track tasks in a specific [group](#grouping-tasks), call `trackTasksInGroup` instead.

If a user kills your app (e.g. by swiping it away in the app tray) then tasks that are running (natively) are killed, and no indication is given to your application. This cannot be avoided. To guard for this, upon app startup you can ask the downloader to reschedule missing tasks, i.e. tasks that show up as `enqueued` or `running` in the database, yet are not enqueued or running on the native side. Method `rescheduleMissingTasks` returns a record with two lists, 1) successfully rescheduled tasks and 2) tasks that failed to reschedule. Together, those are the missing tasks. Reschedule missing tasks a few seconds after you have called `resumeFromBackground`, as that gives the downloader time to processes updates that may have happened while the app was suspended.

By default, the downloader uses a modified version of the [localstore](https://pub.dev/packages/localstore) package to store the `TaskRecord` and other objects. To use a different persistent storage solution, create a class that implements the [PersistentStorage](https://pub.dev/documentation/background_downloader/latest/background_downloader/PersistentStorage-class.html) interface, and initialize the downloader by calling `FileDownloader(persistentStorage: MyPersistentStorage())` as the first use of the `FileDownloader`.

As an alternative to LocalStore, use `SqlitePersistentStorage`, included in [background_downloader_sql](https://pub.dev/packages/background_downloader_sql), which supports SQLite storage and migration from Flutter Downloader.
Expand Down
28 changes: 28 additions & 0 deletions example/integration_test/database_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,32 @@ void main() {
final r2 = await database.recordForId(record2.taskId);
expect(r2, equals(record2));
});

test('rescheduleMissingTasks', () async {
expect(await FileDownloader().allTasks(), isEmpty);
// without task tracking activated, throws assertionError
expect(() async => await FileDownloader().rescheduleMissingTasks(),
throwsAssertionError);
await FileDownloader().trackTasks();
// test empty
final result = await FileDownloader().rescheduleMissingTasks();
expect(result.$1, isEmpty);
expect(result.$2, isEmpty);
// add a record to the database that is not enqueued
await FileDownloader().database.updateRecord(record);
final result2 = await FileDownloader().rescheduleMissingTasks();
expect(result2.$1.length, equals(1));
expect(result2.$2, isEmpty);
expect(result2.$1.first.taskId, equals(task.taskId));
final allTasks = await FileDownloader().allTasks();
expect(allTasks.first.taskId, equals(task.taskId));
await Future.delayed(const Duration(seconds: 2));
expect(await FileDownloader().allTasks(), isEmpty);
// add a record to the database that is also enqueued
expect(await FileDownloader().enqueue(task2), isTrue);
expect(await FileDownloader().database.allRecords(), isNotEmpty);
final result3 = await FileDownloader().rescheduleMissingTasks();
expect(result3.$1, isEmpty);
expect(result3.$2, isEmpty);
});
}
3 changes: 2 additions & 1 deletion example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ version: 2.0.0
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

environment:
sdk: ^3.0.0
sdk: ^3.5.0
flutter: '>=3.24.0'

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
Expand Down
3 changes: 3 additions & 0 deletions lib/src/base_downloader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -516,6 +516,9 @@ abstract base class BaseDownloader {
}
}

/// Return true if we are tracking tasks
bool get isTrackingTasks => trackedGroups.isNotEmpty;

/// Attempt to pause this [task]
///
/// Returns true if successful
Expand Down
46 changes: 46 additions & 0 deletions lib/src/file_downloader.dart
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,52 @@ interface class FileDownloader {
Future<void> resumeFromBackground() =>
_downloader.retrieveLocallyStoredData();

/// Reschedules tasks that are present in the database but missing from
/// the native task queue. Typically called on app start, 5s after establishing
/// the updates listener, calling [trackTasks] or [trackTasksInGroup] and
/// calling [resumeFromBackground].
///
/// This function retrieves all tasks from the database that are in enqueued
/// or running states and compares these tasks with the
/// list of tasks currently present in the native task queue.
///
/// For each task found only in the database, the function:
/// 1. Deletes the corresponding record from the database.
/// 2. Enqueues the task back into the native task queue.
///
/// Finally, the function returns two lists of Tasks in a record. The first
/// item is the list of successfully re-enqueued tasks, the second item
/// is the list of tasks that failed to enqueue.
///
/// Throws assertion error if you are not currently tracking tasks, as that
/// makes this function a no-op that always returns empty lists.
Future<(List<Task>, List<Task>)> rescheduleMissingTasks() async {
assert(
_downloader.isTrackingTasks,
'rescheduleMissingTasks should only be called if you are tracking tasks. '
'Did you call trackTasks or trackTasksInGroup?');
final databaseTasks = (await database.allRecords())
.where((record) => const [
TaskStatus.enqueued,
TaskStatus.running,
].contains(record.status))
.map((record) => record.task)
.toSet();
final nativeTasks = Set<Task>.from(await FileDownloader().allTasks());
final missingTasks = databaseTasks.difference(nativeTasks);
final successfullyEnqueued = <Task>[];
final failedToEnqueue = <Task>[];
for (final task in missingTasks) {
await database.deleteRecordWithId(task.taskId);
if (await FileDownloader().enqueue(task)) {
successfullyEnqueued.add(task);
} else {
failedToEnqueue.add(task);
}
}
return (successfullyEnqueued, failedToEnqueue);
}

/// Returns true if task can be resumed on pause
///
/// This future only completes once the task is running and has received
Expand Down
8 changes: 4 additions & 4 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
name: background_downloader
description: A multi-platform background file downloader and uploader. Define the task, enqueue and monitor progress

version: 8.9.1
version: 8.9.2
repository: https://github.com/781flyingdutchman/background_downloader

environment:
sdk: ^3.3.0
flutter: '>=3.0.0'
sdk: ^3.5.0
flutter: '>=3.24.0'

dependencies:
flutter:
Expand All @@ -22,7 +22,7 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter_lints: '>4.0.0 <6.0.0'
mockito: ^5.0.17
build_runner: ^2.4.13

Expand Down

0 comments on commit ed73407

Please sign in to comment.