mangayomi/lib/services/background_downloader/src/file_downloader.dart
2024-08-05 15:44:59 +01:00

966 lines
44 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:mangayomi/services/background_downloader/background_downloader.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'base_downloader.dart';
import 'localstore/localstore.dart';
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader_http_client.dart';
import 'package:mangayomi/services/background_downloader/src/desktop/desktop_downloader_native_http_client.dart.dart'
as desktop_downloader_native;
/// Provides access to all functions of the plugin in a single place.
interface class FileDownloader {
static FileDownloader? _singleton;
/// If no group is specified the default group name will be used
static const defaultGroup = 'default';
/// Special group name for tasks that download a chunk, as part of a
/// [ParallelDownloadTask]
static String get chunkGroup => BaseDownloader.chunkGroup;
/// Database where tracked tasks are stored.
///
/// Activate tracking by calling [trackTasks], and access the records in the
/// database via this [database] object.
late final Database database;
late final BaseDownloader _downloader;
/// Do not use: for testing only
@visibleForTesting
BaseDownloader get downloaderForTesting => _downloader;
factory FileDownloader(
{PersistentStorage? persistentStorage,
bool useNativeHttpClient = false}) {
assert(
_singleton == null || persistentStorage == null,
'You can only supply a persistentStorage on the very first call to '
'FileDownloader()');
_singleton ??= FileDownloader._internal(
persistentStorage ?? LocalStorePersistentStorage(),
useNativeHttpClient);
return _singleton!;
}
FileDownloader._internal(
PersistentStorage persistentStorage, bool useNativeHttpClient) {
database = Database(persistentStorage);
_downloader = BaseDownloader.instance(
persistentStorage, database, useNativeHttpClient);
}
/// True when initialization is complete and downloader ready for use
Future<bool> get ready => _downloader.ready;
/// Stream of [TaskUpdate] updates for downloads that do
/// not have a registered callback
Stream<TaskUpdate> get updates => _downloader.updates.stream;
/// Configures the downloader
///
/// Configuration is either a single configItem or a list of configItems.
/// Each configItem is a (String, dynamic) where the String is the config
/// type and 'dynamic' can be any appropriate parameter, including another Record.
/// [globalConfig] is routed to every platform, whereas the platform specific
/// ones only get routed to that platform, after the global configs have
/// completed.
/// If a config type appears more than once, they will all be executed in order,
/// with [globalConfig] executed before the platform-specific config.
///
/// Returns a list of (String, String) which is the config type and a response
/// which is empty if OK, 'not implemented' if the item could not be recognized and
/// processed, or may contain other error/warning information
///
/// Please see [CONFIG.md](https://github.com/781flyingdutchman/background_downloader/blob/main/CONFIG.md)
/// for more information
Future<List<(String, String)>> configure(
{dynamic globalConfig,
dynamic androidConfig,
dynamic iOSConfig,
dynamic desktopConfig}) =>
_downloader.configure(
globalConfig: globalConfig,
androidConfig: androidConfig,
iOSConfig: iOSConfig,
desktopConfig: desktopConfig);
/// Register status or progress callbacks to monitor download progress, and
/// [TaskNotificationTapCallback] to respond to user tapping a notification.
///
/// Status callbacks are called only when the state changes, while
/// progress callbacks are called to inform of intermediate progress.
///
/// Note that callbacks will be called based on a task's [updates]
/// property, which defaults to status change callbacks only. To also get
/// progress updates make sure to register a [TaskProgressCallback] and
/// set the task's [updates] property to [Updates.progress] or
/// [Updates.statusAndProgress].
///
/// For notification callbacks, make sure your AndroidManifest includes
/// android:launchMode="singleTask" to ensure proper behavior when a
/// notification is tapped.
///
/// Different callbacks can be set for different groups, and the group
/// can be passed on with the [Task] to ensure the
/// appropriate callbacks are called for that group.
/// For the `taskNotificationTapCallback` callback, the `defaultGroup` callback
/// is used when calling 'convenience' functions like `FileDownloader().download`
///
/// The call returns the [FileDownloader] to make chaining easier
FileDownloader registerCallbacks(
{String group = defaultGroup,
TaskStatusCallback? taskStatusCallback,
TaskProgressCallback? taskProgressCallback,
TaskNotificationTapCallback? taskNotificationTapCallback}) {
assert(
taskStatusCallback != null ||
taskProgressCallback != null ||
taskNotificationTapCallback != null,
'Must provide at least one callback');
if (taskStatusCallback != null) {
_downloader.groupStatusCallbacks[group] = taskStatusCallback;
}
if (taskProgressCallback != null) {
_downloader.groupProgressCallbacks[group] = taskProgressCallback;
}
if (taskNotificationTapCallback != null) {
_downloader.groupNotificationTapCallbacks[group] =
taskNotificationTapCallback;
}
return this;
}
/// Unregister a previously registered [TaskStatusCallback], [TaskProgressCallback]
/// or [TaskNotificationTapCallback].
///
/// [group] defaults to the [FileDownloader.defaultGroup]
/// If [callback] is null, all callbacks for the [group] are unregistered
FileDownloader unregisterCallbacks(
{String group = defaultGroup, Function? callback}) {
if (callback != null) {
// remove specific callback
if (_downloader.groupStatusCallbacks[group] == callback) {
_downloader.groupStatusCallbacks.remove(group);
}
if (_downloader.groupProgressCallbacks[group] == callback) {
_downloader.groupProgressCallbacks.remove(group);
}
if (_downloader.groupNotificationTapCallbacks[group] == callback) {
_downloader.groupNotificationTapCallbacks.remove(group);
}
} else {
// remove all callbacks related to group
_downloader.groupStatusCallbacks.remove(group);
_downloader.groupProgressCallbacks.remove(group);
_downloader.groupNotificationTapCallbacks.remove(group);
}
return this;
}
/// Adds the [taskQueue] to this downloader
///
/// Every [TaskQueue] will receive [TaskQueue.taskFinished] for
/// every task that has reached a final state
void addTaskQueue(TaskQueue taskQueue) =>
_downloader.taskQueues.add(taskQueue);
/// Removes [taskQueue] and return true if successful
bool removeTaskQueue(TaskQueue taskQueue) =>
_downloader.taskQueues.remove(taskQueue);
/// List of connected [TaskQueue]s
List<TaskQueue> get taskQueues => _downloader.taskQueues;
/// Enqueue a new [Task]
///
/// Returns true if successfully enqueued. A new task will also generate
/// a [TaskStatus.enqueued] update to the registered callback,
/// if requested by its [updates] property
///
/// Use [enqueue] instead of the convenience functions (like
/// [download] and [upload]) if:
/// - your download/upload is likely to take long and may require
/// running in the background
/// - you want to monitor tasks centrally, via a listener
/// - you want more detailed progress information
/// (e.g. file size, network speed, time remaining)
Future<bool> enqueue(Task task) => _downloader.enqueue(task);
/// Download a file and return the final [TaskStatusUpdate]
///
/// Different from [enqueue], this method returns a [Future] that completes
/// when the file has been downloaded, or an error has occurred.
/// While it uses the same download mechanism as [enqueue],
/// and will execute the download also when
/// the app moves to the background, it is meant for downloads that are
/// awaited while the app is in the foreground.
///
/// Optional callbacks for status and progress updates may be
/// added. These function only take a [TaskStatus] or [double] argument as
/// the task they refer to is expected to be captured in the closure for
/// this call.
/// For example `Downloader.download(task, onStatus: (status) =>`
/// `print('Status for ${task.taskId} is $status);`
///
/// An optional callback [onElapsedTime] will be called at regular intervals
/// (defined by [elapsedTimeInterval], which defaults to 5 seconds) with a
/// single argument that is the elapsed time since the call to [download].
/// This can be used to trigger UI warnings (e.g. 'this is taking rather long')
/// or to cancel the task if it does not complete within a desired time.
/// For performance reasons the [elapsedTimeInterval] should not be set to
/// a value less than one second.
/// The [onElapsedTime] callback should not be used to indicate progress. For
/// that, use the [onProgress] callback.
///
/// Use [enqueue] instead of [download] if:
/// - your download/upload is likely to take long and may require
/// running in the background
/// - you want to monitor tasks centrally, via a listener
/// - you want more detailed progress information
/// (e.g. file size, network speed, time remaining)
Future<TaskStatusUpdate> download(DownloadTask task,
{void Function(TaskStatus)? onStatus,
void Function(double)? onProgress,
void Function(Duration)? onElapsedTime,
Duration? elapsedTimeInterval}) =>
_downloader.enqueueAndAwait(task,
onStatus: onStatus,
onProgress: onProgress,
onElapsedTime: onElapsedTime,
elapsedTimeInterval: elapsedTimeInterval);
/// Upload a file and return the final [TaskStatusUpdate]
///
/// Different from [enqueue], this method returns a [Future] that completes
/// when the file has been uploaded, or an error has occurred.
/// While it uses the same upload mechanism as [enqueue],
/// and will execute the upload also when
/// the app moves to the background, it is meant for uploads that are
/// awaited while the app is in the foreground.
///
/// Optional callbacks for status and progress updates may be
/// added. These function only take a [TaskStatus] or [double] argument as
/// the task they refer to is expected to be captured in the closure for
/// this call.
/// For example `Downloader.upload(task, onStatus: (status) =>`
/// `print('Status for ${task.taskId} is $status);`
///
/// An optional callback [onElapsedTime] will be called at regular intervals
/// (defined by [elapsedTimeInterval], which defaults to 5 seconds) with a
/// single argument that is the elapsed time since the call to [upload].
/// This can be used to trigger UI warnings (e.g. 'this is taking rather long')
/// or to cancel the task if it does not complete within a desired time.
/// For performance reasons the [elapsedTimeInterval] should not be set to
/// a value less than one second.
/// The [onElapsedTime] callback should not be used to indicate progress. For
/// that, use the [onProgress] callback.
///
/// Note that the task's [group] is ignored and will be replaced with an
/// internal group name 'await' to track status
///
/// Use [enqueue] instead of [upload] if:
/// - your download/upload is likely to take long and may require
/// running in the background
/// - you want to monitor tasks centrally, via a listener
/// - you want more detailed progress information
/// (e.g. file size, network speed, time remaining)
Future<TaskStatusUpdate> upload(UploadTask task,
{void Function(TaskStatus)? onStatus,
void Function(double)? onProgress,
void Function(Duration)? onElapsedTime,
Duration? elapsedTimeInterval}) =>
_downloader.enqueueAndAwait(task,
onStatus: onStatus,
onProgress: onProgress,
onElapsedTime: onElapsedTime,
elapsedTimeInterval: elapsedTimeInterval);
/// Transmit data in the [DataTask] and receive the response
///
/// Different from [enqueue], this method returns a [Future] that completes
/// when the [DataTask] has completed, or an error has occurred.
/// While it uses the same mechanism as [enqueue],
/// and will execute the task also when
/// the app moves to the background, it is meant for data tasks that are
/// awaited while the app is in the foreground.
///
/// [onStatus] is an optional callback for status updates
///
/// An optional callback [onElapsedTime] will be called at regular intervals
/// (defined by [elapsedTimeInterval], which defaults to 5 seconds) with a
/// single argument that is the elapsed time since the call to [transmit].
/// This can be used to trigger UI warnings (e.g. 'this is taking rather long')
/// For performance reasons the [elapsedTimeInterval] should not be set to
/// a value less than one second.
Future<TaskStatusUpdate> transmit(DataTask task,
{void Function(TaskStatus)? onStatus,
void Function(Duration)? onElapsedTime,
Duration? elapsedTimeInterval}) =>
_downloader.enqueueAndAwait(task,
onStatus: onStatus,
onElapsedTime: onElapsedTime,
elapsedTimeInterval: elapsedTimeInterval);
/// Enqueues a list of files to download and returns when all downloads
/// have finished (successfully or otherwise). The returned value is a
/// [Batch] object that contains the original [tasks], the
/// [results] and convenience getters to filter successful and failed results.
///
/// If an optional [batchProgressCallback] function is provided, it will be
/// called upon completion (successfully or otherwise) of each task in the
/// batch, with two parameters: the number of succeeded and the number of
/// failed tasks. The callback can be used, for instance, to show a progress
/// indicator for the batch, where
/// double percent_complete = (succeeded + failed) / tasks.length
///
/// To also monitor status and/or progress for each task in the batch, provide
/// a [taskStatusCallback] and/or [taskProgressCallback], which will be used
/// for each task in the batch.
///
/// An optional callback [onElapsedTime] will be called at regular intervals
/// (defined by [elapsedTimeInterval], which defaults to 5 seconds) with a
/// single argument that is the elapsed time since the call to [downloadBatch].
/// This can be used to trigger UI warnings (e.g. 'this is taking rather long')
/// or to cancel the task if it does not complete within a desired time.
/// For performance reasons the [elapsedTimeInterval] should not be set to
/// a value less than one second.
/// The [onElapsedTime] callback should not be used to indicate progress.
///
/// Note that to allow for special processing of tasks in a batch, the task's
/// [Task.group] and [Task.updates] value will be modified when enqueued, and
/// those modified tasks are returned as part of the [Batch]
/// object.
Future<Batch> downloadBatch(final List<DownloadTask> tasks,
{BatchProgressCallback? batchProgressCallback,
TaskStatusCallback? taskStatusCallback,
TaskProgressCallback? taskProgressCallback,
void Function(Duration)? onElapsedTime,
Duration? elapsedTimeInterval}) =>
_downloader.enqueueAndAwaitBatch(tasks,
batchProgressCallback: batchProgressCallback,
taskStatusCallback: taskStatusCallback,
taskProgressCallback: taskProgressCallback,
onElapsedTime: onElapsedTime,
elapsedTimeInterval: elapsedTimeInterval);
/// Enqueues a list of files to upload and returns when all uploads
/// have finished (successfully or otherwise). The returned value is a
/// [Batch] object that contains the original [tasks], the
/// [results] and convenience getters to filter successful and failed results.
///
/// If an optional [batchProgressCallback] function is provided, it will be
/// called upon completion (successfully or otherwise) of each task in the
/// batch, with two parameters: the number of succeeded and the number of
/// failed tasks. The callback can be used, for instance, to show a progress
/// indicator for the batch, where
/// double percent_complete = (succeeded + failed) / tasks.length
///
/// To also monitor status and/or progress for each task in the batch, provide
/// a [taskStatusCallback] and/or [taskProgressCallback], which will be used
/// for each task in the batch.
///
/// An optional callback [onElapsedTime] will be called at regular intervals
/// (defined by [elapsedTimeInterval], which defaults to 5 seconds) with a
/// single argument that is the elapsed time since the call to [uploadBatch].
/// This can be used to trigger UI warnings (e.g. 'this is taking rather long')
/// or to cancel the task if it does not complete within a desired time.
/// For performance reasons the [elapsedTimeInterval] should not be set to
/// a value less than one second.
/// The [onElapsedTime] callback should not be used to indicate progress.
///
/// Note that to allow for special processing of tasks in a batch, the task's
/// [Task.group] and [Task.updates] value will be modified when enqueued, and
/// those modified tasks are returned as part of the [Batch]
/// object.
Future<Batch> uploadBatch(final List<UploadTask> tasks,
{BatchProgressCallback? batchProgressCallback,
TaskStatusCallback? taskStatusCallback,
TaskProgressCallback? taskProgressCallback,
void Function(Duration)? onElapsedTime,
Duration? elapsedTimeInterval}) =>
_downloader.enqueueAndAwaitBatch(tasks,
batchProgressCallback: batchProgressCallback,
taskStatusCallback: taskStatusCallback,
taskProgressCallback: taskProgressCallback,
onElapsedTime: onElapsedTime,
elapsedTimeInterval: elapsedTimeInterval);
/// Resets the downloader by cancelling all ongoing tasks within
/// the provided [group]
///
/// Returns the number of tasks cancelled. Every canceled task wil emit a
/// [TaskStatus.canceled] update to the registered callback, if
/// requested
///
/// This method acts on a [group] of tasks. If omitted, the [defaultGroup]
/// is used, which is the group used when you [enqueue] a task
Future<int> reset({String group = defaultGroup}) => _downloader.reset(group);
/// Returns a list of taskIds of all tasks currently active in this [group]
///
/// Active means enqueued or running, and if [includeTasksWaitingToRetry] is
/// true also tasks that are waiting to be retried
///
/// This method acts on a [group] of tasks. If omitted, the [defaultGroup]
/// is used, which is the group used when you [enqueue] a task
Future<List<String>> allTaskIds(
{String group = defaultGroup,
bool includeTasksWaitingToRetry = true}) async =>
(await allTasks(
group: group,
includeTasksWaitingToRetry: includeTasksWaitingToRetry))
.map((task) => task.taskId)
.toList();
/// Returns a list of all tasks currently active in this [group]
///
/// Active means enqueued or running, and if [includeTasksWaitingToRetry] is
/// true also tasks that are waiting to be retried
///
/// This method acts on a [group] of tasks. If omitted, the [defaultGroup]
/// is used, which is the group used when you [enqueue] a task.
Future<List<Task>> allTasks(
{String group = defaultGroup,
bool includeTasksWaitingToRetry = true}) =>
_downloader.allTasks(group, includeTasksWaitingToRetry);
/// Returns true if tasks in this [group] are finished
///
/// Finished means "not active", i.e. no tasks are enqueued or running,
/// and if [includeTasksWaitingToRetry] is true (the default), no tasks are
/// waiting to be retried.
/// Finished does not mean that all tasks completed successfully.
///
/// This method acts on a [group] of tasks. If omitted, the [defaultGroup]
/// is used, which is the group used when you [enqueue] a task.
///
/// If an [ignoreTask] is provided, it will be excluded from the test. This
/// allows you to test for [tasksFinished] within the status update callback
/// for a task that just finished. In that situation, that task may still
/// be returned by the platform as 'active', but you already know it is not.
/// Calling [tasksFinished] while passing that just-finished task will ensure
/// a proper test in that situation.
Future<bool> tasksFinished(
{String group = defaultGroup,
bool includeTasksWaitingToRetry = true,
String? ignoreTaskId}) async {
final tasksInProgress = await allTasks(
group: group, includeTasksWaitingToRetry: includeTasksWaitingToRetry);
if (ignoreTaskId != null) {
tasksInProgress.removeWhere((task) => task.taskId == ignoreTaskId);
}
return tasksInProgress.isEmpty;
}
/// Cancel all tasks matching the taskIds in the list
///
/// Every canceled task wil emit a [TaskStatus.canceled] update to
/// the registered callback, if requested
Future<bool> cancelTasksWithIds(List<String> taskIds) =>
_downloader.cancelTasksWithIds(taskIds);
/// Cancel this task
///
/// The task will emit a [TaskStatus.canceled] update to
/// the registered callback, if requested
Future<bool> cancelTaskWithId(String taskId) => cancelTasksWithIds([taskId]);
/// Return [Task] for the given [taskId], or null
/// if not found.
///
/// Only running tasks are guaranteed to be returned, but returning a task
/// does not guarantee that the task is still running. To keep track of
/// the status of tasks, use a [TaskStatusCallback]
Future<Task?> taskForId(String taskId) => _downloader.taskForId(taskId);
/// Activate tracking for tasks in this [group]
///
/// All subsequent tasks in this group will be recorded in persistent storage.
/// Use the [FileDownloader.database] to get or remove [TaskRecord] objects,
/// which contain a [Task], its [TaskStatus] and a [double] for progress.
///
/// If [markDownloadedComplete] is true (default) then all tasks in the
/// database that are marked as not yet [TaskStatus.complete] will be set to
/// [TaskStatus.complete] if the target file for that task exists.
/// They will also emit [TaskStatus.complete] and [progressComplete] to
/// their registered listener or callback.
/// This is a convenient way to capture downloads that have completed while
/// the app was suspended: on app startup, immediately register your
/// listener or callbacks, and call [trackTasks] for each group.
///
/// Returns the [FileDownloader] for easy chaining
Future<FileDownloader> trackTasksInGroup(String group,
{bool markDownloadedComplete = true}) async {
await _downloader.trackTasks(group, markDownloadedComplete);
return this;
}
/// Activate tracking for all tasks
///
/// All subsequent tasks will be recorded in persistent storage.
/// Use the [FileDownloader.database] to get or remove [TaskRecord] objects,
/// which contain a [Task], its [TaskStatus] and a [double] for progress.
///
/// If [markDownloadedComplete] is true (default) then all tasks in the
/// database that are marked as not yet [TaskStatus.complete] will be set to
/// [TaskStatus.complete] if the target file for that task exists.
/// They will also emit [TaskStatus.complete] and [progressComplete] to
/// their registered listener or callback.
/// This is a convenient way to capture downloads that have completed while
/// the app was suspended: on app startup, immediately register your
/// listener or callbacks, and call [trackTasks].
///
/// Returns the [FileDownloader] for easy chaining
Future<FileDownloader> trackTasks(
{bool markDownloadedComplete = true}) async {
await _downloader.trackTasks(null, markDownloadedComplete);
return this;
}
/// Wakes up the FileDownloader from possible background state, triggering
/// a stream of updates that may have been processed while in the background,
/// and have not yet reached the callbacks or listener
///
/// Calling this method multiple times has no effect.
Future<void> resumeFromBackground() =>
_downloader.retrieveLocallyStoredData();
/// Returns true if task can be resumed on pause
///
/// This future only completes once the task is running and has received
/// information from the server to determine whether resume is possible, or
/// if the task fails and resume is possible
Future<bool> taskCanResume(Task task) => _downloader.taskCanResume(task);
/// Pause the task
///
/// Returns true if the pause was attempted successfully. Test the task's
/// status to see if it was executed successfully [TaskStatus.paused] or if
/// it failed after all [TaskStatus.failed]
///
/// If the [Task.allowPause] field is set to false (default) or if this is
/// a POST request, this method returns false immediately.
Future<bool> pause(DownloadTask task) async {
if (task.allowPause && task.post == null) {
return _downloader.pause(task);
}
return false;
}
/// Resume the task
///
/// If no resume data is available for this task, the call to [resume]
/// will return false and the task is not resumed.
/// If resume data is available, the call to [resume] will return true,
/// but this does not guarantee that resuming is actually possible, just that
/// the task is now enqueued for resume.
/// If the task is able to resume, it will, otherwise it will restart the
/// task from scratch, or fail.
Future<bool> resume(DownloadTask task) => _downloader.resume(task);
/// Set WiFi requirement globally, based on [requirement].
///
/// Affects future tasks and reschedules enqueued, inactive tasks
/// with the new setting.
/// Reschedules running tasks if [rescheduleRunningTasks] is true,
/// otherwise leaves those running with their prior setting
Future<bool> requireWiFi(RequireWiFi requirement,
{final rescheduleRunningTasks = true}) =>
_downloader.requireWiFi(requirement, rescheduleRunningTasks);
/// Returns the current global setting for requiring WiFi
Future<RequireWiFi> getRequireWiFiSetting() =>
_downloader.getRequireWiFiSetting();
/// Configure notification for a single task
///
/// The configuration determines what notifications are shown,
/// whether a progress bar is shown (Android only), and whether tapping
/// the 'complete' notification opens the downloaded file.
///
/// [running] is the notification used while the task is in progress
/// [complete] is the notification used when the task completed
/// [error] is the notification used when something went wrong,
/// including pause, failed and notFound status
/// [progressBar] if set will show a progress bar
/// [tapOpensFile] if set will attempt to open the file when the [complete]
/// notification is tapped
/// [groupNotificationId] if set will group all notifications with the same
/// [groupNotificationId] and change the progress bar to number of finished
/// tasks versus total number of tasks in the [groupNotificationId].
/// Use {numFinished} and {numTotal} tokens in the [TaskNotification.title]
/// and [TaskNotification.body] to substitute. Task-specific substitutions
/// such as {filename} are not valid when using [groupNotificationId].
/// The [groupNotificationId] is considered [complete] when there are no
/// more tasks running within that group, and at that point the
/// [complete] notification is shown (if configured). If any task in the
/// [groupNotificationId] fails, the [error] notification is shown.
/// The first character of the [groupNotificationId] cannot be '*'.
///
/// The [TaskNotification] is the actual notification shown for a [Task], and
/// [body] and [title] may contain special strings to substitute display values:
/// {filename} to insert the [Task.filename]
/// {metaData} to insert the [Task.metaData]
/// {displayName} to insert the [Task.displayName]
/// {progress} to insert progress in %
/// {networkSpeed} to insert the network speed in MB/s or kB/s, or '--' if N/A
/// {timeRemaining} to insert the estimated time remaining to complete the task
/// in HH:MM:SS or MM:SS or --:-- if N/A
/// {numFinished} to insert the number of finished tasks in a groupNotification
/// {numFailed} to insert the number of failed tasks in a groupNotification
/// {numTotal} to insert the number of tasks in a groupNotification
///
/// Actual appearance of notification is dependent on the platform, e.g.
/// on iOS {progress} is not available and ignored (except for groupNotifications)
///
/// Returns the [FileDownloader] for easy chaining
FileDownloader configureNotificationForTask(Task task,
{TaskNotification? running,
TaskNotification? complete,
TaskNotification? error,
TaskNotification? paused,
bool progressBar = false,
bool tapOpensFile = false,
String groupNotificationId = ''}) {
_downloader.notificationConfigs.add(TaskNotificationConfig(
taskOrGroup: task,
running: running,
complete: complete,
error: error,
paused: paused,
progressBar: progressBar,
tapOpensFile: tapOpensFile,
groupNotificationId: groupNotificationId));
return this;
}
/// Configure notification for a group of tasks
///
/// The configuration determines what notifications are shown,
/// whether a progress bar is shown (Android only), and whether tapping
/// the 'complete' notification opens the downloaded file.
///
/// [running] is the notification used while the task is in progress
/// [complete] is the notification used when the task completed
/// [error] is the notification used when something went wrong,
/// including pause, failed and notFound status
/// [progressBar] if set will show a progress bar
/// [tapOpensFile] if set will attempt to open the file when the [complete]
/// notification is tapped
/// [groupNotificationId] if set will group all notifications with the same
/// [groupNotificationId] and change the progress bar to number of finished
/// tasks versus total number of tasks in the [groupNotificationId].
/// Use {numFinished} and {numTotal} tokens in the [TaskNotification.title]
/// and [TaskNotification.body] to substitute. Task-specific substitutions
/// such as {filename} are not valid when using [groupNotificationId].
/// The [groupNotificationId] is considered [complete] when there are no
/// more tasks running within that group, and at that point the
/// [complete] notification is shown (if configured). If any task in the
/// [groupNotificationId] fails, the [error] notification is shown.
/// The first character of the [groupNotificationId] cannot be '*'.
///
/// The [TaskNotification] is the actual notification shown for a [Task], and
/// [body] and [title] may contain special strings to substitute display values:
/// {filename} to insert the [Task.filename]
/// {metaData} to insert the [Task.metaData]
/// {displayName} to insert the [Task.displayName]
/// {progress} to insert progress in %
/// {networkSpeed} to insert the network speed in MB/s or kB/s, or '--' if N/A
/// {timeRemaining} to insert the estimated time remaining to complete the task
/// in HH:MM:SS or MM:SS or --:-- if N/A
/// {numFinished} to insert the number of finished tasks in a groupNotification
/// {numFailed} to insert the number of failed tasks in a groupNotification
/// {numTotal} to insert the number of tasks in a groupNotification
///
/// Actual appearance of notification is dependent on the platform, e.g.
/// on iOS {progress} is not available and ignored (except for groupNotifications)
///
/// Returns the [FileDownloader] for easy chaining
FileDownloader configureNotificationForGroup(String group,
{TaskNotification? running,
TaskNotification? complete,
TaskNotification? error,
TaskNotification? paused,
bool progressBar = false,
bool tapOpensFile = false,
String groupNotificationId = ''}) {
_downloader.notificationConfigs.add(TaskNotificationConfig(
taskOrGroup: group,
running: running,
complete: complete,
error: error,
paused: paused,
progressBar: progressBar,
tapOpensFile: tapOpensFile,
groupNotificationId: groupNotificationId));
return this;
}
/// Configure default task notification
///
/// The configuration determines what notifications are shown,
/// whether a progress bar is shown (Android only), and whether tapping
/// the 'complete' notification opens the downloaded file.
///
/// [running] is the notification used while the task is in progress
/// [complete] is the notification used when the task completed
/// [error] is the notification used when something went wrong,
/// including pause, failed and notFound status
/// [progressBar] if set will show a progress bar
/// [tapOpensFile] if set will attempt to open the file when the [complete]
/// notification is tapped
/// [groupNotificationId] if set will group all notifications with the same
/// [groupNotificationId] and change the progress bar to number of finished
/// tasks versus total number of tasks in the [groupNotificationId].
/// Use {numFinished} and {numTotal} tokens in the [TaskNotification.title]
/// and [TaskNotification.body] to substitute. Task-specific substitutions
/// such as {filename} are not valid when using [groupNotificationId].
/// The [groupNotificationId] is considered [complete] when there are no
/// more tasks running within that group, and at that point the
/// [complete] notification is shown (if configured). If any task in the
/// [groupNotificationId] fails, the [error] notification is shown.
/// The first character of the [groupNotificationId] cannot be '*'.
///
/// The [TaskNotification] is the actual notification shown for a [Task], and
/// [body] and [title] may contain special strings to substitute display values:
/// {filename} to insert the [Task.filename]
/// {metaData} to insert the [Task.metaData]
/// {displayName} to insert the [Task.displayName]
/// {progress} to insert progress in %
/// {networkSpeed} to insert the network speed in MB/s or kB/s, or '--' if N/A
/// {timeRemaining} to insert the estimated time remaining to complete the task
/// in HH:MM:SS or MM:SS or --:-- if N/A
/// {numFinished} to insert the number of finished tasks in a groupNotification
/// {numFailed} to insert the number of failed tasks in a groupNotification
/// {numTotal} to insert the number of tasks in a groupNotification
///
/// Actual appearance of notification is dependent on the platform, e.g.
/// on iOS {progress} is not available and ignored (except for groupNotifications)
///
/// Returns the [FileDownloader] for easy chaining
FileDownloader configureNotification(
{TaskNotification? running,
TaskNotification? complete,
TaskNotification? error,
TaskNotification? paused,
bool progressBar = false,
bool tapOpensFile = false,
String groupNotificationId = ''}) {
_downloader.notificationConfigs.add(TaskNotificationConfig(
taskOrGroup: null,
running: running,
complete: complete,
error: error,
paused: paused,
progressBar: progressBar,
tapOpensFile: tapOpensFile,
groupNotificationId: groupNotificationId));
return this;
}
/// Perform a server request for this [request]
///
/// A server request returns an [http.Response] object that includes
/// the [body] as String, the [bodyBytes] as [UInt8List] and the [json]
/// representation if available.
/// It also contains the [statusCode] and [reasonPhrase] that may indicate
/// an error, and several other fields that may be useful.
/// A local error (e.g. a SocketException) will yield [statusCode] 499, with
/// details in the [reasonPhrase]
///
/// The request will abide by the [retries] set on the [request], and set
/// [headers] included in the [request]
///
/// The [http.Client] object used for this request is the [httpClient] field of
/// the downloader. If not set, the default [http.Client] will be used.
/// The request is executed on an Isolate, to ensure minimal interference
/// with the main Isolate
Future<http.Response> request(Request request, bool useNativeHttpClient) {
if (useNativeHttpClient) {
return compute(_doRequest, (
request,
desktop_downloader_native
.DesktopDownloaderNativeHttpClient.requestTimeout,
desktop_downloader_native.DesktopDownloaderNativeHttpClient.proxy,
desktop_downloader_native
.DesktopDownloaderNativeHttpClient.bypassTLSCertificateValidation,
true
));
}
return compute(_doRequest, (
request,
DesktopDownloaderHttpClient.requestTimeout,
DesktopDownloaderHttpClient.proxy,
DesktopDownloaderHttpClient.bypassTLSCertificateValidation,
false
));
}
/// Move the file represented by the [task] to a shared storage
/// [destination] and potentially a [directory] within that destination. If
/// the [mimeType] is not provided we will attempt to derive it from the
/// [Task.filePath] extension
///
/// Returns the path to the stored file, or null if not successful
///
/// NOTE: on iOS, using [destination] [SharedStorage.images] or
/// [SharedStorage.video] adds the photo or video file to the Photos
/// library. This requires the user to grant permission, and requires the
/// "NSPhotoLibraryAddUsageDescription" key to be set in Info.plist. The
/// returned value is NOT a filePath but an identifier. If the full filepath
/// is required, follow the [moveToSharedStorage] call with a call to
/// [pathInSharedStorage], passing the identifier obtained from the call
/// to [moveToSharedStorage] as the filePath parameter. This requires the user to
/// grant additional permissions, and requires the "NSPhotoLibraryUsageDescription"
/// key to be set in Info.plist. The returned value is the actual file path
/// of the photo or video in the Photos Library.
///
/// Platform-dependent, not consistent across all platforms
Future<String?> moveToSharedStorage(
DownloadTask task,
SharedStorage destination, {
String directory = '',
String? mimeType,
}) async =>
moveFileToSharedStorage(await task.filePath(), destination,
directory: directory, mimeType: mimeType);
/// Move the file represented by [filePath] to a shared storage
/// [destination] and potentially a [directory] within that destination. If
/// the [mimeType] is not provided we will attempt to derive it from the
/// [filePath] extension
///
/// Returns the path to the stored file, or null if not successful
/// NOTE: on iOS, using [destination] [SharedStorage.images] or
/// [SharedStorage.video] adds the photo or video file to the Photos
/// library. This requires the user to grant permission, and requires the
/// "NSPhotoLibraryAddUsageDescription" key to be set in Info.plist. The
/// returned value is NOT a filePath but an identifier. If the full filepath
/// is required, follow the [moveToSharedStorage] call with a call to
/// [pathInSharedStorage], passing the identifier obtained from the call
/// to [moveToSharedStorage] as the filePath parameter. This requires the user to
/// grant additional permissions, and requires the "NSPhotoLibraryUsageDescription"
/// key to be set in Info.plist. The returned value is the actual file path
/// of the photo or video in the Photos Library.
///
/// Platform-dependent, not consistent across all platforms
Future<String?> moveFileToSharedStorage(
String filePath,
SharedStorage destination, {
String directory = '',
String? mimeType,
}) async =>
_downloader.moveToSharedStorage(
filePath, destination, directory, mimeType);
/// Returns the filePath to the file represented by [filePath] in shared
/// storage [destination] and potentially a [directory] within that
/// destination.
///
/// Returns the path to the stored file, or null if not successful
///
/// See the documentation for [moveToSharedStorage] for special use case
/// on iOS for .images and .video
///
/// Platform-dependent, not consistent across all platforms
Future<String?> pathInSharedStorage(
String filePath, SharedStorage destination,
{String directory = ''}) async =>
_downloader.pathInSharedStorage(filePath, destination, directory);
/// Open the file represented by [task] or [filePath] using the application
/// available on the platform.
///
/// [mimeType] may override the mimetype derived from the file extension,
/// though implementation depends on the platform and may not always work.
///
/// Returns true if an application was launched successfully
Future<bool> openFile({Task? task, String? filePath, String? mimeType}) {
assert(task != null || filePath != null, 'Task or filePath must be set');
assert(!(task != null && filePath != null),
'Either task or filePath must be set, not both');
return _downloader.openFile(task, filePath, mimeType);
}
/// Return the platform version as a String
///
/// On Android this is the API integer, e.g. "33"
/// On iOS this is the iOS version, e.g. "16.1"
/// On desktop this is a description of the OS version, not parsable
Future<String> platformVersion() => _downloader.platformVersion();
/// Closes the [updates] stream and re-initializes the [StreamController]
/// such that the stream can be listened to again
Future<void> resetUpdates() => _downloader.resetUpdatesStreamController();
/// Destroy the [FileDownloader]. Subsequent use requires initialization
void destroy() {
_downloader.destroy();
Localstore.instance.clearCache();
}
}
/// Performs the actual server request, with retries
///
/// This function is run on an Isolate to ensure performance on the main
/// Isolate is not affected
Future<http.Response> _doRequest(
(Request, Duration?, Map<String, dynamic>, bool, bool) params) async {
final (
request,
requestTimeout,
proxy,
bypassTLSCertificateValidation,
useNativeHttpClient
) = params;
if (useNativeHttpClient) {
desktop_downloader_native.DesktopDownloaderNativeHttpClient.setHttpClient(
requestTimeout, proxy, bypassTLSCertificateValidation);
} else {
DesktopDownloaderHttpClient.setHttpClient(
requestTimeout, proxy, bypassTLSCertificateValidation);
}
final client = useNativeHttpClient
? desktop_downloader_native.DesktopDownloaderNativeHttpClient.httpClient
: DesktopDownloaderHttpClient.httpClient;
var response = http.Response('', 499,
reasonPhrase: 'Not attempted'); // dummy to start with
while (request.retriesRemaining >= 0) {
try {
response = await switch (request.httpRequestMethod) {
'GET' => client.get(Uri.parse(request.url), headers: request.headers),
'POST' => client.post(Uri.parse(request.url),
headers: request.headers, body: request.post),
'HEAD' => client.head(Uri.parse(request.url), headers: request.headers),
'PUT' => client.put(Uri.parse(request.url), headers: request.headers),
'DELETE' =>
client.delete(Uri.parse(request.url), headers: request.headers),
'PATCH' =>
client.patch(Uri.parse(request.url), headers: request.headers),
_ => Future.value(response)
};
if ([200, 201, 202, 203, 204, 205, 206, 404]
.contains(response.statusCode)) {
return response;
}
} catch (e) {
response = http.Response('', 499, reasonPhrase: e.toString());
}
// error, retry if allowed
request.decreaseRetriesRemaining();
if (request.retriesRemaining < 0) {
return response; // final response with error
}
final waitTime = Duration(
seconds: pow(2, (request.retries - request.retriesRemaining)).toInt());
await Future.delayed(waitTime);
}
throw ArgumentError('Request to ${request.url} had no retries remaining');
}