diff --git a/CHANGELOG.md b/CHANGELOG.md
index 39e2195f..8e37d412 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.0.0 - 26-04-2020
+
+* Completely redesign the API to be more intuitive.
+
## 1.5.0 - 09.08.2020
* Update `pubspec` to new format
@@ -5,127 +9,127 @@
## 1.4.4 - 18.04.2020
-* add `debug` (optional) parameter in `initialize()` method that supports disable logging to console
+* Add `debug` (optional) parameter in `initialize()` method that supports disabling logging to console.
## 1.4.3 - 09.04.2020
-* iOS: fix bug on `remove` method
+* iOS: Fix bug in `remove` method.
## 1.4.2 - 02.04.2020
-* add `timeCreated` in `DownloadTask` model
-* iOS: fix bug MissingPluginException
+* Add `timeCreated` in `DownloadTask` model.
+* iOS: Fix: `MissingPluginException`
## 1.4.1 - 30.01.2020
-* Android: fix bug `ensureInitializationComplete must be called after startInitialization`
-* clarify integration documents
+* Android: fix bug `ensureInitializationComplete must be called after startInitialization`.
+* Clarify integration documents.
## 1.4.0 - 12.01.2020
-* migrate to Android v2 embedding.
+* Migrate to Android v2 embedding.
## 1.3.4 - 21.12.2019
-* fix bug stuck in Flutter v12.13
-* fix bug on casting int to long value
+* Fix: No longer stuck in Flutter v12.13.
+* Fix bug when casting int to long value.
## 1.3.3 - 03.11.2019
-* update document
-* assert and make sure FlutterDownloader initialized one time.
+* Update document.
+* Assert and make sure `FlutterDownloader` is initialized one time.
## 1.3.2 - 24.10.2019
-* correct document and example codes about communication with background isolate
+* Correct document and example codes about communication with background isolate.
## 1.3.1 - 18.09.2019
-* assert the initialization of FlutterDownloader
+* `assert` the initialization of `FlutterDownloader`.
## 1.3.0 - 16.09.2019
-* **BREAKING CHANGES**: the plugin has been refactored to support update download events with background isolate. In order to support background execution in Dart, the `callback`, that receives events from platform codes, now must be a static or top-level function. There's also an additional native configuration required on both of iOS and Android. See README for detail.
-* Android: upgrade `WorkManager` to v2.2.0.
-* Fix bug `SecurityException` when saving image/videos to internal storage in Android
-* Fix bug cannot save videos in Android.
+* **BREAKING CHANGES**: The plugin has been refactored to support update download events with background isolate. In order to support background execution in Dart, the `callback` that receives events from platform codes, now must be a static or top-level function. There's also an additional native configuration required on both iOS and Android. See the readme for more details.
+* Android: Upgrade `WorkManager` to v2.2.0.
+* Android: Fix: `SecurityException` no longer occurs when saving image/videos to internal storage.
+* Android: Fix: Videos can now be saved.
## 1.2.2 - 19.09.2019
-* Android: fix bugs
+* Android: Fix bugs.
## 1.2.1 - 27.08.2019
-* Android: hot-fix unregister `BroadcastReceiver` in case using `FlutterFragmentActivity`
+* Android: Hot-fix unregister `BroadcastReceiver` in case of using `FlutterFragmentActivity`.
## 1.2.0 - 27.08.2019
-* Android: support `FlutterFragmentActivity`, fix bug downloaded image/video files not shown in Gallery application, improved HTTP redirection implementation, fix bug cannot open apk file in some cases
+* Android: Support `FlutterFragmentActivity`, fix bug where the downloaded image/video files were not shown in the gallery. Improved HTTP redirection implementation, fix the bug "cannot open apk file" in some cases.
## 1.1.9 - 18.07.2019
-* Android: support HTTP redirection
-* iOS: correct getting file name from HTTP response
+* Android: Support HTTP redirection.
+* iOS: Fix getting the file name from the HTTP response.
## 1.1.8 - 16.07.2019
-* Fix bug in iOS: from iOS 8, absolute path to app's sandbox changes every time you relaunch the app, hence `savedDir` path is needed to truncate the changing part before saving it to DB and recreate the absolute path every time it loaded from DB. Currently, the plugin only supports save files in `NSDocumentDirectory`.
-* Fix bug is iOS: setting wrong status of task in case that the application is terminated
-* Android: upgrade dependencies
+* Fix bug on iOS: Since iOS 8, the absolute path to the app's sandbox changes every time you relaunch the app, hence `savedDir` path is needed to truncate the changing part before saving it to DB and recreate the absolute path every time it loaded from DB. Currently, the plugin only supports save files in `NSDocumentDirectory`.
+* iOS: Set correct status of task in case the application is terminated.
+* Android: Upgrade dependencies.
## 1.1.7 - 24.03.2019
-* Android: upgrade `WorkManager` to version 2.0.0 (AndroidX)
+* Android: Upgrade `WorkManager` to version 2.0.0 (AndroidX).
## 1.1.6 - 09.02.2019
-* Android: upgrade `WorkManager` to version 1.0.0-beta05
-* Android: migrate to AndroidX
+* Android: Upgrade `WorkManager` to version 1.0.0-beta05.
+* Android: Migrate to AndroidX.
## 1.1.5 - 27.01.2019
-* Android: upgrade `WorkManager` to version 1.0.0-beta03
-* fix bugs
+* Android: Upgrade `WorkManager` to version 1.0.0-beta03.
+* Fix several minor bugs.
## 1.1.4 - 06.01.2019
-* add `remove()` feature to delete task (in DB) and downloaded file as well (optional).
-* support clean up callback by setting callback as `null` in `registerCallback()`
-* Android: upgrade `WorkManager` to version 1.0.0-beta01
+* Add `remove()` feature to delete task (in DB) and downloaded file as well (optional).
+* Support clean up callback by setting callback as `null` in `registerCallback()`.
+* Android: Upgrade `WorkManager` to version 1.0.0-beta01.
## 1.1.3 - 18.11.2018
-* Android: fix bug NullPointerException of `saveFilePath`
+* Android: Fix bug `NullPointerException` of `saveFilePath`.
## 1.1.2 - 14.11.2018
-* Android: fix typo error
-* iOS: catch HTTP status code in case of error
+* Android: Fix typo error.
+* iOS: Catch HTTP status code in case of error.
## 1.1.1 - 12.11.2018
-* correct `README` instruction
+* Correct readme instructions.
## 1.1.0 - 12.11.2018
* Android: upgrade `WorkManager` library to version v1.0.0-alpha11
-* **BREAKING CHANGE**: `initialize()` is removed (to deal with the change of the initialization of `WorkManager` in v1.0.0-alpha11). The plugin initializes itself with default configurations. If you would like to change the default configuration, you can follows the instruction in `README.md`
+* **BREAKING CHANGE**: Removed `initialize()` to deal with the change of the initialization of `WorkManager` in v1.0.0-alpha11. The plugin initializes itself with default configurations. If you would like to change the default configuration, you can follows the instruction in the readme.
## 1.0.6 - 28.10.2018
-* fix bug related to `filename`
+* Fix bug related to `filename`.
## 1.0.5 - 22.10.2018
-* Android: re-config dependencies
+* Android: Reconfigure dependencies.
## 1.0.4 - 20.10.2018
-* Android: upgrade WorkManager to v1.0.0-alpha10
+* Android: Upgrade WorkManager to v1.0.0-alpha10.
## 1.0.3 - 29.09.2018
-* Android: upgrade compile sdk version to 28
+* Android: Upgrade compile sdk version to 28.
## 1.0.2 - 20.09.2018
@@ -137,58 +141,58 @@
## 1.0.0 - 09.09.2018
-* **NEW** features: initialize, loadTasksWithRawQuery, pause, resume, retry, open
-* **IMPORTANT**: the plugin must be initialized by `initialize()` at first
-* **BREAKING CHANGE**: `clickToOpenDownloadedFile` now renames to `openFileFromNotification` (to prevent confusing from `open` feature). Static property `maximumConcurrentTask` has been removed, this configuration now moves into `initialize()` method.
-* full support SQLite on both Android and iOS side, the plugin now itself manages its states persistently and exposes `loadTasksWithRawQuery` api that helps developers to load tasks from SQLite database with customized conditions
-* support localizing Android notification messages with `messages` parameter of `initialize()` method
-* full support opening and previewing downloaded file with `open()` method
-* (iOS integration) no need to override `application:handleEventsForBackgroundURLSession:completionHandler:` manually anymore, the plugin now itself takes responsibility for handling it
+* Add `initialize`, `loadTasksWithRawQuery`, `pause`, `resume`, `retry`, `open`.
+* **IMPORTANT:** The plugin has to be initialized by calling `initialize()` at first.
+* **BREAKING CHANGE:** Rename `clickToOpenDownloadedFile` to `openFileFromNotification` (to prevent confusing from `open` feature). Static property `maximumConcurrentTask` has been removed, this configuration now moves into `initialize()` method.
+* Full support of SQLite on both Android and iOS side, the plugin now itself manages its states persistently and exposes a `loadTasksWithRawQuery` API that helps developers to load tasks from SQLite database with customized conditions.
+* Support localizing Android notification messages with `messages` parameter of `initialize()` method.
+* Full support for opening and previewing downloaded file with `open()` method.
+* On iOS, there's no need to override `application:handleEventsForBackgroundURLSession:completionHandler:` manually anymore, the plugin takes responsibility for that itself.
## 0.1.1 - 29.08.2018
-* fix bugs: SQLite leak
-* new feature: support configuration of the maximum of concurrent download tasks
-* upgrade WorkManager to v1.0.0-alpha08
+* Fix SQLite leak.
+* Support configuration of the maximum number of concurrent download tasks.
+* Upgrade WorkManager to v1.0.0-alpha08.
## 0.1.0 - 12.08.2018
-* add: handle click on notification to open downloaded file (for Android)
+* Handle click on notification to open downloaded file (for Android).
## 0.0.9 - 10.08.2018
-* re-config to support Dart2
+* Re-configure to support Dart 2.
## 0.0.8 - 10.08.2018
-* upgrade WorkManager to v1.0.0-alpha06
-* fix bug: disable sound on notifications
+* Upgrade WorkManager to v1.0.0-alpha06.
+* Fix: Disable notification sound.
## 0.0.7 - 28.06.2018
-* upgrade WorkManager to v1.0.0-alpha04
+* Upgrade WorkManager to v1.0.0-alpha04.
## 0.0.6 - 28.06.2018
-* upgrade WorkManager to v1.0.0-alpha03
-* change default value of `showNotification` to `true` (it makes sense on Android 8.0 and above, it helps our tasks not to be killed by system when the app goes to background)
+* Upgrade WorkManager to v1.0.0-alpha03.
+* Change default value of `showNotification` to `true` (it makes sense on Android 8.0 and above as it helps our tasks not to be killed by system when the app goes to background).
## 0.0.5 - 22.06.2018
-* update metadata
+* Update package metadata.
## 0.0.4 - 15.06.2018
-* fix bug: Worker finished with FAILURE on Android API 26 and above
+* Fix: Worker finished with FAILURE on Android API 26 and above.
## 0.0.3 - 11.06.2018
-* support HTTP headers
+* Support providing custom HTTP headers.
## 0.0.2 - 08.06.2018
-* correct README document
+* Correct readme.
## 0.0.1 - 07.06.2018
-* initial release.
+* Initial release.
diff --git a/README.md b/README.md
index 77809093..5037fc3f 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,16 @@
[![Flutter Community: flutter_downloader](https://fluttercommunity.dev/_github/header/flutter_downloader)](https://github.com/fluttercommunity/community)
-# Flutter Downloader
-
[![pub package](https://img.shields.io/pub/v/flutter_downloader.svg)](https://pub.dartlang.org/packages/flutter_downloader)
A plugin for creating and managing download tasks. Supports iOS and Android.
-This plugin is based on [`WorkManager`][1] in Android and [`NSURLSessionDownloadTask`][2] in iOS to run download task in background mode.
+This plugin is based on [`WorkManager`][1] in Android and [`NSURLSessionDownloadTask`][2] in iOS to run download tasks in the background.
+## Setup
+
-## iOS integration
+iOS
### Required configuration:
@@ -20,18 +20,19 @@ This plugin is based on [`WorkManager`][1] in Android and [`NSURLSessionDownload
-* Add `sqlite` library.
+* Add sqlite library.
-
+
-
+
* Configure `AppDelegate`:
Objective-C:
+
```objective-c
/// AppDelegate.h
#import
@@ -65,10 +66,10 @@ void registerPlugins(NSObject* registry) {
}
@end
-
```
Or Swift:
+
```swift
import UIKit
import Flutter
@@ -91,16 +92,15 @@ private func registerPlugins(registry: FlutterPluginRegistry) {
FlutterDownloaderPlugin.register(with: registry.registrar(forPlugin: "FlutterDownloaderPlugin"))
}
}
-
```
### Optional configuration:
-* **Support HTTP request:** if you want to download file with HTTP request, you need to disable Apple Transport Security (ATS) feature. There're two options:
+* **Support HTTP request:** If you want to download file via HTTP request, you need to disable Apple Transport Security (ATS). There are two options:
-1. Disable ATS for a specific domain only: (add following codes to your `Info.plist` file)
+1. **Disable ATS for a specific domain only:** Add following codes to your `Info.plist` file.
-````xml
+```xml
NSAppTransportSecurity
NSExceptionDomains
@@ -121,33 +121,37 @@ private func registerPlugins(registry: FlutterPluginRegistry) {
````
-2. Completely disable ATS: (add following codes to your `Info.plist` file)
+2. **Completely disable ATS:** Add following codes to your `Info.plist` file.
-````xml
+```xml
NSAppTransportSecurity
NSAllowsArbitraryLoads
-````
+```
-* **Configure maximum number of concurrent tasks:** the plugin allows 3 download tasks running at a moment by default (if you enqueue more than 3 tasks, there're only 3 tasks running, other tasks are put in pending state). You can change this number by adding following codes to your `Info.plist` file.
+* **Configure maximum number of concurrent tasks:** the plugin allows 3 `DownloadTask`s running simultaneously by default. If you enqueue more than 3 tasks, there're only 3 tasks running, other tasks are put in a pending state. You can change this number by adding the following code to your `Info.plist` file:
-````xml
+```xml
FDMaximumConcurrentTasks
5
-````
+```
-* **Localize notification messages:** the plugin will send a notification message to notify user in case all files are downloaded while your application is not running in foreground. This message is English by default. You can localize this message by adding and localizing following message in `Info.plist` file. (you can find the detail of `Info.plist` localization in this [link][3])
+* **Localize notification messages:** The plugin will send a notification message to notify the user in case all files are downloaded while your application is not running in foreground. This message is in English by default. You can localize this message by adding and localizing the following message in `Info.plist` file. You can find the detail of `Info.plist` localization in this [link][3].
-````xml
+```xml
FDAllFilesDownloadedMessage
All files have been downloaded
-````
+```
-**Note:**
- - This plugin only supports save files in `NSDocumentDirectory`
+**Note:** This plugin only supports saving files in `NSDocumentDirectory`.
+
+
+
+
+Android
## Android integration
@@ -155,11 +159,11 @@ private func registerPlugins(registry: FlutterPluginRegistry) {
* If your project is running on Flutter versions prior v1.12, have a look at [this document](android_integration_note.md) to configure your Android project.
-* From Flutter v1.12 with Android v2 embedding there's no additional configurations required to work with background isolation in Android (but you need to setup your project properly. See [upgrading pre 1.12 Android projects](https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects))
+* From Flutter v1.12 with Android v2 embedding there's no additional configurations required to work with background isolation in Android, but you need to setup your project properly. See [upgrading pre 1.12 Android projects](https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects).
-* In order to handle click action on notification to open the downloaded file on Android, you need to add some additional configurations. Add the following codes to your `AndroidManifest.xml`:
+* In order to handle notification clicks to open the downloaded file, you need to add some additional configurations. Add the following codes to your `AndroidManifest.xml`:
-````xml
+```xml
-````
+```
**Note:**
- - You have to save your downloaded files in external storage (where the other applications have permission to read your files)
- - The downloaded files are only able to be opened if your device has at least an application that can read these file types (mp3, pdf, etc)
-
+- You have to save your downloaded files in external storage, where the other applications have permission to read your files.
+- The downloaded files can only be opened if your device has at least one application that can read these file types (mp3, pdf, etc).
+
### Optional configuration:
-* **Configure maximum number of concurrent tasks:** the plugin depends on `WorkManager` library and `WorkManager` depends on the number of available processor to configure the maximum number of tasks running at a moment. You can setup a fixed number for this configuration by adding following codes to your `AndroidManifest.xml`:
+* **Configure maximum number of concurrent tasks:** The plugin depends on `WorkManager` library and `WorkManager` depends on the number of available processor to configure the maximum number of tasks running at a moment. You can setup a fixed number for this configuration by adding the following code to your `AndroidManifest.xml`:
-````xml
+```xml
-
-
-
- ````
-
-* **Localize notification messages:** you can localize notification messages of download progress by localizing following messages. (you can find the detail of string localization in Android in this [link][4])
-
-````xml
+ android:name="vn.hunghd.flutterdownloader.FlutterDownloaderInitializer"
+ android:authorities="${applicationId}.flutter-downloader-init"
+ android:exported="false">
+
+
+
+```
+
+* **Localize notification messages:** you can localize notification messages of download progress by localizing following messages. You can find more details about string localization on Android [here][4].
+
+```xml
Download started
Download in progress
Download canceled
Download failed
Download complete
Download paused
-````
+```
-* **PackageInstaller:** in order to open APK files, your application needs `REQUEST_INSTALL_PACKAGES` permission. Add following codes in your `AndroidManifest.xml`:
+* **PackageInstaller:** In order to open APK files, your application needs `REQUEST_INSTALL_PACKAGES` permission. Add the following code in your `AndroidManifest.xml`:
-````xml
+```xml
-````
+```
* [Fix Cleartext Traffic Error in Android 9 Pie](https://medium.com/@son.rommer/fix-cleartext-traffic-error-in-android-9-pie-2f4e9e2235e6)
-## Usage
-
-#### Import package:
-
-````dart
-import 'package:flutter_downloader/flutter_downloader.dart';
-````
+
-#### Initialize
+## Usage
-````dart
-WidgetsFlutterBinding.ensureInitialized();
-await FlutterDownloader.initialize(
- debug: true // optional: set false to disable printing logs to console
-);
-````
+At the beginning of your `main` method, initialize the package:
-- Note: the plugin must be initialized before using.
+```dart
+await FlutterDownloader.initialize();
+```
-#### Create new download task:
+Then, you can create new `DownloadTask`s anywhere in your app:
-````dart
-final taskId = await FlutterDownloader.enqueue(
- url: 'your download link',
- savedDir: 'the path of directory where you want to save downloaded files',
- showNotification: true, // show download progress in status bar (for Android)
- openFileFromNotification: true, // click on notification to open downloaded file (for Android)
+```dart
+final task = await DownloadTask.create(
+ url: 'https://...',
+ downloadDirectory: await getExternalStorageDirectory(),
);
-````
-
-#### Update download progress:
-
-````dart
-FlutterDownloader.registerCallback(callback); // callback is a top-level or static function
-````
-
-**Important note:** your UI is rendered in the main isolate, while download events come from a background isolate (in other words, codes in `callback` are run in the background isolate), so you have to handle the communication between two isolates. For example:
-
-````dart
-ReceivePort _port = ReceivePort();
-
-@override
-void initState() {
- super.initState();
-
- IsolateNameServer.registerPortWithName(_port.sendPort, 'downloader_send_port');
- _port.listen((dynamic data) {
- String id = data[0];
- DownloadTaskStatus status = data[1];
- int progress = data[2];
- setState((){ });
- });
-
- FlutterDownloader.registerCallback(downloadCallback);
-}
-
-@override
-void dispose() {
- IsolateNameServer.removePortNameMapping('downloader_send_port');
- super.dispose();
-}
-
-static void downloadCallback(String id, DownloadTaskStatus status, int progress) {
- final SendPort send = IsolateNameServer.lookupPortByName('downloader_send_port');
- send.send([id, status, progress]);
-}
-
-````
-
+```
-#### Load all tasks:
+> Note: The `getExternalStorageDirectory` method used here is from the [path_provider](https://pub.dev/packages/path_provider) package.
-````dart
-final tasks = await FlutterDownloader.loadTasks();
-````
+Once you got a task, you can do stuff with it.
-#### Load tasks with conditions:
+For example, you can wait until the download completed and then open the downloaded file:
-````dart
-final tasks = await FlutterDownloader.loadTasksWithRawQuery(query: query);
-````
+```dart
+await task.wait();
+final wasSuccessfullyOpened = await task.openFile();
+```
-- Note: In order to parse data into `DownloadTask` object successfully, you should load data with all fields from DB (in the other word, use: `SELECT *` ). For example:
+> *Note:* in Android, you can only open a downloaded file if it is placed in the external storage and there's at least one application that can read that file type on your device.
-````SQL
-SELECT * FROM task WHERE status=3
-````
+You can also listen for `taks.updates`, which is especially useful when showing progress to the user:
-- Note: the following is the schema of `task` table where this plugin stores tasks information
-
-````SQL
-CREATE TABLE `task` (
- `id` INTEGER PRIMARY KEY AUTOINCREMENT,
- `task_id` VARCHAR ( 256 ),
- `url` TEXT,
- `status` INTEGER DEFAULT 0,
- `progress` INTEGER DEFAULT 0,
- `file_name` TEXT,
- `saved_dir` TEXT,
- `resumable` TINYINT DEFAULT 0,
- `headers` TEXT,
- `show_notification` TINYINT DEFAULT 0,
- `open_file_from_notification` TINYINT DEFAULT 0,
- `time_created` INTEGER DEFAULT 0
+```dart
+StreamBuilder(
+ stream: task.updates,
+ initialData: task,
+ builder: (_, __) => LinearProgressIndicator(value: task.progress),
);
-````
-
-#### Cancel a task:
-
-````dart
-FlutterDownloader.cancel(taskId: taskId);
-````
-
-#### Cancel all tasks:
-
-````dart
-FlutterDownloader.cancelAll();
-````
-
-#### Pause a task:
-
-````dart
-FlutterDownloader.pause(taskId: taskId);
-````
-
-#### Resume a task:
-
-````dart
-FlutterDownloader.resume(taskId: taskId);
-````
+```
-- Note: `resume()` will return a new `taskId` corresponding to a new background task that is created to continue the download process. You should replace the original `taskId` (that is marked as `paused` status) by this new `taskId` to continue tracking the download progress.
+And there's so much more you can do: You can `pause` and `resume` tasks, `cancel` them and `retry` failed or canceled tasks.
+For a demonstration of all of these, check out the [example](https://github.com/fluttercommunity/flutter_downloader/blob/master/example/lib/main.dart).
-#### Retry a failed task:
+There's also some global methods under the `FlutterDownloader`, for example to load or cancel all tasks.
-````dart
-FlutterDownloader.retry(taskId: taskId);
-````
+
-- Note: `retry()` will return a new `taskId` (like `resume()`)
+Advanced stuff
-#### Remove a task:
+Internally, all `DownloadTask`s are stored in an SQL database.
+You can directly query into this database:
```dart
-FlutterDownloader.remove(taskId: taskId, shouldDeleteContent:false);
+final tasks = await FlutterDownloader.loadTasksWithRawQuery(query: 'SELECT * FROM task WHERE status=3');
```
-#### Open and preview a downloaded file:
-
-````dart
-FlutterDownloader.open(taskId: taskId);
-````
-
-- Note: in Android, you can only open a downloaded file if it is placed in the external storage and there's at least one application that can read that file type on your device.
-
-## Bugs/Requests
-If you encounter any problems feel free to open an issue. If you feel the library is
-missing a feature, please raise a ticket on Github. Pull request are also welcome.
+> *Note:* This is the schema of the `task` table:
+>
+> ```SQL
+> CREATE TABLE `task` (
+> `id` INTEGER PRIMARY KEY AUTOINCREMENT,
+> `task_id` VARCHAR ( 256 ),
+> `url` TEXT,
+> `status` INTEGER DEFAULT 0,
+> `progress` INTEGER DEFAULT 0,
+> `file_name` TEXT,
+> `saved_dir` TEXT,
+> `resumable` TINYINT DEFAULT 0,
+> `headers` TEXT,
+> `show_notification` TINYINT DEFAULT 0,
+> `open_file_from_notification` TINYINT DEFAULT 0,
+> `time_created` INTEGER DEFAULT 0
+> );
+> ```
+
+## Bugs/PRs
+
+If you encounter any problems or feel the library is missing a feature, feel free to [open an issue on GitHub](https://github.com/fluttercommunity/flutter_downloader/issues/new).
[1]: https://developer.android.com/topic/libraries/architecture/workmanager
[2]: https://developer.apple.com/documentation/foundation/nsurlsessiondownloadtask?language=objc
diff --git a/example/lib/main.dart b/example/lib/main.dart
index cd204c2a..d84870aa 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -1,552 +1,191 @@
-import 'dart:isolate';
-import 'dart:ui';
import 'dart:async';
import 'dart:io';
+import 'package:black_hole_flutter/black_hole_flutter.dart';
import 'package:flutter/material.dart';
-import 'package:path_provider/path_provider.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
+import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
const debug = true;
+/// The concept of a file in this app.
+/// Don't confuse this with dart:io's File.
+class File {
+ File(this.name, this.url)
+ : assert(name != null),
+ assert(url != null);
+
+ final String name;
+ final String url;
+ DownloadTask task; // Represents a download. Is mutable and null by default.
+}
+
+/// A list of example files that can be downloaded by this app.
+final _files = [
+ File('Learning Android Studio',
+ 'http://barbra-coco.dyndns.org/student/learning_android_studio.pdf'),
+ File('Android Programming Cookbook',
+ 'http://enos.itcollege.ee/~jpoial/allalaadimised/reading/Android-Programming-Cookbook.pdf'),
+ File('iOS Programming Guide',
+ 'http://darwinlogic.com/uploads/education/iOS_Programming_Guide.pdf'),
+ File('Objective-C Programming (Pre-Course Workbook)',
+ 'https://www.bignerdranch.com/documents/objective-c-prereading-assignment.pdf'),
+ File('Arches National Park',
+ 'https://upload.wikimedia.org/wikipedia/commons/6/60/The_Organ_at_Arches_National_Park_Utah_Corrected.jpg'),
+ File('Canyonlands National Park',
+ 'https://upload.wikimedia.org/wikipedia/commons/7/78/Canyonlands_National_Park%E2%80%A6Needles_area_%286294480744%29.jpg'),
+ File('Death Valley National Park',
+ 'https://upload.wikimedia.org/wikipedia/commons/b/b2/Sand_Dunes_in_Death_Valley_National_Park.jpg'),
+ File('Gates of the Arctic National Park and Preserve',
+ 'https://upload.wikimedia.org/wikipedia/commons/e/e4/GatesofArctic.jpg'),
+ File('Big Buck Bunny',
+ 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'),
+ File('Elephant Dream',
+ 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4'),
+];
+
void main() async {
- WidgetsFlutterBinding.ensureInitialized();
await FlutterDownloader.initialize(debug: debug);
-
- runApp(new MyApp());
+ runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
- final platform = Theme.of(context).platform;
-
- return new MaterialApp(
- title: 'Flutter Demo',
- theme: new ThemeData(
- primarySwatch: Colors.blue,
- ),
- home: new MyHomePage(
- title: 'Downloader',
- platform: platform,
- ),
+ return MaterialApp(
+ title: 'Downloader Example',
+ theme: ThemeData(primarySwatch: Colors.blue),
+ home: StartPage(),
);
}
}
-class MyHomePage extends StatefulWidget with WidgetsBindingObserver {
- final TargetPlatform platform;
-
- MyHomePage({Key key, this.title, this.platform}) : super(key: key);
-
- final String title;
-
- @override
- _MyHomePageState createState() => new _MyHomePageState();
-}
-
-class _MyHomePageState extends State {
- final _documents = [
- {
- 'name': 'Learning Android Studio',
- 'link':
- 'http://barbra-coco.dyndns.org/student/learning_android_studio.pdf'
- },
- {
- 'name': 'Android Programming Cookbook',
- 'link':
- 'http://enos.itcollege.ee/~jpoial/allalaadimised/reading/Android-Programming-Cookbook.pdf'
- },
- {
- 'name': 'iOS Programming Guide',
- 'link':
- 'http://darwinlogic.com/uploads/education/iOS_Programming_Guide.pdf'
- },
- {
- 'name': 'Objective-C Programming (Pre-Course Workbook',
- 'link':
- 'https://www.bignerdranch.com/documents/objective-c-prereading-assignment.pdf'
- },
- ];
-
- final _images = [
- {
- 'name': 'Arches National Park',
- 'link':
- 'https://upload.wikimedia.org/wikipedia/commons/6/60/The_Organ_at_Arches_National_Park_Utah_Corrected.jpg'
- },
- {
- 'name': 'Canyonlands National Park',
- 'link':
- 'https://upload.wikimedia.org/wikipedia/commons/7/78/Canyonlands_National_Park%E2%80%A6Needles_area_%286294480744%29.jpg'
- },
- {
- 'name': 'Death Valley National Park',
- 'link':
- 'https://upload.wikimedia.org/wikipedia/commons/b/b2/Sand_Dunes_in_Death_Valley_National_Park.jpg'
- },
- {
- 'name': 'Gates of the Arctic National Park and Preserve',
- 'link':
- 'https://upload.wikimedia.org/wikipedia/commons/e/e4/GatesofArctic.jpg'
- }
- ];
-
- final _videos = [
- {
- 'name': 'Big Buck Bunny',
- 'link':
- 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'
- },
- {
- 'name': 'Elephant Dream',
- 'link':
- 'http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4'
- }
- ];
-
- List<_TaskInfo> _tasks;
- List<_ItemHolder> _items;
- bool _isLoading;
- bool _permissionReady;
- String _localPath;
- ReceivePort _port = ReceivePort();
-
+/// This page introduces the user into the concept of the app.
+class StartPage extends StatelessWidget {
@override
- void initState() {
- super.initState();
-
- _bindBackgroundIsolate();
-
- FlutterDownloader.registerCallback(downloadCallback);
-
- _isLoading = true;
- _permissionReady = false;
-
- _prepare();
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: Text('Downloader Example')),
+ body: Center(child: Text("We're gonna download some files.")),
+ floatingActionButton: ContinueButton(),
+ );
}
+}
+class ContinueButton extends StatelessWidget {
@override
- void dispose() {
- _unbindBackgroundIsolate();
- super.dispose();
+ Widget build(BuildContext context) {
+ return FloatingActionButton.extended(
+ label: Text('Continue'),
+ onPressed: () => _onClick(context),
+ );
}
- void _bindBackgroundIsolate() {
- bool isSuccess = IsolateNameServer.registerPortWithName(
- _port.sendPort, 'downloader_send_port');
- if (!isSuccess) {
- _unbindBackgroundIsolate();
- _bindBackgroundIsolate();
+ Future _onClick(BuildContext context) async {
+ // Ensure that the storage permission is granted on Android.
+ if (Platform.isAndroid && !await Permission.storage.request().isGranted) {
+ context.scaffold.showSnackBar(SnackBar(
+ content: Text('You need to grant storage permission so that we '
+ 'can download files. 😇'),
+ ));
return;
}
- _port.listen((dynamic data) {
- if (debug) {
- print('UI Isolate Callback: $data');
- }
- String id = data[0];
- DownloadTaskStatus status = data[1];
- int progress = data[2];
- final task = _tasks?.firstWhere((task) => task.taskId == id);
- if (task != null) {
- setState(() {
- task.status = status;
- task.progress = progress;
- });
- }
- });
- }
-
- void _unbindBackgroundIsolate() {
- IsolateNameServer.removePortNameMapping('downloader_send_port');
- }
-
- static void downloadCallback(
- String id, DownloadTaskStatus status, int progress) {
- if (debug) {
- print(
- 'Background Isolate Callback: task ($id) is in status ($status) and process ($progress)');
+ // Load the existing tasks from the DB and bind matching [File]s to them.
+ for (final existingTask in await FlutterDownloader.loadTasks()) {
+ // Find the [File] that corresponds to the task. If there is one, set its
+ // tasks to the already existing one.
+ final task = _files.singleWhere((task) => task.url == existingTask.url,
+ orElse: () => null);
+ task?.task = existingTask;
}
- final SendPort send =
- IsolateNameServer.lookupPortByName('downloader_send_port');
- send.send([id, status, progress]);
+
+ // Push the new page.
+ // Note: The new page also uses the [_files]. In production, you probably
+ // don't want to solve this with global state, but rather pass an array of
+ // files or something similar to the other widget.
+ context.navigator.pushReplacement(MaterialPageRoute(
+ builder: (_) => DownloadList(),
+ ));
}
+}
+/// This page shows the downloadable files.
+class DownloadList extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return new Scaffold(
- appBar: new AppBar(
- title: new Text(widget.title),
- ),
- body: Builder(
- builder: (context) => _isLoading
- ? new Center(
- child: new CircularProgressIndicator(),
- )
- : _permissionReady
- ? _buildDownloadList()
- : _buildNoPermissionWarning()),
+ return Scaffold(
+ appBar: AppBar(title: Text('Downloader Example')),
+ body: ListView(children: _files.map((file) => FileWidget(file)).toList()),
);
}
+}
- Widget _buildDownloadList() => Container(
- child: ListView(
- padding: const EdgeInsets.symmetric(vertical: 16.0),
- children: _items
- .map((item) => item.task == null
- ? _buildListSection(item.name)
- : DownloadItem(
- data: item,
- onItemClick: (task) {
- _openDownloadedFile(task).then((success) {
- if (!success) {
- Scaffold.of(context).showSnackBar(SnackBar(
- content: Text('Cannot open this file')));
- }
- });
- },
- onAtionClick: (task) {
- if (task.status == DownloadTaskStatus.undefined) {
- _requestDownload(task);
- } else if (task.status == DownloadTaskStatus.running) {
- _pauseDownload(task);
- } else if (task.status == DownloadTaskStatus.paused) {
- _resumeDownload(task);
- } else if (task.status == DownloadTaskStatus.complete) {
- _delete(task);
- } else if (task.status == DownloadTaskStatus.failed) {
- _retryDownload(task);
- }
- },
- ))
- .toList(),
- ),
- );
-
- Widget _buildListSection(String title) => Container(
- padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
- child: Text(
- title,
- style: TextStyle(
- fontWeight: FontWeight.bold, color: Colors.blue, fontSize: 18.0),
- ),
- );
-
- Widget _buildNoPermissionWarning() => Container(
- child: Center(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 24.0),
- child: Text(
- 'Please grant accessing storage permission to continue -_-',
- textAlign: TextAlign.center,
- style: TextStyle(color: Colors.blueGrey, fontSize: 18.0),
- ),
- ),
- SizedBox(
- height: 32.0,
- ),
- FlatButton(
- onPressed: () {
- _checkPermission().then((hasGranted) {
- setState(() {
- _permissionReady = hasGranted;
- });
- });
- },
- child: Text(
- 'Retry',
- style: TextStyle(
- color: Colors.blue,
- fontWeight: FontWeight.bold,
- fontSize: 20.0),
- ))
- ],
- ),
- ),
- );
-
- void _requestDownload(_TaskInfo task) async {
- task.taskId = await FlutterDownloader.enqueue(
- url: task.link,
- headers: {"auth": "test_for_sql_encoding"},
- savedDir: _localPath,
- showNotification: true,
- openFileFromNotification: true);
- }
-
- void _cancelDownload(_TaskInfo task) async {
- await FlutterDownloader.cancel(taskId: task.taskId);
- }
-
- void _pauseDownload(_TaskInfo task) async {
- await FlutterDownloader.pause(taskId: task.taskId);
- }
-
- void _resumeDownload(_TaskInfo task) async {
- String newTaskId = await FlutterDownloader.resume(taskId: task.taskId);
- task.taskId = newTaskId;
- }
-
- void _retryDownload(_TaskInfo task) async {
- String newTaskId = await FlutterDownloader.retry(taskId: task.taskId);
- task.taskId = newTaskId;
- }
-
- Future _openDownloadedFile(_TaskInfo task) {
- return FlutterDownloader.open(taskId: task.taskId);
- }
-
- void _delete(_TaskInfo task) async {
- await FlutterDownloader.remove(
- taskId: task.taskId, shouldDeleteContent: true);
- await _prepare();
- setState(() {});
- }
-
- Future _checkPermission() async {
- if (widget.platform == TargetPlatform.android) {
- final status = await Permission.storage.status;
- if (status != PermissionStatus.granted) {
- final result = await Permission.storage.request();
- if (result == PermissionStatus.granted) {
- return true;
- }
- } else {
- return true;
- }
- } else {
- return true;
- }
- return false;
- }
-
- Future _prepare() async {
- final tasks = await FlutterDownloader.loadTasks();
-
- int count = 0;
- _tasks = [];
- _items = [];
-
- _tasks.addAll(_documents.map((document) =>
- _TaskInfo(name: document['name'], link: document['link'])));
-
- _items.add(_ItemHolder(name: 'Documents'));
- for (int i = count; i < _tasks.length; i++) {
- _items.add(_ItemHolder(name: _tasks[i].name, task: _tasks[i]));
- count++;
- }
-
- _tasks.addAll(_images
- .map((image) => _TaskInfo(name: image['name'], link: image['link'])));
-
- _items.add(_ItemHolder(name: 'Images'));
- for (int i = count; i < _tasks.length; i++) {
- _items.add(_ItemHolder(name: _tasks[i].name, task: _tasks[i]));
- count++;
- }
-
- _tasks.addAll(_videos
- .map((video) => _TaskInfo(name: video['name'], link: video['link'])));
-
- _items.add(_ItemHolder(name: 'Videos'));
- for (int i = count; i < _tasks.length; i++) {
- _items.add(_ItemHolder(name: _tasks[i].name, task: _tasks[i]));
- count++;
- }
-
- tasks?.forEach((task) {
- for (_TaskInfo info in _tasks) {
- if (info.link == task.url) {
- info.taskId = task.taskId;
- info.status = task.status;
- info.progress = task.progress;
- }
- }
- });
-
- _permissionReady = await _checkPermission();
-
- _localPath = (await _findLocalPath()) + Platform.pathSeparator + 'Download';
-
- final savedDir = Directory(_localPath);
- bool hasExisted = await savedDir.exists();
- if (!hasExisted) {
- savedDir.create();
- }
+/// Widget that shows a [File] in the list.
+class FileWidget extends StatefulWidget {
+ const FileWidget(this.file);
- setState(() {
- _isLoading = false;
- });
- }
+ final File file;
- Future _findLocalPath() async {
- final directory = widget.platform == TargetPlatform.android
- ? await getExternalStorageDirectory()
- : await getApplicationDocumentsDirectory();
- return directory.path;
- }
+ @override
+ _FileWidgetState createState() => _FileWidgetState();
}
-class DownloadItem extends StatelessWidget {
- final _ItemHolder data;
- final Function(_TaskInfo) onItemClick;
- final Function(_TaskInfo) onAtionClick;
-
- DownloadItem({this.data, this.onItemClick, this.onAtionClick});
+class _FileWidgetState extends State {
+ File get file => widget.file;
+ DownloadTask get task => file.task;
@override
- Widget build(BuildContext context) {
- return Container(
- padding: const EdgeInsets.only(left: 16.0, right: 8.0),
- child: InkWell(
- onTap: data.task.status == DownloadTaskStatus.complete
- ? () {
- onItemClick(data.task);
- }
- : null,
- child: Stack(
- children: [
- Container(
- width: double.infinity,
- height: 64.0,
- child: Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Expanded(
- child: Text(
- data.name,
- maxLines: 1,
- softWrap: true,
- overflow: TextOverflow.ellipsis,
- ),
- ),
- Padding(
- padding: const EdgeInsets.only(left: 8.0),
- child: _buildActionForTask(data.task),
- ),
- ],
- ),
- ),
- data.task.status == DownloadTaskStatus.running ||
- data.task.status == DownloadTaskStatus.paused
- ? Positioned(
- left: 0.0,
- right: 0.0,
- bottom: 0.0,
- child: LinearProgressIndicator(
- value: data.task.progress / 100,
- ),
- )
- : Container()
- ].where((child) => child != null).toList(),
- ),
- ),
- );
+ void initState() {
+ super.initState();
+ // Update this widget whenever the download task's status updates.
+ task?.updates?.listen((_) => setState(() {}));
}
- Widget _buildActionForTask(_TaskInfo task) {
- if (task.status == DownloadTaskStatus.undefined) {
- return RawMaterialButton(
- onPressed: () {
- onAtionClick(task);
- },
- child: Icon(Icons.file_download),
- shape: CircleBorder(),
- constraints: BoxConstraints(minHeight: 32.0, minWidth: 32.0),
- );
- } else if (task.status == DownloadTaskStatus.running) {
- return RawMaterialButton(
- onPressed: () {
- onAtionClick(task);
- },
- child: Icon(
- Icons.pause,
- color: Colors.red,
- ),
- shape: CircleBorder(),
- constraints: BoxConstraints(minHeight: 32.0, minWidth: 32.0),
- );
- } else if (task.status == DownloadTaskStatus.paused) {
- return RawMaterialButton(
- onPressed: () {
- onAtionClick(task);
+ @override
+ Widget build(BuildContext context) {
+ Widget trailing;
+ if (task == null) {
+ trailing = IconButton(
+ icon: Icon(Icons.file_download),
+ onPressed: () async {
+ file.task = await DownloadTask.create(
+ url: file.url,
+ downloadDirectory: Platform.isAndroid
+ ? await getExternalStorageDirectory()
+ : await getApplicationDocumentsDirectory(),
+ );
+ // Update this widget when the task status updates.
+ task?.updates?.listen((_) => setState(() {}));
},
- child: Icon(
- Icons.play_arrow,
- color: Colors.green,
- ),
- shape: CircleBorder(),
- constraints: BoxConstraints(minHeight: 32.0, minWidth: 32.0),
);
- } else if (task.status == DownloadTaskStatus.complete) {
- return Row(
- mainAxisSize: MainAxisSize.min,
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- Text(
- 'Ready',
- style: TextStyle(color: Colors.green),
- ),
- RawMaterialButton(
- onPressed: () {
- onAtionClick(task);
- },
- child: Icon(
- Icons.delete_forever,
- color: Colors.red,
- ),
- shape: CircleBorder(),
- constraints: BoxConstraints(minHeight: 32.0, minWidth: 32.0),
- )
- ],
- );
- } else if (task.status == DownloadTaskStatus.canceled) {
- return Text('Canceled', style: TextStyle(color: Colors.red));
- } else if (task.status == DownloadTaskStatus.failed) {
- return Row(
- mainAxisSize: MainAxisSize.min,
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- Text('Failed', style: TextStyle(color: Colors.red)),
- RawMaterialButton(
- onPressed: () {
- onAtionClick(task);
- },
- child: Icon(
- Icons.refresh,
- color: Colors.green,
- ),
- shape: CircleBorder(),
- constraints: BoxConstraints(minHeight: 32.0, minWidth: 32.0),
- )
- ],
- );
- } else {
- return null;
+ } else if (task.isRunning) {
+ trailing = IconButton(icon: Icon(Icons.pause), onPressed: task.pause);
+ } else if (task.isPaused) {
+ trailing =
+ IconButton(icon: Icon(Icons.play_arrow), onPressed: task.resume);
+ } else if (task.hasFailed || task.gotCanceled) {
+ trailing = IconButton(icon: Icon(Icons.refresh), onPressed: task.retry);
+ } else if (task.isCompleted) {
+ trailing =
+ IconButton(icon: Icon(Icons.open_in_new), onPressed: task.openFile);
+ } else if (task.isEnqueued) {
+ trailing = Icon(Icons.schedule);
+ } else if (task.hasUndefinedStatus) {
+ trailing = Icon(Icons.help_outline);
}
- }
-}
-
-class _TaskInfo {
- final String name;
- final String link;
-
- String taskId;
- int progress = 0;
- DownloadTaskStatus status = DownloadTaskStatus.undefined;
- _TaskInfo({this.name, this.link});
-}
-
-class _ItemHolder {
- final String name;
- final _TaskInfo task;
-
- _ItemHolder({this.name, this.task});
+ return ListTile(
+ title: Text(file.name),
+ subtitle: task == null
+ ? Text(file.url)
+ : task.isRunning || task.isPaused
+ ? LinearProgressIndicator(value: task.progress)
+ : Text(task.hasFailed
+ ? 'failed'
+ : task.gotCanceled ? 'canceled' : file.url),
+ trailing: trailing ?? Container(),
+ );
+ }
}
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 0ad04322..1e2a19d2 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -12,6 +12,7 @@ version: 1.0.0+1
dependencies:
flutter:
sdk: flutter
+ black_hole_flutter: ^0.2.12
path_provider: ^1.6.11
permission_handler: ^5.0.1+1
flutter_downloader:
@@ -25,28 +26,26 @@ dev_dependencies:
flutter_test:
sdk: flutter
+dependency_overrides:
+ dartx: ^0.4.1
+
# For information on the generic Dart part of this file, see the
# following page: https://www.dartlang.org/tools/pub/pubspec
# The following section is specific to Flutter.
flutter:
-
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
-
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
-
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.io/assets-and-images/#resolution-aware.
-
# For details regarding adding assets from package dependencies, see
# https://flutter.io/assets-and-images/#from-packages
-
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
diff --git a/lib/flutter_downloader.dart b/lib/flutter_downloader.dart
index 51a0ce46..d514521f 100644
--- a/lib/flutter_downloader.dart
+++ b/lib/flutter_downloader.dart
@@ -11,9 +11,7 @@
/// All task information is saved in a Sqlite database, it gives a Flutter
/// application benefit of either getting rid of managing task information
/// manually or querying task data with SQL statements easily.
-///
library flutter_downloader;
export 'src/downloader.dart';
-export 'src/models.dart';
diff --git a/lib/src/callback_dispatcher.dart b/lib/src/callback_dispatcher.dart
index 7ff00f12..778b83ab 100644
--- a/lib/src/callback_dispatcher.dart
+++ b/lib/src/callback_dispatcher.dart
@@ -1,13 +1,7 @@
-import 'dart:ui';
+part of 'downloader.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter/widgets.dart';
-
-import 'models.dart';
-
-void callbackDispatcher() {
- const MethodChannel backgroundChannel =
- MethodChannel('vn.hunghd/downloader_background');
+void dispatchCallback() {
+ const backgroundChannel = MethodChannel('vn.hunghd/downloader_background');
WidgetsFlutterBinding.ensureInitialized();
@@ -17,11 +11,11 @@ void callbackDispatcher() {
final Function callback = PluginUtilities.getCallbackFromHandle(
CallbackHandle.fromRawHandle(args[0]));
- final String id = args[1];
- final int status = args[2];
- final int progress = args[3];
+ final id = args[1] as String;
+ final status = args[2] as int;
+ final progress = (args[3] as int).toDouble() / 100.0;
- callback(id, DownloadTaskStatus(status), progress);
+ callback(id, status, progress);
});
backgroundChannel.invokeMethod('didInitializeDispatcher');
diff --git a/lib/src/downloader.dart b/lib/src/downloader.dart
index 267cd295..412156d3 100644
--- a/lib/src/downloader.dart
+++ b/lib/src/downloader.dart
@@ -1,395 +1,233 @@
import 'dart:async';
import 'dart:io';
+import 'dart:isolate';
import 'dart:ui';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
+import 'package:meta/meta.dart';
-import 'callback_dispatcher.dart';
-import 'models.dart';
-
-///
-/// A signature function for download progress updating callback
-///
-/// * `id`: unique identifier of a download task
-/// * `status`: current status of a download task
-/// * `progress`: current progress value of a download task, the value is in
-/// range of 0 and 100
-///
-typedef void DownloadCallback(
- String id, DownloadTaskStatus status, int progress);
-
-///
-/// A convenient class wraps all api functions of **FlutterDownloader** plugin
-///
-class FlutterDownloader {
+part 'task.dart';
+part 'status.dart';
+part 'callback_dispatcher.dart';
+
+File _fileFromDirAndName(String dir, String name) =>
+ File('$dir${Platform.pathSeparator}$name');
+
+abstract class FlutterDownloader {
static const _channel = const MethodChannel('vn.hunghd/downloader');
static bool _initialized = false;
+ static var _tasksById = {};
- static Future initialize({bool debug = true}) async {
+ static Future initialize({bool debug = true}) async {
assert(!_initialized,
- 'FlutterDownloader.initialize() must be called only once!');
+ 'FlutterDownloader.initialize() must be called only once.');
WidgetsFlutterBinding.ensureInitialized();
- final callback = PluginUtilities.getCallbackHandle(callbackDispatcher);
- await _channel.invokeMethod(
- 'initialize', [callback.toRawHandle(), debug ? 1 : 0]);
+ final callback = PluginUtilities.getCallbackHandle(dispatchCallback);
+ await _channel.invokeMethod('initialize', [
+ callback.toRawHandle(),
+ debug ? 1 : 0,
+ ]);
+
+ // Create callback listener.
+ final port = ReceivePort()
+ ..listen((dynamic data) {
+ final id = data[0] as String;
+ final status = _StatusByValue.create(data[1] as int);
+ final progress = data[2] as double;
+ final task = _tasksById[id];
+ task?._update(id, status, progress, task?.destination);
+ });
+ IsolateNameServer.registerPortWithName(port.sendPort, 'downloader_port');
+ final callbackHandle = PluginUtilities.getCallbackHandle(_onUpdate);
+ _channel.invokeMethod('registerCallback', [
+ callbackHandle.toRawHandle(),
+ ]);
+
_initialized = true;
- return null;
}
- ///
- /// Create a new download task
- ///
- /// **parameters:**
- ///
- /// * `url`: download link
- /// * `savedDir`: absolute path of the directory where downloaded file is saved
- /// * `fileName`: name of downloaded file. If this parameter is not set, the
- /// plugin will try to extract a file name from HTTP headers response or `url`
- /// * `headers`: HTTP headers
- /// * `showNotification`: sets `true` to show a notification displaying the
- /// download progress (only Android), otherwise, `false` value will disable
- /// this feature. The default value is `true`
- /// * `openFileFromNotification`: if `showNotification` is `true`, this flag
- /// controls the way to response to user's click action on the notification
- /// (only Android). If it is `true`, user can click on the notification to
- /// open and preview the downloaded file, otherwise, nothing happens. The
- /// default value is `true`
- ///
- /// **return:**
- ///
- /// an unique identifier of the new download task
- ///
- static Future enqueue(
- {@required String url,
- @required String savedDir,
- String fileName,
- Map headers,
- bool showNotification = true,
- bool openFileFromNotification = true,
- bool requiresStorageNotLow = true}) async {
+ static void _onUpdate(String id, int statusCode, double progress) {
+ print(
+ 'onUpdate called with id $id, status $statusCode and progress $progress');
+ final send = IsolateNameServer.lookupPortByName('downloader_port');
+ send.send([id, statusCode, progress]);
+ }
+
+ static void _ensureInitialized() {
assert(_initialized, 'FlutterDownloader.initialize() must be called first');
- assert(Directory(savedDir).existsSync());
+ }
+
+ static Future _enqueue({
+ @required String url,
+ @required Directory downloadDirectory,
+ String fileName,
+ Map headers,
+ bool showNotification,
+ bool openFileFromNotification,
+ bool requiresStorageNotLow,
+ }) async {
+ _ensureInitialized();
+ assert(downloadDirectory.existsSync());
+ assert(showNotification != null);
+ assert(openFileFromNotification != null);
+ assert(requiresStorageNotLow != null);
StringBuffer headerBuilder = StringBuffer();
if (headers != null) {
- headerBuilder.write('{');
- headerBuilder.writeAll(
- headers.entries
- .map((entry) => '\"${entry.key}\": \"${entry.value}\"'),
- ',');
- headerBuilder.write('}');
- }
- try {
- String taskId = await _channel.invokeMethod('enqueue', {
- 'url': url,
- 'saved_dir': savedDir,
- 'file_name': fileName,
- 'headers': headerBuilder.toString(),
- 'show_notification': showNotification,
- 'open_file_from_notification': openFileFromNotification,
- 'requires_storage_not_low': requiresStorageNotLow,
- });
- return taskId;
- } on PlatformException catch (e) {
- print('Download task is failed with reason(${e.message})');
- return null;
+ headerBuilder
+ ..write('{')
+ ..writeAll([
+ for (final entry in headers.entries)
+ '"${entry.key}": "${entry.value}",',
+ ])
+ ..write('}');
}
+
+ // This call might fail, in which case we just throw the PlatformException.
+ final arguments = {
+ 'url': url,
+ 'saved_dir': downloadDirectory.path,
+ 'file_name': fileName,
+ 'headers': headerBuilder.toString(),
+ 'show_notification': showNotification,
+ 'open_file_from_notification': openFileFromNotification,
+ 'requires_storage_not_low': requiresStorageNotLow,
+ };
+ print(arguments);
+
+ final taskId = await _channel.invokeMethod('enqueue', arguments) as String;
+
+ // Create download task.
+ final task = DownloadTask._(
+ id: taskId,
+ status: DownloadTaskStatus.enqueued,
+ progress: 0.0,
+ url: url,
+ destination: _fileFromDirAndName(downloadDirectory.path, fileName),
+ created: DateTime.now(),
+ );
+ _tasksById[taskId] = task;
+
+ return task;
}
- ///
- /// Load all tasks from Sqlite database
- ///
- /// **return:**
- ///
- /// A list of [DownloadTask] objects
- ///
+ /// Loads all tasks.
static Future> loadTasks() async {
- assert(_initialized, 'FlutterDownloader.initialize() must be called first');
+ _ensureInitialized();
- try {
- List result = await _channel.invokeMethod('loadTasks');
- return result
- .map((item) => new DownloadTask(
- taskId: item['task_id'],
- status: DownloadTaskStatus(item['status']),
- progress: item['progress'],
- url: item['url'],
- filename: item['file_name'],
- savedDir: item['saved_dir'],
- timeCreated: item['time_created']))
- .toList();
- } on PlatformException catch (e) {
- print(e.message);
- return null;
- }
+ final allTasks = (await _channel.invokeMethod('loadTasks') as List)
+ .map((item) => DownloadTask._fromQueryResult(item))
+ .toList();
+
+ _tasksById = {
+ for (final task in allTasks)
+ if (_tasksById.containsKey(task.id))
+ task.id: _tasksById[task.id].._merge(task)
+ else
+ task.id: task,
+ };
+
+ return _tasksById.values.toList();
}
- ///
- /// Load tasks from Sqlite database with SQL statements
- ///
- /// **parameters:**
- ///
- /// * `query`: SQL statement. Note that the plugin will parse loaded data from
- /// database into [DownloadTask] object, in order to make it work, you should
- /// load tasks with all fields from database. In other words, using `SELECT *`
- /// statement.
- ///
- /// **return:**
- ///
- /// A list of [DownloadTask] objects
- ///
- /// **example:**
+ /// Loads tasks from Sqlite database with a custom SQL statement.
+ /// Use the `SELECT *` statement to load tasks with all fields from the
+ /// database – otherwise this function will fail because it tries to parse
+ /// the result into [DownloadTask]s.
///
/// ```dart
- /// FlutterDownloader.loadTasksWithRawQuery(query: 'SELECT * FROM task WHERE status=3');
+ /// FlutterDownloader.loadTasksWithRawQuery('SELECT * FROM task WHERE status=3');
/// ```
- ///
- static Future> loadTasksWithRawQuery(
- {@required String query}) async {
- assert(_initialized, 'FlutterDownloader.initialize() must be called first');
-
- try {
- List result = await _channel
- .invokeMethod('loadTasksWithRawQuery', {'query': query});
- return result
- .map((item) => new DownloadTask(
- taskId: item['task_id'],
- status: DownloadTaskStatus(item['status']),
- progress: item['progress'],
- url: item['url'],
- filename: item['file_name'],
- savedDir: item['saved_dir'],
- timeCreated: item['time_created']))
- .toList();
- } on PlatformException catch (e) {
- print(e.message);
- return null;
- }
- }
+ static Future> loadTasksWithRawQuery(String query) async {
+ _ensureInitialized();
- ///
- /// Cancel a given download task
- ///
- /// **parameters:**
- ///
- /// * `taskId`: unique identifier of the download task
- ///
- static Future cancel({@required String taskId}) async {
- assert(_initialized, 'FlutterDownloader.initialize() must be called first');
+ final tasks = (await _channel.invokeMethod('loadTasksWithRawQuery', {
+ 'query': query,
+ }) as List)
+ .map((item) => DownloadTask._fromQueryResult(item))
+ .toList();
- try {
- return await _channel.invokeMethod('cancel', {'task_id': taskId});
- } on PlatformException catch (e) {
- print(e.message);
- return null;
+ for (final task in tasks) {
+ _tasksById.putIfAbsent(task.id, () => task)._merge(task);
}
- }
- ///
- /// Cancel all enqueued and running download tasks
- ///
- static Future cancelAll() async {
- assert(_initialized, 'FlutterDownloader.initialize() must be called first');
+ return tasks;
+ }
- try {
- return await _channel.invokeMethod('cancelAll');
- } on PlatformException catch (e) {
- print(e.message);
- return null;
+ /// Returns the task with the given [id].
+ /// **Caution**: The [id] is inserted into a raw SQL query with no further
+ /// checks done. Prone to SQL injection if unsanitized input is fed in as
+ /// [id].
+ static Future loadTaskById(String id) async {
+ _ensureInitialized();
+
+ final tasks =
+ await loadTasksWithRawQuery('SELECT * FROM task WHERE task_id="$id"');
+ if (tasks.isEmpty) {
+ throw Exception('No task with id $id exists.');
}
+ return tasks.single;
}
- ///
- /// Pause a running download task
- ///
- /// **parameters:**
- ///
- /// * `taskId`: unique identifier of a running download task
- ///
- static Future pause({@required String taskId}) async {
- assert(_initialized, 'FlutterDownloader.initialize() must be called first');
-
- try {
- return await _channel.invokeMethod('pause', {'task_id': taskId});
- } on PlatformException catch (e) {
- print(e.message);
- return null;
- }
+ static Future _cancel(DownloadTask task) async {
+ _ensureInitialized();
+ await _channel.invokeMethod('cancel', {'task_id': task.id});
}
- ///
- /// Resume a paused download task
- ///
- /// **parameters:**
- ///
- /// * `taskId`: unique identifier of a paused download task
- ///
- /// **return:**
- ///
- /// An unique identifier of a new download task that is created to continue
- /// the partial download progress
- ///
- static Future resume({
- @required String taskId,
- bool requiresStorageNotLow = true,
- }) async {
- assert(_initialized, 'FlutterDownloader.initialize() must be called first');
+ /// Cancels all enqueued and running [DownloadTask]s.
+ static Future cancelAllTasks() async {
+ _ensureInitialized();
+ await _channel.invokeMethod('cancelAll');
+ }
- try {
- return await _channel.invokeMethod('resume', {
- 'task_id': taskId,
- 'requires_storage_not_low': requiresStorageNotLow,
- });
- } on PlatformException catch (e) {
- print(e.message);
- return null;
- }
+ static Future _pause(DownloadTask task) async {
+ _ensureInitialized();
+ await _channel.invokeMethod('pause', {'task_id': task.id});
}
- ///
- /// Retry a failed download task
- ///
- /// **parameters:**
- ///
- /// * `taskId`: unique identifier of a failed download task
- ///
- /// **return:**
- ///
- /// An unique identifier of a new download task that is created to start the
- /// failed download progress from the beginning
- ///
- static Future retry({
- @required String taskId,
- bool requiresStorageNotLow = true,
+ static Future _resume(
+ DownloadTask task, {
+ @required bool requiresStorageNotLow,
}) async {
- assert(_initialized, 'FlutterDownloader.initialize() must be called first');
-
- try {
- return await _channel.invokeMethod('retry', {
- 'task_id': taskId,
- 'requires_storage_not_low': requiresStorageNotLow,
- });
- } on PlatformException catch (e) {
- print(e.message);
- return null;
- }
+ _ensureInitialized();
+ assert(requiresStorageNotLow != null);
+
+ final idOfNewTask = await _channel.invokeMethod('resume', {
+ 'task_id': task.id,
+ 'requires_storage_not_low': requiresStorageNotLow,
+ });
+ _tasksById[idOfNewTask] = task.._merge(await loadTaskById(idOfNewTask));
}
- ///
- /// Delete a download task from DB. If the given task is running, it is canceled
- /// as well. If the task is completed and `shouldDeleteContent` is `true`,
- /// the downloaded file will be deleted.
- ///
- /// **parameters:**
- ///
- /// * `taskId`: unique identifier of a download task
- /// * `shouldDeleteContent`: if the task is completed, set `true` to let the
- /// plugin remove the downloaded file. The default value is `false`.
- ///
- static Future remove(
- {@required String taskId, bool shouldDeleteContent = false}) async {
- assert(_initialized, 'FlutterDownloader.initialize() must be called first');
-
- try {
- return await _channel.invokeMethod('remove',
- {'task_id': taskId, 'should_delete_content': shouldDeleteContent});
- } on PlatformException catch (e) {
- print(e.message);
- return null;
- }
+ static Future _retry(
+ DownloadTask task, {
+ @required bool requiresStorageNotLow,
+ }) async {
+ _ensureInitialized();
+ assert(requiresStorageNotLow != null);
+
+ final idOfNewTask = await _channel.invokeMethod('retry', {
+ 'task_id': task.id,
+ 'requires_storage_not_low': requiresStorageNotLow,
+ });
+ _tasksById[idOfNewTask] = task.._merge(await loadTaskById(idOfNewTask));
}
- ///
- /// Open and preview a downloaded file
- ///
- /// **parameters:**
- ///
- /// * `taskId`: An unique identifier of a completed download task
- ///
- /// **return:**
- ///
- /// Returns `true` if the downloaded file can be open on the current device,
- /// `false` in otherwise.
- ///
- /// **Note:**
- ///
- /// In Android case, there're two requirements in order to be able to open
- /// a file:
- /// - The file have to be saved in external storage where other applications
- /// have permission to read this file
- /// - The current device has at least an application that can read the file
- /// type of the file
- ///
- static Future open({@required String taskId}) async {
- assert(_initialized, 'FlutterDownloader.initialize() must be called first');
+ static Future _remove(DownloadTask task, {bool removeContent}) async {
+ _ensureInitialized();
+ assert(removeContent != null);
- try {
- return await _channel.invokeMethod('open', {'task_id': taskId});
- } on PlatformException catch (e) {
- print(e.message);
- return false;
- }
+ await _channel.invokeMethod('remove', {
+ 'task_id': task.id,
+ 'should_delete_content': removeContent,
+ });
}
- ///
- /// Register a callback to track status and progress of download task
- ///
- /// **parameters:**
- ///
- /// * `callback`: a top-level or static function of [DownloadCallback] type
- /// which is called whenever the status or progress value of a download task
- /// has been changed.
- ///
- /// **Note:**
- ///
- /// Your UI is rendered in the main isolate, while download events come from a
- /// background isolate (in other words, codes in `callback` are run in the
- /// background isolate), so you have to handle the communication between two
- /// isolates.
- ///
- /// **Example:**
- ///
- /// {@tool sample}
- ///
- /// ```dart
- ///
- /// ReceivePort _port = ReceivePort();
- ///
- /// @override
- /// void initState() {
- /// super.initState();
- ///
- /// IsolateNameServer.registerPortWithName(_port.sendPort, 'downloader_send_port');
- /// _port.listen((dynamic data) {
- /// String id = data[0];
- /// DownloadTaskStatus status = data[1];
- /// int progress = data[2];
- /// setState((){ });
- /// });
- ///
- /// FlutterDownloader.registerCallback(downloadCallback);
- ///
- /// }
- ///
- /// static void downloadCallback(String id, DownloadTaskStatus status, int progress) {
- /// final SendPort send = IsolateNameServer.lookupPortByName('downloader_send_port');
- /// send.send([id, status, progress]);
- /// }
- ///
- /// ```
- ///
- /// {@end-tool}
- ///
- static registerCallback(DownloadCallback callback) {
- assert(_initialized, 'FlutterDownloader.initialize() must be called first');
-
- final callbackHandle = PluginUtilities.getCallbackHandle(callback);
- assert(callbackHandle != null,
- 'callback must be a top-level or a static function');
- _channel.invokeMethod(
- 'registerCallback', [callbackHandle.toRawHandle()]);
+ static Future _openFile(DownloadTask task) async {
+ _ensureInitialized();
+ return await _channel.invokeMethod('open', {'task_id': task.id});
}
}
diff --git a/lib/src/models.dart b/lib/src/models.dart
deleted file mode 100644
index 4d08532c..00000000
--- a/lib/src/models.dart
+++ /dev/null
@@ -1,60 +0,0 @@
-///
-/// A class defines a set of possible statuses of a download task
-///
-class DownloadTaskStatus {
- final int _value;
-
- const DownloadTaskStatus(int value) : _value = value;
-
- int get value => _value;
-
- get hashCode => _value;
-
- operator ==(status) => status._value == this._value;
-
- toString() => 'DownloadTaskStatus($_value)';
-
- static DownloadTaskStatus from(int value) => DownloadTaskStatus(value);
-
- static const undefined = const DownloadTaskStatus(0);
- static const enqueued = const DownloadTaskStatus(1);
- static const running = const DownloadTaskStatus(2);
- static const complete = const DownloadTaskStatus(3);
- static const failed = const DownloadTaskStatus(4);
- static const canceled = const DownloadTaskStatus(5);
- static const paused = const DownloadTaskStatus(6);
-}
-
-///
-/// A model class encapsulates all task information according to data in Sqlite
-/// database.
-///
-/// * [taskId] the unique identifier of a download task
-/// * [status] the latest status of a download task
-/// * [progress] the latest progress value of a download task
-/// * [url] the download link
-/// * [filename] the local file name of a downloaded file
-/// * [savedDir] the absolute path of the directory where the downloaded file is saved
-///
-class DownloadTask {
- final String taskId;
- final DownloadTaskStatus status;
- final int progress;
- final String url;
- final String filename;
- final String savedDir;
- final int timeCreated;
-
- DownloadTask(
- {this.taskId,
- this.status,
- this.progress,
- this.url,
- this.filename,
- this.savedDir,
- this.timeCreated});
-
- @override
- String toString() =>
- "DownloadTask(taskId: $taskId, status: $status, progress: $progress, url: $url, filename: $filename, savedDir: $savedDir, timeCreated: $timeCreated)";
-}
diff --git a/lib/src/status.dart b/lib/src/status.dart
new file mode 100644
index 00000000..2537e574
--- /dev/null
+++ b/lib/src/status.dart
@@ -0,0 +1,50 @@
+part of 'downloader.dart';
+
+/// Possible statuses of a [DownloadTask].
+enum DownloadTaskStatus {
+ undefined,
+ enqueued,
+ running,
+ paused,
+ completed,
+ failed,
+ canceled,
+}
+
+extension _StatusByValue on DownloadTaskStatus {
+ static const _byIndex = [
+ DownloadTaskStatus.undefined, // Index 0
+ DownloadTaskStatus.enqueued, // Index 1
+ DownloadTaskStatus.running, // Index 2
+ DownloadTaskStatus.completed, // Index 3
+ DownloadTaskStatus.failed, // Index 4
+ DownloadTaskStatus.canceled, // Index 5
+ DownloadTaskStatus.paused, // Index 6
+ ];
+
+ static DownloadTaskStatus create(int value) => _byIndex[value];
+ int get value => _byIndex.indexOf(this);
+}
+
+extension StringableStatus on DownloadTaskStatus {
+ String toShortString() {
+ switch (this) {
+ case DownloadTaskStatus.undefined:
+ return 'undefined';
+ case DownloadTaskStatus.enqueued:
+ return 'enqueued';
+ case DownloadTaskStatus.running:
+ return 'running';
+ case DownloadTaskStatus.paused:
+ return 'paused';
+ case DownloadTaskStatus.completed:
+ return 'completed';
+ case DownloadTaskStatus.failed:
+ return 'failed';
+ case DownloadTaskStatus.canceled:
+ return 'canceled';
+ default:
+ throw UnimplementedError();
+ }
+ }
+}
diff --git a/lib/src/task.dart b/lib/src/task.dart
new file mode 100644
index 00000000..8a88ca7f
--- /dev/null
+++ b/lib/src/task.dart
@@ -0,0 +1,163 @@
+part of 'downloader.dart';
+
+/// Represents a single download.
+class DownloadTask {
+ DownloadTask._({
+ @required String id,
+ @required DownloadTaskStatus status,
+ @required double progress,
+ @required this.url,
+ @required File destination,
+ @required DateTime created,
+ }) : _id = id,
+ _status = status,
+ _progress = progress,
+ _destination = destination,
+ _updatesController = StreamController() {
+ _updates = _updatesController.stream.asBroadcastStream();
+ }
+
+ /// Uniquely identifies this [DownloadTask] among all other [DownloadTask]s
+ /// that were happing, are happening and will be happening during this
+ /// runtime of the app.
+ /// The [id] will change if you [pause] and [resume] this task or if you
+ /// [retry] this task after it [hasFailed] or [gotCanceled].
+ String get id => _id;
+ String _id;
+
+ /// The [status] of this task.
+ /// For more readable code, check out the [hasUndefinedStatus], [isEnqueued],
+ /// [isRunning], [isPaused], [isCompleted], [hasFailed], [gotCanceled]
+ /// getters.
+ DownloadTaskStatus get status => _status;
+ DownloadTaskStatus _status;
+ bool get hasUndefinedStatus => status == DownloadTaskStatus.undefined;
+ bool get isEnqueued => status == DownloadTaskStatus.enqueued;
+ bool get isRunning => status == DownloadTaskStatus.running;
+ bool get isPaused => status == DownloadTaskStatus.paused;
+ bool get isCompleted => status == DownloadTaskStatus.completed;
+ bool get hasFailed => status == DownloadTaskStatus.failed;
+ bool get gotCanceled => status == DownloadTaskStatus.canceled;
+
+ /// The progress of this task, where `0.0` refers to nothing being downloaded
+ /// yet and `1.0` refers to all data being downloaded.
+ double get progress => _progress;
+ double _progress;
+
+ /// The [url] that this task downloads from.
+ final String url;
+
+ /// The local file that this tasks downloads to.
+ File get destination => _destination;
+ File _destination;
+
+ /// A [Stream] that repeatedly emits [this] [DownloadTask] whenever some of
+ /// its fields change.
+ Stream get updates => _updates;
+ StreamController _updatesController;
+ Stream _updates;
+
+ /// When this task got created.
+ int get timeCreated => _timeCreated;
+ int _timeCreated;
+
+ // The following are functions and methods that interact with the
+ // [FlutterDownloader].
+
+ /// Creates a new [DownloadTask].
+ /// If the [destinationFileName] is not set, it will be extracted from HTTP headers
+ /// response or the [url].
+ /// Both [showNotification] and [openFileFromNotification] only work on
+ /// Android.
+ static Future create({
+ @required String url,
+ @required Directory downloadDirectory,
+ String destinationFileName,
+ Map httpHeaders,
+ bool showNotification = true,
+ bool openFileFromNotification = true,
+ bool requiresStorageNotLow = true,
+ }) =>
+ FlutterDownloader._enqueue(
+ url: url,
+ downloadDirectory: downloadDirectory,
+ fileName: destinationFileName,
+ headers: httpHeaders,
+ showNotification: showNotification,
+ openFileFromNotification: openFileFromNotification,
+ requiresStorageNotLow: requiresStorageNotLow,
+ );
+
+ DownloadTask._fromQueryResult(dynamic result)
+ : this._(
+ id: result['task_id'] as String,
+ status: _StatusByValue.create(result['status']),
+ progress: (result['progress'] as int).toDouble() * 0.01,
+ url: result['url'] as String,
+ destination: _fileFromDirAndName(
+ result['saved_dir'] as String, result['file_name'] as String),
+ created: DateTime.fromMillisecondsSinceEpoch(result['time_created']),
+ );
+
+ void _merge(DownloadTask other) {
+ assert(url == other.url);
+
+ _update(other.id, other.status, other.progress, other.destination);
+ }
+
+ void _update(
+ String id, DownloadTaskStatus status, double progress, File destination) {
+ if (_id != id ||
+ _status != status ||
+ _progress != progress ||
+ _destination != destination) {
+ _id = id;
+ _status = status;
+ _progress = progress;
+ _destination = destination;
+
+ _updatesController.add(this);
+ }
+ }
+
+ @override
+ String toString() => 'DownloadTask #$id (${status.toShortString()} – '
+ '${(progress * 100.0).toStringAsFixed(0)} %): $url -> ${destination.path}';
+
+ Future pause() => FlutterDownloader._pause(this);
+ Future resume({bool requiresStorageNotLow = true}) =>
+ FlutterDownloader._resume(this,
+ requiresStorageNotLow: requiresStorageNotLow);
+
+ Future cancel() => FlutterDownloader._cancel(this);
+ Future retry({bool requiresStorageNotLow = true}) =>
+ FlutterDownloader._retry(this,
+ requiresStorageNotLow: requiresStorageNotLow);
+
+ /// Removes the task.
+ /// If the task is running, it's cancelled.
+ /// If the task is completed and and [removeContent] is set to `true`, the
+ /// downloaded file will be deleted.
+ Future remove({bool removeContent = false}) =>
+ FlutterDownloader._remove(this, removeContent: removeContent);
+
+ /// Opens and previews a downloaded file.
+ /// Returns `true` if the downloaded file can be opened on the current device,
+ /// `false` otherwise.
+ ///
+ /// **Note:**
+ ///
+ /// To succeed on Android, these two requirements need to be met:
+ /// - The file has to be saved in external storage where other applications
+ /// have permission to read this file.
+ /// - The current device has at least an application that can read the file
+ /// type of the file.
+ Future openFile() => FlutterDownloader._openFile(this);
+
+ Future wait() async {
+ await updates.firstWhere(
+ (task) => task.isCompleted || task.gotCanceled || task.hasFailed,
+ orElse: () => null,
+ );
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 5e65bae8..429f9804 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,6 +1,6 @@
name: flutter_downloader
description: A plugin for creating and managing download tasks. Supports iOS and Android.
-version: 1.5.0
+version: 2.0.0
homepage: https://github.com/fluttercommunity/flutter_downloader
maintainer: Hung Duy Ha (@hnvn)
@@ -14,7 +14,7 @@ flutter:
pluginClass: FlutterDownloaderPlugin
environment:
- sdk: '>=1.20.1 <3.0.0'
+ sdk: ">=2.7.0 <3.0.0"
flutter: ^1.12.13
dependencies: