Skip to content

Commit

Permalink
Modifies TaskProgressUpdate interval
Browse files Browse the repository at this point in the history
Now, if progress is available, a progress update will be sent at least once every 2.5 seconds, even if less than 2% progress has been made. This helps keep UI notifications alive for large and slow loading files.
  • Loading branch information
781flyingdutchman committed Jan 25, 2025
1 parent 1fc51aa commit ce3884b
Show file tree
Hide file tree
Showing 8 changed files with 63 additions and 35 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ If you want to monitor progress during the download itself (e.g. for a large fil
final result = await FileDownloader().download(task,
onProgress: (progress) => print('Progress update: $progress'));
```
Progress updates start with 0.0 when the actual download starts (which may be in the future, e.g. if waiting for a WiFi connection), and will be sent periodically, not more than twice per second per task. If a task completes successfully you will receive a final progress update with a `progress` value of 1.0 (`progressComplete`). Failed tasks generate `progress` of `progressFailed` (-1.0), canceled tasks `progressCanceled` (-2.0), notFound tasks `progressNotFound` (-3.0), waitingToRetry tasks `progressWaitingToRetry` (-4.0) and paused tasks `progressPaused` (-5.0).
Progress updates start with 0.0 when the actual download starts (which may be in the future, e.g. if waiting for a WiFi connection), and will be sent periodically, not more than twice per second per task, and not less than once every 2.5 seconds. If a task completes successfully you will receive a final progress update with a `progress` value of 1.0 (`progressComplete`). Failed tasks generate `progress` of `progressFailed` (-1.0), canceled tasks `progressCanceled` (-2.0), notFound tasks `progressNotFound` (-3.0), waitingToRetry tasks `progressWaitingToRetry` (-4.0) and paused tasks `progressPaused` (-5.0).

Use `await task.expectedFileSize()` to query the server for the size of the file you are about
to download. The expected file size is also included in `TaskProgressUpdate`s that are sent to
Expand Down Expand Up @@ -362,7 +362,7 @@ In summary, to track your tasks persistently, follow these steps in order, immed
3. Call `await FileDownloader().start()` to execute the following calls in the correct order (or call these manually):
a. Call `await FileDownloader().trackTasks()` if you want to track the tasks in a persistent database
b. Call `await FileDownloader().resumeFromBackground()` to ensure events that happened while your app was in the background are processed
c. If you are tracking tasks in the database, after ~5 seconds, call `await FileDownloader().rescheduleKilledTasks()` 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)
c. If you are tracking tasks in the database, after ~5 seconds, call `await FileDownloader().rescheduleKilledTasks()` to reschedule tasks that are in the database as `enqueued` or `running` yet are not enqueued or running on the native side, or that are `waitingToRetry` but not registered as such. 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 @@ -452,7 +452,7 @@ print('TaskId ${record.taskId} with task ${record.task} has '
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 killed 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 `rescheduleKilledTasks` 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, or call `FileDownloader().start()` with `doRescheduleKilledTasks` set to true (the default).
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 killed tasks, i.e. tasks that show up as `enqueued` or `running` in the database, yet are not enqueued or running on the native side, or are `waitingToRetry` but not registered as such. Method `rescheduleKilledTasks` 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, or call `FileDownloader().start()` with `doRescheduleKilledTasks` set to true (the default).

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`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -671,12 +671,14 @@ open class TaskWorker(
}

/**
* Returns true if [currentProgress] > [lastProgressUpdate] + threshold and
* [now] > [nextProgressUpdateTime]
* Returns true if [currentProgress] > [lastProgressUpdate] + 2% and
* [now] > [nextProgressUpdateTime], or if there was progress and
* [now] > [nextProgressUpdateTime] + 2 seconds
*/
open fun shouldSendProgressUpdate(currentProgress: Double, now: Long): Boolean {
return currentProgress - lastProgressUpdate > 0.02 &&
now > nextProgressUpdateTime
return (currentProgress - lastProgressUpdate > 0.02 &&
now > nextProgressUpdateTime) || (currentProgress > lastProgressUpdate &&
now > nextProgressUpdateTime + 2000)
}

/**
Expand Down
1 change: 1 addition & 0 deletions example/integration_test/downloader_integration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ void main() {
final subscription = FileDownloader().updates.listen((update) {
expect(update is TaskProgressUpdate, isTrue);
if (update is TaskProgressUpdate) {
print("ProgressUpdate ${update.progress} at ${DateTime.now()}");
progressCallback(update);
}
});
Expand Down
40 changes: 22 additions & 18 deletions example/macos/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
7A5FBE1A8A4574067AE99057 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 327E02FF0D7765B97772BC21 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -81,6 +82,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
7A5FBE1A8A4574067AE99057 /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -185,14 +187,16 @@
33CC10EB2044A3C60003C045 /* Resources */,
33CC110E2044A8840003C045 /* Bundle Framework */,
3399D490228B24CF009A79C7 /* ShellScript */,
50F2B87867DEB156BA98A61F /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
33CC11202044C79F0003C045 /* PBXTargetDependency */,
);
name = Runner;
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
productName = Runner;
productReference = 33CC10ED2044A3C60003C045 /* example.app */;
productType = "com.apple.product-type.application";
Expand Down Expand Up @@ -232,6 +236,9 @@
Base,
);
mainGroup = 33CC10E42044A3C60003C045;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
);
productRefGroup = 33CC10EE2044A3C60003C045 /* Products */;
projectDirPath = "";
projectRoot = "";
Expand Down Expand Up @@ -293,23 +300,6 @@
shellPath = /bin/sh;
shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire";
};
50F2B87867DEB156BA98A61F /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
BDB2605E80924098FA31483D /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
Expand Down Expand Up @@ -629,6 +619,20 @@
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */

/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 33CC10E52044A3C60003C045 /* Project object */;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "&quot;$FLUTTER_ROOT&quot;/packages/flutter_tools/bin/macos_assemble.sh prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "33CC10EC2044A3C60003C045"
BuildableName = "example.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,10 @@ func updateProgress(task: Task, totalBytesExpected: Int64, totalBytesDone: Int64
let info = BDPlugin.propertyLock.withLock({
return BDPlugin.progressInfo[task.taskId] ?? (lastProgressUpdateTime: 0.0, lastProgressValue: 0.0, lastTotalBytesDone: 0, lastNetworkSpeed: -1.0)
})
if totalBytesExpected != NSURLSessionTransferSizeUnknown && Date().timeIntervalSince1970 > info.lastProgressUpdateTime + 0.5 {
let now = Date().timeIntervalSince1970
if totalBytesExpected != NSURLSessionTransferSizeUnknown && now > info.lastProgressUpdateTime + 0.5 {
let progress = min(Double(totalBytesDone) / Double(totalBytesExpected), 0.999)
if progress - info.lastProgressValue > 0.02 {
if (progress - info.lastProgressValue > 0.02) || (progress > info.lastProgressValue && now > info.lastProgressUpdateTime + 2.5) {
// calculate network speed and time remaining
let now = Date().timeIntervalSince1970
let timeSinceLastUpdate = now - info.lastProgressUpdateTime
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,8 @@ public class UrlSessionDelegate : NSObject, URLSessionDelegate, URLSessionDownlo

/// Process taskdelegate progress update for download task
///
/// If the task requires progress updates, provide these at a reasonable interval
/// If this is the first update for this file, also emit a 'running' status update
/// If the task requires progress updates, provides these at a reasonable interval
/// If this is the first update for this file, also emits a 'running' status update
public func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
// task is var because the filename can be changed on the first 'didWriteData' call
guard var task = getTaskFrom(urlSessionTask: downloadTask) else { return }
Expand Down
14 changes: 8 additions & 6 deletions lib/src/desktop/isolate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -435,9 +435,11 @@ Future<String?> responseContent(http.StreamedResponse response) {
}
}

/// Returns true if [currentProgress] > [lastProgressUpdate] + threshold and
/// [now] > [nextProgressUpdateTime]
bool shouldSendProgressUpdate(double currentProgress, DateTime now) {
return currentProgress - lastProgressUpdate > 0.02 &&
now.isAfter(nextProgressUpdateTime);
}
/// Returns true if [currentProgress] > [lastProgressUpdate] + 2% and
/// [now] > [nextProgressUpdateTime], or if there was progress and
/// [now] > [nextProgressUpdateTime] + 2 seconds
bool shouldSendProgressUpdate(double currentProgress, DateTime now) =>
(currentProgress - lastProgressUpdate > 0.02 &&
now.isAfter(nextProgressUpdateTime)) ||
(currentProgress > lastProgressUpdate &&
now.isAfter(nextProgressUpdateTime.add(const Duration(seconds: 2))));

0 comments on commit ce3884b

Please sign in to comment.