Repository files navigation

SimpleDownloader

JitPackGitHub releaseAndroid API

SimpleDownloader is an Android download library project. It handles the parts that usually make downloading difficult: queues, concurrent downloads, pause and resume, unstable networks, scoped storage, task persistence, foreground, notifications, progress updates, etc.

The simple API:

DownloadTasktask = SimpleDownloader.with(context)
.enableForeground(true)
.setOutput(folderPath, FileName.AUTO) // Or .setOutput(folderUri, FileName)
.setFileUrl(fileUrl)
.startDownload();

Features

  • Multiple downloads with queue and priority support
  • automatic download concurrency
  • Pause, resume, cancel, retry, remove, requeue, and force download
  • Resume using HTTP range requests
  • Network loss handling and Wi-Fi-only downloads
  • Task persistence and restoration with filters.
  • Complete built-in storage support for filesystem paths, SAF/document-tree output, and MediaStore output.
  • Built-in Subfolder support
  • Built-in file overwrite support
  • Automatic file name and MIME type resolution
  • Progress, speed, ETA, status, and lifecycle callbacks
  • Progress, completion, and error notifications with actions, thumbnails, and more.
  • Optional foreground execution
  • Custom OkHttpClient support
  • Custom task Comparator
  • RetryPolicy, timeouts, headers, cookies, checksums and many more
  • Minimum Android version: API 21

Installation

Add JitPack to your repositories:

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
}
}

Add SimpleDownloader to your app module:

dependencies {
implementation "com.github.jeetarc:SimpleDownloader:1.0.0-beta.3"
}

SimpleDownloader is built with Java 8 and compileSdk 35.

Setup

If you enable notifications, add:

<uses-permissionandroid:name="android.permission.POST_NOTIFICATIONS" />

On Android 13 and newer, request this permission at runtime.

If you enable enableForeground(true), also add:

<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE" />
<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

Foreground mode can automatically enable notifications, also you can use enableNotifications(true).

If using normal file path, add:

<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permissionandroid:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

And request the permission at runtime. Storage permissions are not needed when saving to app-specific folder or using folderUri via MediaStore or Storage Access Framework.

All other permissions are added by default.

Quick start

SimpleDownloader has three built-in storage modes.

1. Download into a file system folder:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderPath, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

2. Download into a MediaStore collection (Android 10+):

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(MediaStore.Downloads.EXTERNAL_CONTENT_URI, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

You can also use other MediaStore collections:

MediaStore.Images.Media.EXTERNAL_CONTENT_URIMediaStore.Video.Media.EXTERNAL_CONTENT_URIMediaStore.Audio.Media.EXTERNAL_CONTENT_URI

3. Download into a selected folder via SAF/document-tree:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(treeUri, FileName.AUTO)
.setFileUrl("https://example.com/files/document.pdf")
.startDownload();

Keep the URI permission:

intflags = Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
getContentResolver().takePersistableUriPermission(treeUri, flags);

Use a custom name if needed:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, "example.mp4")
.setFileUrl(fileUrl)
.startDownload();

SimpleDownloader creates a new file inside each folder. If a file with the same name already exists, it creates a unique name automatically.

Overwrite a file:

Use a DocumentFile URI or a specific MediaStore item URI:

DownloadTasktask = SimpleDownloader.with(context)
.overwrite(fileUri)
.setFileUrl(fileUrl)
.startDownload();

Or use a file path:

DownloadTasktask = SimpleDownloader.with(context)
.overwrite(outputFile.getAbsolutePath())
.setFileUrl(fileUrl)
.startDownload();

Subfolder:

A subfolder is an optional folder inside the main output folder where you want the downloaded file to be saved.

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, FileName.AUTO)
.setSubFolder("app")
.setFileUrl(fileUrl)
.startDownload();

Subfolders can be nested, for example "app/videos". They work with filesystem paths, SAF/document-tree output, and MediaStore output.

Listener for download updates

All DownloadListener callbacks run on the main thread. Every method is optional.

DownloadListenerlistener = newDownloadListener() {
@OverridepublicvoidonProgress(longid, intprogress, longspeed, longetaMs, DownloadTasktask) {
progressBar.setProgress(progress);
speedText.setText(Formator.formatSpeed(speed));
etaText.setText(Formator.formatEta(etaMs));
}
@OverridepublicvoidonComplete(longid, UrioutputUri, DownloadTasktask) {
// The download finished successfully.
}
@OverridepublicvoidonError(longid, UrioutputUri, Exceptionerror, DownloadTasktask) {
// The download failed.
}
};
DownloadTasktask = SimpleDownloader.with(this)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.addListener(listener)
.startDownload();

Other callbacks include:

onStart(longid, DownloadTasktask) {}
onQueued(longid, intposition, DownloadTasktask) {}
onPaused(longid, DownloadTasktask) {}
onResumed(longid, DownloadTasktask) {}
onCancelled(longid, DownloadTasktask) {}
onRemoved(longid, booleanoutputDeleted, DownloadTasktask) {}
onRetry(longid, intattempt, DownloadTasktask) {}
onWaitingForNetwork(longid, intnetworkType, DownloadTasktask) {}
onStatusChanged(longid, Statusstatus, DownloadTasktask) {}
onActiveChanged(longid, booleanisActive, DownloadTasktask) {}
onLifecycleChanged(longid, intlifecycle, DownloadTasktask) {}

onStart() can run again on resume and retry. Use onLifecycleChanged() to know the start or end of the full task lifecycle.

You can also add listeners directly on a task:

task.addListener(listener);
task.removeListener(listener);
task.releaseCallbacks();

Observe the task list

Use TaskListObserver when showing all downloads in a list, RecyclerView, etc.

TaskListObserverobserver = newTaskListObserver() {
@OverridepublicvoidonTasksChanged(List<DownloadTask> tasks) {
// The list order changed
}
@OverridepublicvoidonTaskUpdated(longid, DownloadTasktask) {
// Update only the matching item
}
};
SimpleDownloaderdownloader = SimpleDownloader.with(this)
.addObserver(observer);

The list passed to onTasksChanged() is an unmodifiable snapshot. DownloadTask objects inside it are live and can update.

Release the observer when not needed:

SimpleDownloader.releaseObserver(observer);

Control a task

task.pause();
task.resume();
task.cancel();
task.retry();
task.requeue();
task.remove();
task.forceDownload();

Check:

task.canPause();
task.canResume();
task.canRetry();

Change some task settings after creation:

task.setPriority(Priority.HIGH);
task.setWifiOnly(true);
task.setLockedInQueue(true);
task.setDeleteOnRemoval(true);

Note:

  • cancel() stops the task and deletes its output.
  • remove() removes the task from SimpleDownloader register.
  • remove() deletes the output only when setDeleteOnRemoval(true) is enabled.
  • forceDownload() starts a queued task even when it is locked. It doesn't care about concurrency limit

Task info

task.getId();
task.getFileUrl();
task.getFileName();
task.getMimeType();
task.getOutputUri();
task.getOutputFile();
task.getOutputDocumentFile();
task.getOutputFolderUri();
task.getOutputFolderPath();
task.getSubFolderPath();
task.getOutputPath();
task.getOverwriteUri();
task.getProgress();
task.getDownloadedBytes();
task.getTotalBytes();
task.getSpeed();
task.getEtaMs();
task.getStatus();
task.getPriority();
task.getError();
task.getCreatedAt();
task.getMaxRetryCount();
task.isActive();
task.isQueued();
task.isPaused();
task.isWaitingForNetwork();
task.isFinished();
task.isOccupiedSlot();

Reuse a configured instance

You can configure a SimpleDownloader instance once inside onCreate() and reuse it:

privateSimpleDownloaderdownloader;
@OverrideprotectedvoidonCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
downloader = SimpleDownloader.with(this)
.setMaxConcurrent(3)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setBufferSize(16 * 1024)
.enableHistory(true);
}
// You can configure all fields you want be the same across downloads.

Then use the configured instance whenever you start a download:

DownloadTasktask = downloader
.setOutput(folderUri, fileName)
.setMimeType(mimeType)
.setFileUrl(fileUrl)
.startDownload();

You can also use the same instance to restore tasks:

List<DownloadTask> tasks = downloader.restoreTasks();

Other common settings, such as retry policy, user agent, priority, Wi-Fi-only mode, notifications, foreground execution, etc can also be configured on the same downloader instance, instead of being set again for every download.

File URL, output destination, and custom ID should be set again before starting each task.

Global controls

Control a task by ID:

SimpleDownloader.pause(id);
SimpleDownloader.resume(id);
SimpleDownloader.cancel(id);
SimpleDownloader.retry(id);
SimpleDownloader.requeue(id);
SimpleDownloader.remove(id);
SimpleDownloader.forceDownload(id);

Control multiple tasks:

SimpleDownloader.pauseAll();
SimpleDownloader.resumeAll();
SimpleDownloader.cancelAll();
SimpleDownloader.retryAll();
SimpleDownloader.requeueAll();
SimpleDownloader.removeAll();
SimpleDownloader.pause(Priority.LOW);
SimpleDownloader.resumeAll(Priority.HIGH);
SimpleDownloader.remove(Status.COMPLETED);
SimpleDownloader.remove(Priority.LOW);

get task from registry:

DownloadTasktask = SimpleDownloader.getTask(id);
List<DownloadTask> all = SimpleDownloader.getTasks();
List<DownloadTask> completed = SimpleDownloader.getTasks(TaskField.STATUS, Status.COMPLETED);
SimpleDownloader.getTask(TaskField.FILE_NAME, fileName) // returns latest matching single task, null if not found.inttotal = SimpleDownloader.getTotalCount();
intactive = SimpleDownloader.getActiveCount();
intqueued = SimpleDownloader.getQueuedCount();
intoccupied = SimpleDownloader.getOccupiedCount();
intconcurrency = SimpleDownloader.getEffectiveMaxConcurrent();

Update a task by ID:

SimpleDownloader.setPriority(id, Priority.NEXT);
SimpleDownloader.setWifiOnly(id, true);
SimpleDownloader.setLockedInQueue(id, true);
SimpleDownloader.setDeleteOnRemoval(id, true);

Restore tasks

Restore methods immediately restore saved tasks using the downloader's current configuration.

Important: Configure all downloader settings before calling any restore method. Restore methods should always be the last configuration call, otherwise restored tasks may not receive the configuration called after restore.

Automatic Restore:

Enable automatic restoration of previously saved download tasks:

SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setRetryPolicy(...)
.enableNotifications(true)
.enableForeground(true)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setNotification(...)
.setAutoRestore(true); // Keep this last.

When enabled, SimpleDownloader automatically restores previously saved tasks and resumes eligible ones. You do not need to call restoreTasks() separately when using auto restore. Auto restore is disabled by default.

Explicit restore when your app ready to show or continue downloads:

restored = downloader.restoreTasks();

Active tasks are restored as paused. Resume the tasks you want to continue:

SimpleDownloader.resumeAll();
//orfor (DownloadTasktask : restored) {
task.resume();
}

Restore matching tasks:

restoreTasks(...) returns an empty list when no match:

List<DownloadTask> paused = downloader.restoreTasks(TaskField.STATUS, Status.PAUSED);
List<DownloadTask> videos = downloader.restoreTasks(TaskField.MIME_TYPE, "video/mp4");
List<DownloadTask> matchingUrl = downloader.restoreTasks(TaskField.FILE_URL, fileUrl);

restoreTask() returns a single newest matching task, or null when no match:

DownloadTasktask = downloader.restoreTask(TaskField.FILE_URL, fileUrl);
if (task != null) task.resume();

Available fields:

TaskField.IDTaskField.FILE_URLTaskField.STATUSTaskField.PRIORITYTaskField.MIME_TYPETaskField.FILE_NAMETaskField.CREATED_ATTaskField.WIFI_ONLYTaskField.BUFFER_SIZETaskField.PROGRESSTaskField.BYTES_DOWNLOADEDTaskField.TOTAL_BYTESTaskField.OUTPUT_URITaskField.OUTPUT_PATHTaskField.OVERWRITE_URITaskField.OVERWRITE_PATHTaskField.OUTPUT_FOLDER_URITaskField.OUTPUT_FOLDER_PATHTaskField.SUB_FOLDER_PATHTaskField.DELETE_ON_REMOVALTaskField.LOCKED_IN_QUEUE

Finished tasks are kept in the database only when history is enabled:

SimpleDownloader.with(context)
.enableHistory(true);

Queue and concurrency

By default, SimpleDownloader use automatic concurrency. It starts with 1 and go up to 10 slot beased on download speed.

auto concurrency starts when setMaxConcurrent(0) or not set.

Stop queued tasks from starting automatically when a slot becomes free:

SimpleDownloader.with(context)
.setDownloadOnSlotFree(false);

Lock task in the queue:

task.setLockedInQueue(true);

Priorities :

Priority.NEXTPriority.HIGHPriority.NORMALPriority.LOW// NEXT > HIGH > NORMAL > LOW

Task statuses:

Status.STARTINGStatus.QUEUEDStatus.CONNECTINGStatus.DOWNLOADINGStatus.PAUSEDStatus.CANCELLEDStatus.WAITING_FOR_NETWORKStatus.RETRYINGStatus.COMPLETEDStatus.FAILED

Retry policy

The default policy with one automatic retry. Configure:

RetryPolicyretryPolicy = RetryPolicy.builder()
.maxRetryCount(3)
.initialDelayMs(1000)
.multiplier(2.0)
.maxDelayMs(30_000)
.build();
SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setRetryPolicy(retryPolicy);

Retry settings stay on the SimpleDownloader instance used to create or restore tasks.

MIME Type

A MIME type (media type) is a used to identify the format of a file, SimpleDownloader resolves it automatically, but can be set explicitly:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, "manual.pdf")
.setMimeType("application/pdf")
.setFileUrl(fileUrl)
.startDownload();

Or use automatic resolution:

.setMimeType(MimeType.AUTO) // or MimeType.FROM_NAME

Network, headers, cookies

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.setHeader("Authorization", "Bearer " + token)
.setHeader("Referer", pageUrl)
.setCookies("session=" + sessionId)
.setWifiOnly(true)
.startDownload();

Add headers:

Map<String, String> headers = newHashMap<>();
headers.put("Authorization", "Bearer " + token);
headers.put("Referer", pageUrl);
SimpleDownloader.with(context)
.setHeaders(headers);

Network info:

booleanavailable = SimpleDownloader.isNetworkAvailable();
intnetworkType = SimpleDownloader.getNetworkType();

Network constants:

NETWORK_TYPE_NONENETWORK_TYPE_UNKNOWNNETWORK_TYPE_WIFINETWORK_TYPE_CELLULARNETWORK_TYPE_ETHERNETNETWORK_TYPE_BLUETOOTHNETWORK_TYPE_VPNNETWORK_TYPE_USBNETWORK_TYPE_ROAMING

Waiting tasks resume when the network becomes available by default. Change with:

SimpleDownloader.with(context)
.enableResumeOnNetworkGain(false);

Notifications

Notifications are optional and disabled by default.

DownloadNotificationnotification = newDownloadNotification()
.setSmallIcon(R.drawable.ic_download)
.setCompleteIcon(R.drawable.ic_download_done)
.setErrorIcon(R.drawable.ic_download_error)
.setColorAccent(0xFF0087E5)
.setShowPauseAction(true)
.setShowCancelAction(true)
.setShowRetryAction(true);
DownloadTasktask = SimpleDownloader.with(context)
.enableNotifications(true)
.setNotification(notification)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

DownloadNotification configuration is optional, only use enableNotifications(true) if you don't want to customize. It will use the default config.

Run tasks using a foreground service:

SimpleDownloader.with(context)
.enableForeground(true)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

SimpleDownloader has built in thumbnail system for notifications, it can generate a thumbnail from a video, image, audio (album art), APK, PDF automatically.

You can also set a thumbnail:

notification.setThumbnail(bitmap);

Or load it from a URL:

notification.setThumbnailUrl(thumbnailUrl, thumbHeaders);
// pass null for headers, if it not available

DownloadNotification can configure the channel, importance, lock-screen visibility, sound, vibration, color, update interval, actions, etc.

Checksums

Verify the completed file with algorithms supported by MessageDigest. Like SHA-256, SHA-1, or MD5:

DownloadTasktask = SimpleDownloader.with(context)
.setChecksum("SHA-256", expectedChecksum)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

File names and MIME types modes

FileName.AUTOFileName.TIME_BASED
MimeType.AUTOMimeType.FROM_NAME

AUTO uses the URL, response headers, file extension, and content type to resolve the name and MIME type.

Error handling

Failures exceptions are instance of DownloadException:

@OverridepublicvoidonError(longid, UrioutputUri, Exceptionerror, DownloadTasktask) {
if (!(errorinstanceofDownloadException)) return;
DownloadExceptionfailure = (DownloadException) error;
DownloadException.Typetype = failure.getType();
inthttpCode = failure.getCode();
booleanretryable = failure.isRetryable();
Throwablecause = failure.getCause();
}

Error types:

NETWORK_LOSTTIMEOUTDNS_ERRORSSL_ERRORHTTP_ERRORENOSPCFILE_ERRORSTORAGE_PERMISSION_DENIEDOUTPUT_INVALIDRANGE_NOT_SUPPORTEDEMPTY_RESPONSECHECKSUM_FAILEDCANCELLEDUNKNOWN

Other configuration

SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setId(customId)
.setUserAgent(userAgent)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setBufferSize(16 * 1024)
.enableHistory(true)
.enableSorting(true);

Custom IDs are optional. SimpleDownloader generates an ID when setId() is not used. An active task cannot be replaced by another task with the same ID.

Passing 0 for a connection or read timeout keeps the OkHttp default.

Use your own HTTP client:

OkHttpClientclient = newOkHttpClient.Builder()
.followRedirects(true)
.build();
SimpleDownloader.with(context)
.setHttpClient(client);

The HTTP client cannot be replaced while a worker is running or already scheduled.

Use a custom task list order:

SimpleDownloader.with(context)
.setTaskComparator(myComparator);

Formatting helpers

Stringsize = Formator.formatBytes(bytes);
Stringspeed = Formator.formatSpeed(bytesPerSecond);
Stringeta = Formator.formatEta(etaMs);

TypeResolver is used internally, but it is also available for resolving file extensions and MIME types for you 🙂.

Cleanup

When listeners and observers are owned by an Activity or Fragment, release them using the same owner object given to with(...):

@OverrideprotectedvoidonDestroy() {
SimpleDownloader.releaseCallbacks(this);
SimpleDownloader.releaseObserver(observer);
super.onDestroy();
}

Call shutdown() only when you intentionally want to stop the library and release all workers, network callbacks, HTTP resources, thumbnails, database, etc:

SimpleDownloader.shutdown();

You do not need to call shutdown() normally or when an Activity is destroyed.

Support

Found a problem or have a suggestion? Open an issue: https://github.com/jeetarc/SimpleDownloader/issues

Copyright © 2026 Jeet / Jeetarc.

About

A modern Android download manager library designed to simplify complex file downloading behind a simple API

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

SimpleDownloader

JitPackGitHub releaseAndroid API

SimpleDownloader is an Android download library project. It handles the parts that usually make downloading difficult: queues, concurrent downloads, pause and resume, unstable networks, scoped storage, task persistence, foreground, notifications, progress updates, etc.

The simple API:

DownloadTasktask = SimpleDownloader.with(context)
.enableForeground(true)
.setOutput(folderPath, FileName.AUTO) // Or .setOutput(folderUri, FileName)
.setFileUrl(fileUrl)
.startDownload();

Features

  • Multiple downloads with queue and priority support
  • automatic download concurrency
  • Pause, resume, cancel, retry, remove, requeue, and force download
  • Resume using HTTP range requests
  • Network loss handling and Wi-Fi-only downloads
  • Task persistence and restoration with filters.
  • Complete built-in storage support for filesystem paths, SAF/document-tree output, and MediaStore output.
  • Built-in Subfolder support
  • Built-in file overwrite support
  • Automatic file name and MIME type resolution
  • Progress, speed, ETA, status, and lifecycle callbacks
  • Progress, completion, and error notifications with actions, thumbnails, and more.
  • Optional foreground execution
  • Custom OkHttpClient support
  • Custom task Comparator
  • RetryPolicy, timeouts, headers, cookies, checksums and many more
  • Minimum Android version: API 21

Installation

Add JitPack to your repositories:

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
}
}

Add SimpleDownloader to your app module:

dependencies {
implementation "com.github.jeetarc:SimpleDownloader:1.0.0-beta.3"
}

SimpleDownloader is built with Java 8 and compileSdk 35.

Setup

If you enable notifications, add:

<uses-permissionandroid:name="android.permission.POST_NOTIFICATIONS" />

On Android 13 and newer, request this permission at runtime.

If you enable enableForeground(true), also add:

<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE" />
<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

Foreground mode can automatically enable notifications, also you can use enableNotifications(true).

If using normal file path, add:

<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permissionandroid:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

And request the permission at runtime. Storage permissions are not needed when saving to app-specific folder or using folderUri via MediaStore or Storage Access Framework.

All other permissions are added by default.

Quick start

SimpleDownloader has three built-in storage modes.

1. Download into a file system folder:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderPath, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

2. Download into a MediaStore collection (Android 10+):

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(MediaStore.Downloads.EXTERNAL_CONTENT_URI, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

You can also use other MediaStore collections:

MediaStore.Images.Media.EXTERNAL_CONTENT_URIMediaStore.Video.Media.EXTERNAL_CONTENT_URIMediaStore.Audio.Media.EXTERNAL_CONTENT_URI

3. Download into a selected folder via SAF/document-tree:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(treeUri, FileName.AUTO)
.setFileUrl("https://example.com/files/document.pdf")
.startDownload();

Keep the URI permission:

intflags = Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
getContentResolver().takePersistableUriPermission(treeUri, flags);

Use a custom name if needed:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, "example.mp4")
.setFileUrl(fileUrl)
.startDownload();

SimpleDownloader creates a new file inside each folder. If a file with the same name already exists, it creates a unique name automatically.

Overwrite a file:

Use a DocumentFile URI or a specific MediaStore item URI:

DownloadTasktask = SimpleDownloader.with(context)
.overwrite(fileUri)
.setFileUrl(fileUrl)
.startDownload();

Or use a file path:

DownloadTasktask = SimpleDownloader.with(context)
.overwrite(outputFile.getAbsolutePath())
.setFileUrl(fileUrl)
.startDownload();

Subfolder:

A subfolder is an optional folder inside the main output folder where you want the downloaded file to be saved.

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, FileName.AUTO)
.setSubFolder("app")
.setFileUrl(fileUrl)
.startDownload();

Subfolders can be nested, for example "app/videos". They work with filesystem paths, SAF/document-tree output, and MediaStore output.

Listener for download updates

All DownloadListener callbacks run on the main thread. Every method is optional.

DownloadListenerlistener = newDownloadListener() {
@OverridepublicvoidonProgress(longid, intprogress, longspeed, longetaMs, DownloadTasktask) {
progressBar.setProgress(progress);
speedText.setText(Formator.formatSpeed(speed));
etaText.setText(Formator.formatEta(etaMs));
}
@OverridepublicvoidonComplete(longid, UrioutputUri, DownloadTasktask) {
// The download finished successfully.
}
@OverridepublicvoidonError(longid, UrioutputUri, Exceptionerror, DownloadTasktask) {
// The download failed.
}
};
DownloadTasktask = SimpleDownloader.with(this)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.addListener(listener)
.startDownload();

Other callbacks include:

onStart(longid, DownloadTasktask) {}
onQueued(longid, intposition, DownloadTasktask) {}
onPaused(longid, DownloadTasktask) {}
onResumed(longid, DownloadTasktask) {}
onCancelled(longid, DownloadTasktask) {}
onRemoved(longid, booleanoutputDeleted, DownloadTasktask) {}
onRetry(longid, intattempt, DownloadTasktask) {}
onWaitingForNetwork(longid, intnetworkType, DownloadTasktask) {}
onStatusChanged(longid, Statusstatus, DownloadTasktask) {}
onActiveChanged(longid, booleanisActive, DownloadTasktask) {}
onLifecycleChanged(longid, intlifecycle, DownloadTasktask) {}

onStart() can run again on resume and retry. Use onLifecycleChanged() to know the start or end of the full task lifecycle.

You can also add listeners directly on a task:

task.addListener(listener);
task.removeListener(listener);
task.releaseCallbacks();

Observe the task list

Use TaskListObserver when showing all downloads in a list, RecyclerView, etc.

TaskListObserverobserver = newTaskListObserver() {
@OverridepublicvoidonTasksChanged(List<DownloadTask> tasks) {
// The list order changed
}
@OverridepublicvoidonTaskUpdated(longid, DownloadTasktask) {
// Update only the matching item
}
};
SimpleDownloaderdownloader = SimpleDownloader.with(this)
.addObserver(observer);

The list passed to onTasksChanged() is an unmodifiable snapshot. DownloadTask objects inside it are live and can update.

Release the observer when not needed:

SimpleDownloader.releaseObserver(observer);

Control a task

task.pause();
task.resume();
task.cancel();
task.retry();
task.requeue();
task.remove();
task.forceDownload();

Check:

task.canPause();
task.canResume();
task.canRetry();

Change some task settings after creation:

task.setPriority(Priority.HIGH);
task.setWifiOnly(true);
task.setLockedInQueue(true);
task.setDeleteOnRemoval(true);

Note:

  • cancel() stops the task and deletes its output.
  • remove() removes the task from SimpleDownloader register.
  • remove() deletes the output only when setDeleteOnRemoval(true) is enabled.
  • forceDownload() starts a queued task even when it is locked. It doesn't care about concurrency limit

Task info

task.getId();
task.getFileUrl();
task.getFileName();
task.getMimeType();
task.getOutputUri();
task.getOutputFile();
task.getOutputDocumentFile();
task.getOutputFolderUri();
task.getOutputFolderPath();
task.getSubFolderPath();
task.getOutputPath();
task.getOverwriteUri();
task.getProgress();
task.getDownloadedBytes();
task.getTotalBytes();
task.getSpeed();
task.getEtaMs();
task.getStatus();
task.getPriority();
task.getError();
task.getCreatedAt();
task.getMaxRetryCount();
task.isActive();
task.isQueued();
task.isPaused();
task.isWaitingForNetwork();
task.isFinished();
task.isOccupiedSlot();

Reuse a configured instance

You can configure a SimpleDownloader instance once inside onCreate() and reuse it:

privateSimpleDownloaderdownloader;
@OverrideprotectedvoidonCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
downloader = SimpleDownloader.with(this)
.setMaxConcurrent(3)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setBufferSize(16 * 1024)
.enableHistory(true);
}
// You can configure all fields you want be the same across downloads.

Then use the configured instance whenever you start a download:

DownloadTasktask = downloader
.setOutput(folderUri, fileName)
.setMimeType(mimeType)
.setFileUrl(fileUrl)
.startDownload();

You can also use the same instance to restore tasks:

List<DownloadTask> tasks = downloader.restoreTasks();

Other common settings, such as retry policy, user agent, priority, Wi-Fi-only mode, notifications, foreground execution, etc can also be configured on the same downloader instance, instead of being set again for every download.

File URL, output destination, and custom ID should be set again before starting each task.

Global controls

Control a task by ID:

SimpleDownloader.pause(id);
SimpleDownloader.resume(id);
SimpleDownloader.cancel(id);
SimpleDownloader.retry(id);
SimpleDownloader.requeue(id);
SimpleDownloader.remove(id);
SimpleDownloader.forceDownload(id);

Control multiple tasks:

SimpleDownloader.pauseAll();
SimpleDownloader.resumeAll();
SimpleDownloader.cancelAll();
SimpleDownloader.retryAll();
SimpleDownloader.requeueAll();
SimpleDownloader.removeAll();
SimpleDownloader.pause(Priority.LOW);
SimpleDownloader.resumeAll(Priority.HIGH);
SimpleDownloader.remove(Status.COMPLETED);
SimpleDownloader.remove(Priority.LOW);

get task from registry:

DownloadTasktask = SimpleDownloader.getTask(id);
List<DownloadTask> all = SimpleDownloader.getTasks();
List<DownloadTask> completed = SimpleDownloader.getTasks(TaskField.STATUS, Status.COMPLETED);
SimpleDownloader.getTask(TaskField.FILE_NAME, fileName) // returns latest matching single task, null if not found.inttotal = SimpleDownloader.getTotalCount();
intactive = SimpleDownloader.getActiveCount();
intqueued = SimpleDownloader.getQueuedCount();
intoccupied = SimpleDownloader.getOccupiedCount();
intconcurrency = SimpleDownloader.getEffectiveMaxConcurrent();

Update a task by ID:

SimpleDownloader.setPriority(id, Priority.NEXT);
SimpleDownloader.setWifiOnly(id, true);
SimpleDownloader.setLockedInQueue(id, true);
SimpleDownloader.setDeleteOnRemoval(id, true);

Restore tasks

Restore methods immediately restore saved tasks using the downloader's current configuration.

Important: Configure all downloader settings before calling any restore method. Restore methods should always be the last configuration call, otherwise restored tasks may not receive the configuration called after restore.

Automatic Restore:

Enable automatic restoration of previously saved download tasks:

SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setRetryPolicy(...)
.enableNotifications(true)
.enableForeground(true)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setNotification(...)
.setAutoRestore(true); // Keep this last.

When enabled, SimpleDownloader automatically restores previously saved tasks and resumes eligible ones. You do not need to call restoreTasks() separately when using auto restore. Auto restore is disabled by default.

Explicit restore when your app ready to show or continue downloads:

restored = downloader.restoreTasks();

Active tasks are restored as paused. Resume the tasks you want to continue:

SimpleDownloader.resumeAll();
//orfor (DownloadTasktask : restored) {
task.resume();
}

Restore matching tasks:

restoreTasks(...) returns an empty list when no match:

List<DownloadTask> paused = downloader.restoreTasks(TaskField.STATUS, Status.PAUSED);
List<DownloadTask> videos = downloader.restoreTasks(TaskField.MIME_TYPE, "video/mp4");
List<DownloadTask> matchingUrl = downloader.restoreTasks(TaskField.FILE_URL, fileUrl);

restoreTask() returns a single newest matching task, or null when no match:

DownloadTasktask = downloader.restoreTask(TaskField.FILE_URL, fileUrl);
if (task != null) task.resume();

Available fields:

TaskField.IDTaskField.FILE_URLTaskField.STATUSTaskField.PRIORITYTaskField.MIME_TYPETaskField.FILE_NAMETaskField.CREATED_ATTaskField.WIFI_ONLYTaskField.BUFFER_SIZETaskField.PROGRESSTaskField.BYTES_DOWNLOADEDTaskField.TOTAL_BYTESTaskField.OUTPUT_URITaskField.OUTPUT_PATHTaskField.OVERWRITE_URITaskField.OVERWRITE_PATHTaskField.OUTPUT_FOLDER_URITaskField.OUTPUT_FOLDER_PATHTaskField.SUB_FOLDER_PATHTaskField.DELETE_ON_REMOVALTaskField.LOCKED_IN_QUEUE

Finished tasks are kept in the database only when history is enabled:

SimpleDownloader.with(context)
.enableHistory(true);

Queue and concurrency

By default, SimpleDownloader use automatic concurrency. It starts with 1 and go up to 10 slot beased on download speed.

auto concurrency starts when setMaxConcurrent(0) or not set.

Stop queued tasks from starting automatically when a slot becomes free:

SimpleDownloader.with(context)
.setDownloadOnSlotFree(false);

Lock task in the queue:

task.setLockedInQueue(true);

Priorities :

Priority.NEXTPriority.HIGHPriority.NORMALPriority.LOW// NEXT > HIGH > NORMAL > LOW

Task statuses:

Status.STARTINGStatus.QUEUEDStatus.CONNECTINGStatus.DOWNLOADINGStatus.PAUSEDStatus.CANCELLEDStatus.WAITING_FOR_NETWORKStatus.RETRYINGStatus.COMPLETEDStatus.FAILED

Retry policy

The default policy with one automatic retry. Configure:

RetryPolicyretryPolicy = RetryPolicy.builder()
.maxRetryCount(3)
.initialDelayMs(1000)
.multiplier(2.0)
.maxDelayMs(30_000)
.build();
SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setRetryPolicy(retryPolicy);

Retry settings stay on the SimpleDownloader instance used to create or restore tasks.

MIME Type

A MIME type (media type) is a used to identify the format of a file, SimpleDownloader resolves it automatically, but can be set explicitly:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, "manual.pdf")
.setMimeType("application/pdf")
.setFileUrl(fileUrl)
.startDownload();

Or use automatic resolution:

.setMimeType(MimeType.AUTO) // or MimeType.FROM_NAME

Network, headers, cookies

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.setHeader("Authorization", "Bearer " + token)
.setHeader("Referer", pageUrl)
.setCookies("session=" + sessionId)
.setWifiOnly(true)
.startDownload();

Add headers:

Map<String, String> headers = newHashMap<>();
headers.put("Authorization", "Bearer " + token);
headers.put("Referer", pageUrl);
SimpleDownloader.with(context)
.setHeaders(headers);

Network info:

booleanavailable = SimpleDownloader.isNetworkAvailable();
intnetworkType = SimpleDownloader.getNetworkType();

Network constants:

NETWORK_TYPE_NONENETWORK_TYPE_UNKNOWNNETWORK_TYPE_WIFINETWORK_TYPE_CELLULARNETWORK_TYPE_ETHERNETNETWORK_TYPE_BLUETOOTHNETWORK_TYPE_VPNNETWORK_TYPE_USBNETWORK_TYPE_ROAMING

Waiting tasks resume when the network becomes available by default. Change with:

SimpleDownloader.with(context)
.enableResumeOnNetworkGain(false);

Notifications

Notifications are optional and disabled by default.

DownloadNotificationnotification = newDownloadNotification()
.setSmallIcon(R.drawable.ic_download)
.setCompleteIcon(R.drawable.ic_download_done)
.setErrorIcon(R.drawable.ic_download_error)
.setColorAccent(0xFF0087E5)
.setShowPauseAction(true)
.setShowCancelAction(true)
.setShowRetryAction(true);
DownloadTasktask = SimpleDownloader.with(context)
.enableNotifications(true)
.setNotification(notification)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

DownloadNotification configuration is optional, only use enableNotifications(true) if you don't want to customize. It will use the default config.

Run tasks using a foreground service:

SimpleDownloader.with(context)
.enableForeground(true)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

SimpleDownloader has built in thumbnail system for notifications, it can generate a thumbnail from a video, image, audio (album art), APK, PDF automatically.

You can also set a thumbnail:

notification.setThumbnail(bitmap);

Or load it from a URL:

notification.setThumbnailUrl(thumbnailUrl, thumbHeaders);
// pass null for headers, if it not available

DownloadNotification can configure the channel, importance, lock-screen visibility, sound, vibration, color, update interval, actions, etc.

Checksums

Verify the completed file with algorithms supported by MessageDigest. Like SHA-256, SHA-1, or MD5:

DownloadTasktask = SimpleDownloader.with(context)
.setChecksum("SHA-256", expectedChecksum)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

File names and MIME types modes

FileName.AUTOFileName.TIME_BASED
MimeType.AUTOMimeType.FROM_NAME

AUTO uses the URL, response headers, file extension, and content type to resolve the name and MIME type.

Error handling

Failures exceptions are instance of DownloadException:

@OverridepublicvoidonError(longid, UrioutputUri, Exceptionerror, DownloadTasktask) {
if (!(errorinstanceofDownloadException)) return;
DownloadExceptionfailure = (DownloadException) error;
DownloadException.Typetype = failure.getType();
inthttpCode = failure.getCode();
booleanretryable = failure.isRetryable();
Throwablecause = failure.getCause();
}

Error types:

NETWORK_LOSTTIMEOUTDNS_ERRORSSL_ERRORHTTP_ERRORENOSPCFILE_ERRORSTORAGE_PERMISSION_DENIEDOUTPUT_INVALIDRANGE_NOT_SUPPORTEDEMPTY_RESPONSECHECKSUM_FAILEDCANCELLEDUNKNOWN

Other configuration

SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setId(customId)
.setUserAgent(userAgent)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setBufferSize(16 * 1024)
.enableHistory(true)
.enableSorting(true);

Custom IDs are optional. SimpleDownloader generates an ID when setId() is not used. An active task cannot be replaced by another task with the same ID.

Passing 0 for a connection or read timeout keeps the OkHttp default.

Use your own HTTP client:

OkHttpClientclient = newOkHttpClient.Builder()
.followRedirects(true)
.build();
SimpleDownloader.with(context)
.setHttpClient(client);

The HTTP client cannot be replaced while a worker is running or already scheduled.

Use a custom task list order:

SimpleDownloader.with(context)
.setTaskComparator(myComparator);

Formatting helpers

Stringsize = Formator.formatBytes(bytes);
Stringspeed = Formator.formatSpeed(bytesPerSecond);
Stringeta = Formator.formatEta(etaMs);

TypeResolver is used internally, but it is also available for resolving file extensions and MIME types for you 🙂.

Cleanup

When listeners and observers are owned by an Activity or Fragment, release them using the same owner object given to with(...):

@OverrideprotectedvoidonDestroy() {
SimpleDownloader.releaseCallbacks(this);
SimpleDownloader.releaseObserver(observer);
super.onDestroy();
}

Call shutdown() only when you intentionally want to stop the library and release all workers, network callbacks, HTTP resources, thumbnails, database, etc:

SimpleDownloader.shutdown();

You do not need to call shutdown() normally or when an Activity is destroyed.

Support

Found a problem or have a suggestion? Open an issue: https://github.com/jeetarc/SimpleDownloader/issues

Copyright © 2026 Jeet / Jeetarc.

About

A modern Android download manager library designed to simplify complex file downloading behind a simple API

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

SimpleDownloader

JitPackGitHub releaseAndroid API

SimpleDownloader is an Android download library project. It handles the parts that usually make downloading difficult: queues, concurrent downloads, pause and resume, unstable networks, scoped storage, task persistence, foreground, notifications, progress updates, etc.

The simple API:

DownloadTasktask = SimpleDownloader.with(context)
.enableForeground(true)
.setOutput(folderPath, FileName.AUTO) // Or .setOutput(folderUri, FileName)
.setFileUrl(fileUrl)
.startDownload();

Features

  • Multiple downloads with queue and priority support
  • automatic download concurrency
  • Pause, resume, cancel, retry, remove, requeue, and force download
  • Resume using HTTP range requests
  • Network loss handling and Wi-Fi-only downloads
  • Task persistence and restoration with filters.
  • Complete built-in storage support for filesystem paths, SAF/document-tree output, and MediaStore output.
  • Built-in Subfolder support
  • Built-in file overwrite support
  • Automatic file name and MIME type resolution
  • Progress, speed, ETA, status, and lifecycle callbacks
  • Progress, completion, and error notifications with actions, thumbnails, and more.
  • Optional foreground execution
  • Custom OkHttpClient support
  • Custom task Comparator
  • RetryPolicy, timeouts, headers, cookies, checksums and many more
  • Minimum Android version: API 21

Installation

Add JitPack to your repositories:

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
}
}

Add SimpleDownloader to your app module:

dependencies {
implementation "com.github.jeetarc:SimpleDownloader:1.0.0-beta.3"
}

SimpleDownloader is built with Java 8 and compileSdk 35.

Setup

If you enable notifications, add:

<uses-permissionandroid:name="android.permission.POST_NOTIFICATIONS" />

On Android 13 and newer, request this permission at runtime.

If you enable enableForeground(true), also add:

<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE" />
<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

Foreground mode can automatically enable notifications, also you can use enableNotifications(true).

If using normal file path, add:

<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permissionandroid:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

And request the permission at runtime. Storage permissions are not needed when saving to app-specific folder or using folderUri via MediaStore or Storage Access Framework.

All other permissions are added by default.

Quick start

SimpleDownloader has three built-in storage modes.

1. Download into a file system folder:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderPath, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

2. Download into a MediaStore collection (Android 10+):

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(MediaStore.Downloads.EXTERNAL_CONTENT_URI, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

You can also use other MediaStore collections:

MediaStore.Images.Media.EXTERNAL_CONTENT_URIMediaStore.Video.Media.EXTERNAL_CONTENT_URIMediaStore.Audio.Media.EXTERNAL_CONTENT_URI

3. Download into a selected folder via SAF/document-tree:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(treeUri, FileName.AUTO)
.setFileUrl("https://example.com/files/document.pdf")
.startDownload();

Keep the URI permission:

intflags = Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
getContentResolver().takePersistableUriPermission(treeUri, flags);

Use a custom name if needed:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, "example.mp4")
.setFileUrl(fileUrl)
.startDownload();

SimpleDownloader creates a new file inside each folder. If a file with the same name already exists, it creates a unique name automatically.

Overwrite a file:

Use a DocumentFile URI or a specific MediaStore item URI:

DownloadTasktask = SimpleDownloader.with(context)
.overwrite(fileUri)
.setFileUrl(fileUrl)
.startDownload();

Or use a file path:

DownloadTasktask = SimpleDownloader.with(context)
.overwrite(outputFile.getAbsolutePath())
.setFileUrl(fileUrl)
.startDownload();

Subfolder:

A subfolder is an optional folder inside the main output folder where you want the downloaded file to be saved.

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, FileName.AUTO)
.setSubFolder("app")
.setFileUrl(fileUrl)
.startDownload();

Subfolders can be nested, for example "app/videos". They work with filesystem paths, SAF/document-tree output, and MediaStore output.

Listener for download updates

All DownloadListener callbacks run on the main thread. Every method is optional.

DownloadListenerlistener = newDownloadListener() {
@OverridepublicvoidonProgress(longid, intprogress, longspeed, longetaMs, DownloadTasktask) {
progressBar.setProgress(progress);
speedText.setText(Formator.formatSpeed(speed));
etaText.setText(Formator.formatEta(etaMs));
}
@OverridepublicvoidonComplete(longid, UrioutputUri, DownloadTasktask) {
// The download finished successfully.
}
@OverridepublicvoidonError(longid, UrioutputUri, Exceptionerror, DownloadTasktask) {
// The download failed.
}
};
DownloadTasktask = SimpleDownloader.with(this)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.addListener(listener)
.startDownload();

Other callbacks include:

onStart(longid, DownloadTasktask) {}
onQueued(longid, intposition, DownloadTasktask) {}
onPaused(longid, DownloadTasktask) {}
onResumed(longid, DownloadTasktask) {}
onCancelled(longid, DownloadTasktask) {}
onRemoved(longid, booleanoutputDeleted, DownloadTasktask) {}
onRetry(longid, intattempt, DownloadTasktask) {}
onWaitingForNetwork(longid, intnetworkType, DownloadTasktask) {}
onStatusChanged(longid, Statusstatus, DownloadTasktask) {}
onActiveChanged(longid, booleanisActive, DownloadTasktask) {}
onLifecycleChanged(longid, intlifecycle, DownloadTasktask) {}

onStart() can run again on resume and retry. Use onLifecycleChanged() to know the start or end of the full task lifecycle.

You can also add listeners directly on a task:

task.addListener(listener);
task.removeListener(listener);
task.releaseCallbacks();

Observe the task list

Use TaskListObserver when showing all downloads in a list, RecyclerView, etc.

TaskListObserverobserver = newTaskListObserver() {
@OverridepublicvoidonTasksChanged(List<DownloadTask> tasks) {
// The list order changed
}
@OverridepublicvoidonTaskUpdated(longid, DownloadTasktask) {
// Update only the matching item
}
};
SimpleDownloaderdownloader = SimpleDownloader.with(this)
.addObserver(observer);

The list passed to onTasksChanged() is an unmodifiable snapshot. DownloadTask objects inside it are live and can update.

Release the observer when not needed:

SimpleDownloader.releaseObserver(observer);

Control a task

task.pause();
task.resume();
task.cancel();
task.retry();
task.requeue();
task.remove();
task.forceDownload();

Check:

task.canPause();
task.canResume();
task.canRetry();

Change some task settings after creation:

task.setPriority(Priority.HIGH);
task.setWifiOnly(true);
task.setLockedInQueue(true);
task.setDeleteOnRemoval(true);

Note:

  • cancel() stops the task and deletes its output.
  • remove() removes the task from SimpleDownloader register.
  • remove() deletes the output only when setDeleteOnRemoval(true) is enabled.
  • forceDownload() starts a queued task even when it is locked. It doesn't care about concurrency limit

Task info

task.getId();
task.getFileUrl();
task.getFileName();
task.getMimeType();
task.getOutputUri();
task.getOutputFile();
task.getOutputDocumentFile();
task.getOutputFolderUri();
task.getOutputFolderPath();
task.getSubFolderPath();
task.getOutputPath();
task.getOverwriteUri();
task.getProgress();
task.getDownloadedBytes();
task.getTotalBytes();
task.getSpeed();
task.getEtaMs();
task.getStatus();
task.getPriority();
task.getError();
task.getCreatedAt();
task.getMaxRetryCount();
task.isActive();
task.isQueued();
task.isPaused();
task.isWaitingForNetwork();
task.isFinished();
task.isOccupiedSlot();

Reuse a configured instance

You can configure a SimpleDownloader instance once inside onCreate() and reuse it:

privateSimpleDownloaderdownloader;
@OverrideprotectedvoidonCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
downloader = SimpleDownloader.with(this)
.setMaxConcurrent(3)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setBufferSize(16 * 1024)
.enableHistory(true);
}
// You can configure all fields you want be the same across downloads.

Then use the configured instance whenever you start a download:

DownloadTasktask = downloader
.setOutput(folderUri, fileName)
.setMimeType(mimeType)
.setFileUrl(fileUrl)
.startDownload();

You can also use the same instance to restore tasks:

List<DownloadTask> tasks = downloader.restoreTasks();

Other common settings, such as retry policy, user agent, priority, Wi-Fi-only mode, notifications, foreground execution, etc can also be configured on the same downloader instance, instead of being set again for every download.

File URL, output destination, and custom ID should be set again before starting each task.

Global controls

Control a task by ID:

SimpleDownloader.pause(id);
SimpleDownloader.resume(id);
SimpleDownloader.cancel(id);
SimpleDownloader.retry(id);
SimpleDownloader.requeue(id);
SimpleDownloader.remove(id);
SimpleDownloader.forceDownload(id);

Control multiple tasks:

SimpleDownloader.pauseAll();
SimpleDownloader.resumeAll();
SimpleDownloader.cancelAll();
SimpleDownloader.retryAll();
SimpleDownloader.requeueAll();
SimpleDownloader.removeAll();
SimpleDownloader.pause(Priority.LOW);
SimpleDownloader.resumeAll(Priority.HIGH);
SimpleDownloader.remove(Status.COMPLETED);
SimpleDownloader.remove(Priority.LOW);

get task from registry:

DownloadTasktask = SimpleDownloader.getTask(id);
List<DownloadTask> all = SimpleDownloader.getTasks();
List<DownloadTask> completed = SimpleDownloader.getTasks(TaskField.STATUS, Status.COMPLETED);
SimpleDownloader.getTask(TaskField.FILE_NAME, fileName) // returns latest matching single task, null if not found.inttotal = SimpleDownloader.getTotalCount();
intactive = SimpleDownloader.getActiveCount();
intqueued = SimpleDownloader.getQueuedCount();
intoccupied = SimpleDownloader.getOccupiedCount();
intconcurrency = SimpleDownloader.getEffectiveMaxConcurrent();

Update a task by ID:

SimpleDownloader.setPriority(id, Priority.NEXT);
SimpleDownloader.setWifiOnly(id, true);
SimpleDownloader.setLockedInQueue(id, true);
SimpleDownloader.setDeleteOnRemoval(id, true);

Restore tasks

Restore methods immediately restore saved tasks using the downloader's current configuration.

Important: Configure all downloader settings before calling any restore method. Restore methods should always be the last configuration call, otherwise restored tasks may not receive the configuration called after restore.

Automatic Restore:

Enable automatic restoration of previously saved download tasks:

SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setRetryPolicy(...)
.enableNotifications(true)
.enableForeground(true)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setNotification(...)
.setAutoRestore(true); // Keep this last.

When enabled, SimpleDownloader automatically restores previously saved tasks and resumes eligible ones. You do not need to call restoreTasks() separately when using auto restore. Auto restore is disabled by default.

Explicit restore when your app ready to show or continue downloads:

restored = downloader.restoreTasks();

Active tasks are restored as paused. Resume the tasks you want to continue:

SimpleDownloader.resumeAll();
//orfor (DownloadTasktask : restored) {
task.resume();
}

Restore matching tasks:

restoreTasks(...) returns an empty list when no match:

List<DownloadTask> paused = downloader.restoreTasks(TaskField.STATUS, Status.PAUSED);
List<DownloadTask> videos = downloader.restoreTasks(TaskField.MIME_TYPE, "video/mp4");
List<DownloadTask> matchingUrl = downloader.restoreTasks(TaskField.FILE_URL, fileUrl);

restoreTask() returns a single newest matching task, or null when no match:

DownloadTasktask = downloader.restoreTask(TaskField.FILE_URL, fileUrl);
if (task != null) task.resume();

Available fields:

TaskField.IDTaskField.FILE_URLTaskField.STATUSTaskField.PRIORITYTaskField.MIME_TYPETaskField.FILE_NAMETaskField.CREATED_ATTaskField.WIFI_ONLYTaskField.BUFFER_SIZETaskField.PROGRESSTaskField.BYTES_DOWNLOADEDTaskField.TOTAL_BYTESTaskField.OUTPUT_URITaskField.OUTPUT_PATHTaskField.OVERWRITE_URITaskField.OVERWRITE_PATHTaskField.OUTPUT_FOLDER_URITaskField.OUTPUT_FOLDER_PATHTaskField.SUB_FOLDER_PATHTaskField.DELETE_ON_REMOVALTaskField.LOCKED_IN_QUEUE

Finished tasks are kept in the database only when history is enabled:

SimpleDownloader.with(context)
.enableHistory(true);

Queue and concurrency

By default, SimpleDownloader use automatic concurrency. It starts with 1 and go up to 10 slot beased on download speed.

auto concurrency starts when setMaxConcurrent(0) or not set.

Stop queued tasks from starting automatically when a slot becomes free:

SimpleDownloader.with(context)
.setDownloadOnSlotFree(false);

Lock task in the queue:

task.setLockedInQueue(true);

Priorities :

Priority.NEXTPriority.HIGHPriority.NORMALPriority.LOW// NEXT > HIGH > NORMAL > LOW

Task statuses:

Status.STARTINGStatus.QUEUEDStatus.CONNECTINGStatus.DOWNLOADINGStatus.PAUSEDStatus.CANCELLEDStatus.WAITING_FOR_NETWORKStatus.RETRYINGStatus.COMPLETEDStatus.FAILED

Retry policy

The default policy with one automatic retry. Configure:

RetryPolicyretryPolicy = RetryPolicy.builder()
.maxRetryCount(3)
.initialDelayMs(1000)
.multiplier(2.0)
.maxDelayMs(30_000)
.build();
SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setRetryPolicy(retryPolicy);

Retry settings stay on the SimpleDownloader instance used to create or restore tasks.

MIME Type

A MIME type (media type) is a used to identify the format of a file, SimpleDownloader resolves it automatically, but can be set explicitly:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, "manual.pdf")
.setMimeType("application/pdf")
.setFileUrl(fileUrl)
.startDownload();

Or use automatic resolution:

.setMimeType(MimeType.AUTO) // or MimeType.FROM_NAME

Network, headers, cookies

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.setHeader("Authorization", "Bearer " + token)
.setHeader("Referer", pageUrl)
.setCookies("session=" + sessionId)
.setWifiOnly(true)
.startDownload();

Add headers:

Map<String, String> headers = newHashMap<>();
headers.put("Authorization", "Bearer " + token);
headers.put("Referer", pageUrl);
SimpleDownloader.with(context)
.setHeaders(headers);

Network info:

booleanavailable = SimpleDownloader.isNetworkAvailable();
intnetworkType = SimpleDownloader.getNetworkType();

Network constants:

NETWORK_TYPE_NONENETWORK_TYPE_UNKNOWNNETWORK_TYPE_WIFINETWORK_TYPE_CELLULARNETWORK_TYPE_ETHERNETNETWORK_TYPE_BLUETOOTHNETWORK_TYPE_VPNNETWORK_TYPE_USBNETWORK_TYPE_ROAMING

Waiting tasks resume when the network becomes available by default. Change with:

SimpleDownloader.with(context)
.enableResumeOnNetworkGain(false);

Notifications

Notifications are optional and disabled by default.

DownloadNotificationnotification = newDownloadNotification()
.setSmallIcon(R.drawable.ic_download)
.setCompleteIcon(R.drawable.ic_download_done)
.setErrorIcon(R.drawable.ic_download_error)
.setColorAccent(0xFF0087E5)
.setShowPauseAction(true)
.setShowCancelAction(true)
.setShowRetryAction(true);
DownloadTasktask = SimpleDownloader.with(context)
.enableNotifications(true)
.setNotification(notification)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

DownloadNotification configuration is optional, only use enableNotifications(true) if you don't want to customize. It will use the default config.

Run tasks using a foreground service:

SimpleDownloader.with(context)
.enableForeground(true)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

SimpleDownloader has built in thumbnail system for notifications, it can generate a thumbnail from a video, image, audio (album art), APK, PDF automatically.

You can also set a thumbnail:

notification.setThumbnail(bitmap);

Or load it from a URL:

notification.setThumbnailUrl(thumbnailUrl, thumbHeaders);
// pass null for headers, if it not available

DownloadNotification can configure the channel, importance, lock-screen visibility, sound, vibration, color, update interval, actions, etc.

Checksums

Verify the completed file with algorithms supported by MessageDigest. Like SHA-256, SHA-1, or MD5:

DownloadTasktask = SimpleDownloader.with(context)
.setChecksum("SHA-256", expectedChecksum)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

File names and MIME types modes

FileName.AUTOFileName.TIME_BASED
MimeType.AUTOMimeType.FROM_NAME

AUTO uses the URL, response headers, file extension, and content type to resolve the name and MIME type.

Error handling

Failures exceptions are instance of DownloadException:

@OverridepublicvoidonError(longid, UrioutputUri, Exceptionerror, DownloadTasktask) {
if (!(errorinstanceofDownloadException)) return;
DownloadExceptionfailure = (DownloadException) error;
DownloadException.Typetype = failure.getType();
inthttpCode = failure.getCode();
booleanretryable = failure.isRetryable();
Throwablecause = failure.getCause();
}

Error types:

NETWORK_LOSTTIMEOUTDNS_ERRORSSL_ERRORHTTP_ERRORENOSPCFILE_ERRORSTORAGE_PERMISSION_DENIEDOUTPUT_INVALIDRANGE_NOT_SUPPORTEDEMPTY_RESPONSECHECKSUM_FAILEDCANCELLEDUNKNOWN

Other configuration

SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setId(customId)
.setUserAgent(userAgent)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setBufferSize(16 * 1024)
.enableHistory(true)
.enableSorting(true);

Custom IDs are optional. SimpleDownloader generates an ID when setId() is not used. An active task cannot be replaced by another task with the same ID.

Passing 0 for a connection or read timeout keeps the OkHttp default.

Use your own HTTP client:

OkHttpClientclient = newOkHttpClient.Builder()
.followRedirects(true)
.build();
SimpleDownloader.with(context)
.setHttpClient(client);

The HTTP client cannot be replaced while a worker is running or already scheduled.

Use a custom task list order:

SimpleDownloader.with(context)
.setTaskComparator(myComparator);

Formatting helpers

Stringsize = Formator.formatBytes(bytes);
Stringspeed = Formator.formatSpeed(bytesPerSecond);
Stringeta = Formator.formatEta(etaMs);

TypeResolver is used internally, but it is also available for resolving file extensions and MIME types for you 🙂.

Cleanup

When listeners and observers are owned by an Activity or Fragment, release them using the same owner object given to with(...):

@OverrideprotectedvoidonDestroy() {
SimpleDownloader.releaseCallbacks(this);
SimpleDownloader.releaseObserver(observer);
super.onDestroy();
}

Call shutdown() only when you intentionally want to stop the library and release all workers, network callbacks, HTTP resources, thumbnails, database, etc:

SimpleDownloader.shutdown();

You do not need to call shutdown() normally or when an Activity is destroyed.

Support

Found a problem or have a suggestion? Open an issue: https://github.com/jeetarc/SimpleDownloader/issues

Copyright © 2026 Jeet / Jeetarc.

About

A modern Android download manager library designed to simplify complex file downloading behind a simple API

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

SimpleDownloader

JitPackGitHub releaseAndroid API

SimpleDownloader is an Android download library project. It handles the parts that usually make downloading difficult: queues, concurrent downloads, pause and resume, unstable networks, scoped storage, task persistence, foreground, notifications, progress updates, etc.

The simple API:

DownloadTasktask = SimpleDownloader.with(context)
.enableForeground(true)
.setOutput(folderPath, FileName.AUTO) // Or .setOutput(folderUri, FileName)
.setFileUrl(fileUrl)
.startDownload();

Features

  • Multiple downloads with queue and priority support
  • automatic download concurrency
  • Pause, resume, cancel, retry, remove, requeue, and force download
  • Resume using HTTP range requests
  • Network loss handling and Wi-Fi-only downloads
  • Task persistence and restoration with filters.
  • Complete built-in storage support for filesystem paths, SAF/document-tree output, and MediaStore output.
  • Built-in Subfolder support
  • Built-in file overwrite support
  • Automatic file name and MIME type resolution
  • Progress, speed, ETA, status, and lifecycle callbacks
  • Progress, completion, and error notifications with actions, thumbnails, and more.
  • Optional foreground execution
  • Custom OkHttpClient support
  • Custom task Comparator
  • RetryPolicy, timeouts, headers, cookies, checksums and many more
  • Minimum Android version: API 21

Installation

Add JitPack to your repositories:

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
}
}

Add SimpleDownloader to your app module:

dependencies {
implementation "com.github.jeetarc:SimpleDownloader:1.0.0-beta.3"
}

SimpleDownloader is built with Java 8 and compileSdk 35.

Setup

If you enable notifications, add:

<uses-permissionandroid:name="android.permission.POST_NOTIFICATIONS" />

On Android 13 and newer, request this permission at runtime.

If you enable enableForeground(true), also add:

<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE" />
<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

Foreground mode can automatically enable notifications, also you can use enableNotifications(true).

If using normal file path, add:

<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permissionandroid:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

And request the permission at runtime. Storage permissions are not needed when saving to app-specific folder or using folderUri via MediaStore or Storage Access Framework.

All other permissions are added by default.

Quick start

SimpleDownloader has three built-in storage modes.

1. Download into a file system folder:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderPath, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

2. Download into a MediaStore collection (Android 10+):

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(MediaStore.Downloads.EXTERNAL_CONTENT_URI, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

You can also use other MediaStore collections:

MediaStore.Images.Media.EXTERNAL_CONTENT_URIMediaStore.Video.Media.EXTERNAL_CONTENT_URIMediaStore.Audio.Media.EXTERNAL_CONTENT_URI

3. Download into a selected folder via SAF/document-tree:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(treeUri, FileName.AUTO)
.setFileUrl("https://example.com/files/document.pdf")
.startDownload();

Keep the URI permission:

intflags = Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
getContentResolver().takePersistableUriPermission(treeUri, flags);

Use a custom name if needed:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, "example.mp4")
.setFileUrl(fileUrl)
.startDownload();

SimpleDownloader creates a new file inside each folder. If a file with the same name already exists, it creates a unique name automatically.

Overwrite a file:

Use a DocumentFile URI or a specific MediaStore item URI:

DownloadTasktask = SimpleDownloader.with(context)
.overwrite(fileUri)
.setFileUrl(fileUrl)
.startDownload();

Or use a file path:

DownloadTasktask = SimpleDownloader.with(context)
.overwrite(outputFile.getAbsolutePath())
.setFileUrl(fileUrl)
.startDownload();

Subfolder:

A subfolder is an optional folder inside the main output folder where you want the downloaded file to be saved.

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, FileName.AUTO)
.setSubFolder("app")
.setFileUrl(fileUrl)
.startDownload();

Subfolders can be nested, for example "app/videos". They work with filesystem paths, SAF/document-tree output, and MediaStore output.

Listener for download updates

All DownloadListener callbacks run on the main thread. Every method is optional.

DownloadListenerlistener = newDownloadListener() {
@OverridepublicvoidonProgress(longid, intprogress, longspeed, longetaMs, DownloadTasktask) {
progressBar.setProgress(progress);
speedText.setText(Formator.formatSpeed(speed));
etaText.setText(Formator.formatEta(etaMs));
}
@OverridepublicvoidonComplete(longid, UrioutputUri, DownloadTasktask) {
// The download finished successfully.
}
@OverridepublicvoidonError(longid, UrioutputUri, Exceptionerror, DownloadTasktask) {
// The download failed.
}
};
DownloadTasktask = SimpleDownloader.with(this)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.addListener(listener)
.startDownload();

Other callbacks include:

onStart(longid, DownloadTasktask) {}
onQueued(longid, intposition, DownloadTasktask) {}
onPaused(longid, DownloadTasktask) {}
onResumed(longid, DownloadTasktask) {}
onCancelled(longid, DownloadTasktask) {}
onRemoved(longid, booleanoutputDeleted, DownloadTasktask) {}
onRetry(longid, intattempt, DownloadTasktask) {}
onWaitingForNetwork(longid, intnetworkType, DownloadTasktask) {}
onStatusChanged(longid, Statusstatus, DownloadTasktask) {}
onActiveChanged(longid, booleanisActive, DownloadTasktask) {}
onLifecycleChanged(longid, intlifecycle, DownloadTasktask) {}

onStart() can run again on resume and retry. Use onLifecycleChanged() to know the start or end of the full task lifecycle.

You can also add listeners directly on a task:

task.addListener(listener);
task.removeListener(listener);
task.releaseCallbacks();

Observe the task list

Use TaskListObserver when showing all downloads in a list, RecyclerView, etc.

TaskListObserverobserver = newTaskListObserver() {
@OverridepublicvoidonTasksChanged(List<DownloadTask> tasks) {
// The list order changed
}
@OverridepublicvoidonTaskUpdated(longid, DownloadTasktask) {
// Update only the matching item
}
};
SimpleDownloaderdownloader = SimpleDownloader.with(this)
.addObserver(observer);

The list passed to onTasksChanged() is an unmodifiable snapshot. DownloadTask objects inside it are live and can update.

Release the observer when not needed:

SimpleDownloader.releaseObserver(observer);

Control a task

task.pause();
task.resume();
task.cancel();
task.retry();
task.requeue();
task.remove();
task.forceDownload();

Check:

task.canPause();
task.canResume();
task.canRetry();

Change some task settings after creation:

task.setPriority(Priority.HIGH);
task.setWifiOnly(true);
task.setLockedInQueue(true);
task.setDeleteOnRemoval(true);

Note:

  • cancel() stops the task and deletes its output.
  • remove() removes the task from SimpleDownloader register.
  • remove() deletes the output only when setDeleteOnRemoval(true) is enabled.
  • forceDownload() starts a queued task even when it is locked. It doesn't care about concurrency limit

Task info

task.getId();
task.getFileUrl();
task.getFileName();
task.getMimeType();
task.getOutputUri();
task.getOutputFile();
task.getOutputDocumentFile();
task.getOutputFolderUri();
task.getOutputFolderPath();
task.getSubFolderPath();
task.getOutputPath();
task.getOverwriteUri();
task.getProgress();
task.getDownloadedBytes();
task.getTotalBytes();
task.getSpeed();
task.getEtaMs();
task.getStatus();
task.getPriority();
task.getError();
task.getCreatedAt();
task.getMaxRetryCount();
task.isActive();
task.isQueued();
task.isPaused();
task.isWaitingForNetwork();
task.isFinished();
task.isOccupiedSlot();

Reuse a configured instance

You can configure a SimpleDownloader instance once inside onCreate() and reuse it:

privateSimpleDownloaderdownloader;
@OverrideprotectedvoidonCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
downloader = SimpleDownloader.with(this)
.setMaxConcurrent(3)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setBufferSize(16 * 1024)
.enableHistory(true);
}
// You can configure all fields you want be the same across downloads.

Then use the configured instance whenever you start a download:

DownloadTasktask = downloader
.setOutput(folderUri, fileName)
.setMimeType(mimeType)
.setFileUrl(fileUrl)
.startDownload();

You can also use the same instance to restore tasks:

List<DownloadTask> tasks = downloader.restoreTasks();

Other common settings, such as retry policy, user agent, priority, Wi-Fi-only mode, notifications, foreground execution, etc can also be configured on the same downloader instance, instead of being set again for every download.

File URL, output destination, and custom ID should be set again before starting each task.

Global controls

Control a task by ID:

SimpleDownloader.pause(id);
SimpleDownloader.resume(id);
SimpleDownloader.cancel(id);
SimpleDownloader.retry(id);
SimpleDownloader.requeue(id);
SimpleDownloader.remove(id);
SimpleDownloader.forceDownload(id);

Control multiple tasks:

SimpleDownloader.pauseAll();
SimpleDownloader.resumeAll();
SimpleDownloader.cancelAll();
SimpleDownloader.retryAll();
SimpleDownloader.requeueAll();
SimpleDownloader.removeAll();
SimpleDownloader.pause(Priority.LOW);
SimpleDownloader.resumeAll(Priority.HIGH);
SimpleDownloader.remove(Status.COMPLETED);
SimpleDownloader.remove(Priority.LOW);

get task from registry:

DownloadTasktask = SimpleDownloader.getTask(id);
List<DownloadTask> all = SimpleDownloader.getTasks();
List<DownloadTask> completed = SimpleDownloader.getTasks(TaskField.STATUS, Status.COMPLETED);
SimpleDownloader.getTask(TaskField.FILE_NAME, fileName) // returns latest matching single task, null if not found.inttotal = SimpleDownloader.getTotalCount();
intactive = SimpleDownloader.getActiveCount();
intqueued = SimpleDownloader.getQueuedCount();
intoccupied = SimpleDownloader.getOccupiedCount();
intconcurrency = SimpleDownloader.getEffectiveMaxConcurrent();

Update a task by ID:

SimpleDownloader.setPriority(id, Priority.NEXT);
SimpleDownloader.setWifiOnly(id, true);
SimpleDownloader.setLockedInQueue(id, true);
SimpleDownloader.setDeleteOnRemoval(id, true);

Restore tasks

Restore methods immediately restore saved tasks using the downloader's current configuration.

Important: Configure all downloader settings before calling any restore method. Restore methods should always be the last configuration call, otherwise restored tasks may not receive the configuration called after restore.

Automatic Restore:

Enable automatic restoration of previously saved download tasks:

SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setRetryPolicy(...)
.enableNotifications(true)
.enableForeground(true)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setNotification(...)
.setAutoRestore(true); // Keep this last.

When enabled, SimpleDownloader automatically restores previously saved tasks and resumes eligible ones. You do not need to call restoreTasks() separately when using auto restore. Auto restore is disabled by default.

Explicit restore when your app ready to show or continue downloads:

restored = downloader.restoreTasks();

Active tasks are restored as paused. Resume the tasks you want to continue:

SimpleDownloader.resumeAll();
//orfor (DownloadTasktask : restored) {
task.resume();
}

Restore matching tasks:

restoreTasks(...) returns an empty list when no match:

List<DownloadTask> paused = downloader.restoreTasks(TaskField.STATUS, Status.PAUSED);
List<DownloadTask> videos = downloader.restoreTasks(TaskField.MIME_TYPE, "video/mp4");
List<DownloadTask> matchingUrl = downloader.restoreTasks(TaskField.FILE_URL, fileUrl);

restoreTask() returns a single newest matching task, or null when no match:

DownloadTasktask = downloader.restoreTask(TaskField.FILE_URL, fileUrl);
if (task != null) task.resume();

Available fields:

TaskField.IDTaskField.FILE_URLTaskField.STATUSTaskField.PRIORITYTaskField.MIME_TYPETaskField.FILE_NAMETaskField.CREATED_ATTaskField.WIFI_ONLYTaskField.BUFFER_SIZETaskField.PROGRESSTaskField.BYTES_DOWNLOADEDTaskField.TOTAL_BYTESTaskField.OUTPUT_URITaskField.OUTPUT_PATHTaskField.OVERWRITE_URITaskField.OVERWRITE_PATHTaskField.OUTPUT_FOLDER_URITaskField.OUTPUT_FOLDER_PATHTaskField.SUB_FOLDER_PATHTaskField.DELETE_ON_REMOVALTaskField.LOCKED_IN_QUEUE

Finished tasks are kept in the database only when history is enabled:

SimpleDownloader.with(context)
.enableHistory(true);

Queue and concurrency

By default, SimpleDownloader use automatic concurrency. It starts with 1 and go up to 10 slot beased on download speed.

auto concurrency starts when setMaxConcurrent(0) or not set.

Stop queued tasks from starting automatically when a slot becomes free:

SimpleDownloader.with(context)
.setDownloadOnSlotFree(false);

Lock task in the queue:

task.setLockedInQueue(true);

Priorities :

Priority.NEXTPriority.HIGHPriority.NORMALPriority.LOW// NEXT > HIGH > NORMAL > LOW

Task statuses:

Status.STARTINGStatus.QUEUEDStatus.CONNECTINGStatus.DOWNLOADINGStatus.PAUSEDStatus.CANCELLEDStatus.WAITING_FOR_NETWORKStatus.RETRYINGStatus.COMPLETEDStatus.FAILED

Retry policy

The default policy with one automatic retry. Configure:

RetryPolicyretryPolicy = RetryPolicy.builder()
.maxRetryCount(3)
.initialDelayMs(1000)
.multiplier(2.0)
.maxDelayMs(30_000)
.build();
SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setRetryPolicy(retryPolicy);

Retry settings stay on the SimpleDownloader instance used to create or restore tasks.

MIME Type

A MIME type (media type) is a used to identify the format of a file, SimpleDownloader resolves it automatically, but can be set explicitly:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, "manual.pdf")
.setMimeType("application/pdf")
.setFileUrl(fileUrl)
.startDownload();

Or use automatic resolution:

.setMimeType(MimeType.AUTO) // or MimeType.FROM_NAME

Network, headers, cookies

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.setHeader("Authorization", "Bearer " + token)
.setHeader("Referer", pageUrl)
.setCookies("session=" + sessionId)
.setWifiOnly(true)
.startDownload();

Add headers:

Map<String, String> headers = newHashMap<>();
headers.put("Authorization", "Bearer " + token);
headers.put("Referer", pageUrl);
SimpleDownloader.with(context)
.setHeaders(headers);

Network info:

booleanavailable = SimpleDownloader.isNetworkAvailable();
intnetworkType = SimpleDownloader.getNetworkType();

Network constants:

NETWORK_TYPE_NONENETWORK_TYPE_UNKNOWNNETWORK_TYPE_WIFINETWORK_TYPE_CELLULARNETWORK_TYPE_ETHERNETNETWORK_TYPE_BLUETOOTHNETWORK_TYPE_VPNNETWORK_TYPE_USBNETWORK_TYPE_ROAMING

Waiting tasks resume when the network becomes available by default. Change with:

SimpleDownloader.with(context)
.enableResumeOnNetworkGain(false);

Notifications

Notifications are optional and disabled by default.

DownloadNotificationnotification = newDownloadNotification()
.setSmallIcon(R.drawable.ic_download)
.setCompleteIcon(R.drawable.ic_download_done)
.setErrorIcon(R.drawable.ic_download_error)
.setColorAccent(0xFF0087E5)
.setShowPauseAction(true)
.setShowCancelAction(true)
.setShowRetryAction(true);
DownloadTasktask = SimpleDownloader.with(context)
.enableNotifications(true)
.setNotification(notification)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

DownloadNotification configuration is optional, only use enableNotifications(true) if you don't want to customize. It will use the default config.

Run tasks using a foreground service:

SimpleDownloader.with(context)
.enableForeground(true)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

SimpleDownloader has built in thumbnail system for notifications, it can generate a thumbnail from a video, image, audio (album art), APK, PDF automatically.

You can also set a thumbnail:

notification.setThumbnail(bitmap);

Or load it from a URL:

notification.setThumbnailUrl(thumbnailUrl, thumbHeaders);
// pass null for headers, if it not available

DownloadNotification can configure the channel, importance, lock-screen visibility, sound, vibration, color, update interval, actions, etc.

Checksums

Verify the completed file with algorithms supported by MessageDigest. Like SHA-256, SHA-1, or MD5:

DownloadTasktask = SimpleDownloader.with(context)
.setChecksum("SHA-256", expectedChecksum)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

File names and MIME types modes

FileName.AUTOFileName.TIME_BASED
MimeType.AUTOMimeType.FROM_NAME

AUTO uses the URL, response headers, file extension, and content type to resolve the name and MIME type.

Error handling

Failures exceptions are instance of DownloadException:

@OverridepublicvoidonError(longid, UrioutputUri, Exceptionerror, DownloadTasktask) {
if (!(errorinstanceofDownloadException)) return;
DownloadExceptionfailure = (DownloadException) error;
DownloadException.Typetype = failure.getType();
inthttpCode = failure.getCode();
booleanretryable = failure.isRetryable();
Throwablecause = failure.getCause();
}

Error types:

NETWORK_LOSTTIMEOUTDNS_ERRORSSL_ERRORHTTP_ERRORENOSPCFILE_ERRORSTORAGE_PERMISSION_DENIEDOUTPUT_INVALIDRANGE_NOT_SUPPORTEDEMPTY_RESPONSECHECKSUM_FAILEDCANCELLEDUNKNOWN

Other configuration

SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setId(customId)
.setUserAgent(userAgent)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setBufferSize(16 * 1024)
.enableHistory(true)
.enableSorting(true);

Custom IDs are optional. SimpleDownloader generates an ID when setId() is not used. An active task cannot be replaced by another task with the same ID.

Passing 0 for a connection or read timeout keeps the OkHttp default.

Use your own HTTP client:

OkHttpClientclient = newOkHttpClient.Builder()
.followRedirects(true)
.build();
SimpleDownloader.with(context)
.setHttpClient(client);

The HTTP client cannot be replaced while a worker is running or already scheduled.

Use a custom task list order:

SimpleDownloader.with(context)
.setTaskComparator(myComparator);

Formatting helpers

Stringsize = Formator.formatBytes(bytes);
Stringspeed = Formator.formatSpeed(bytesPerSecond);
Stringeta = Formator.formatEta(etaMs);

TypeResolver is used internally, but it is also available for resolving file extensions and MIME types for you 🙂.

Cleanup

When listeners and observers are owned by an Activity or Fragment, release them using the same owner object given to with(...):

@OverrideprotectedvoidonDestroy() {
SimpleDownloader.releaseCallbacks(this);
SimpleDownloader.releaseObserver(observer);
super.onDestroy();
}

Call shutdown() only when you intentionally want to stop the library and release all workers, network callbacks, HTTP resources, thumbnails, database, etc:

SimpleDownloader.shutdown();

You do not need to call shutdown() normally or when an Activity is destroyed.

Support

Found a problem or have a suggestion? Open an issue: https://github.com/jeetarc/SimpleDownloader/issues

Copyright © 2026 Jeet / Jeetarc.

About

A modern Android download manager library designed to simplify complex file downloading behind a simple API

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

SimpleDownloader

JitPackGitHub releaseAndroid API

SimpleDownloader is an Android download library project. It handles the parts that usually make downloading difficult: queues, concurrent downloads, pause and resume, unstable networks, scoped storage, task persistence, foreground, notifications, progress updates, etc.

The simple API:

DownloadTasktask = SimpleDownloader.with(context)
.enableForeground(true)
.setOutput(folderPath, FileName.AUTO) // Or .setOutput(folderUri, FileName)
.setFileUrl(fileUrl)
.startDownload();

Features

  • Multiple downloads with queue and priority support
  • automatic download concurrency
  • Pause, resume, cancel, retry, remove, requeue, and force download
  • Resume using HTTP range requests
  • Network loss handling and Wi-Fi-only downloads
  • Task persistence and restoration with filters.
  • Complete built-in storage support for filesystem paths, SAF/document-tree output, and MediaStore output.
  • Built-in Subfolder support
  • Built-in file overwrite support
  • Automatic file name and MIME type resolution
  • Progress, speed, ETA, status, and lifecycle callbacks
  • Progress, completion, and error notifications with actions, thumbnails, and more.
  • Optional foreground execution
  • Custom OkHttpClient support
  • Custom task Comparator
  • RetryPolicy, timeouts, headers, cookies, checksums and many more
  • Minimum Android version: API 21

Installation

Add JitPack to your repositories:

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
}
}

Add SimpleDownloader to your app module:

dependencies {
implementation "com.github.jeetarc:SimpleDownloader:1.0.0-beta.3"
}

SimpleDownloader is built with Java 8 and compileSdk 35.

Setup

If you enable notifications, add:

<uses-permissionandroid:name="android.permission.POST_NOTIFICATIONS" />

On Android 13 and newer, request this permission at runtime.

If you enable enableForeground(true), also add:

<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE" />
<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

Foreground mode can automatically enable notifications, also you can use enableNotifications(true).

If using normal file path, add:

<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permissionandroid:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

And request the permission at runtime. Storage permissions are not needed when saving to app-specific folder or using folderUri via MediaStore or Storage Access Framework.

All other permissions are added by default.

Quick start

SimpleDownloader has three built-in storage modes.

1. Download into a file system folder:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderPath, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

2. Download into a MediaStore collection (Android 10+):

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(MediaStore.Downloads.EXTERNAL_CONTENT_URI, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

You can also use other MediaStore collections:

MediaStore.Images.Media.EXTERNAL_CONTENT_URIMediaStore.Video.Media.EXTERNAL_CONTENT_URIMediaStore.Audio.Media.EXTERNAL_CONTENT_URI

3. Download into a selected folder via SAF/document-tree:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(treeUri, FileName.AUTO)
.setFileUrl("https://example.com/files/document.pdf")
.startDownload();

Keep the URI permission:

intflags = Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
getContentResolver().takePersistableUriPermission(treeUri, flags);

Use a custom name if needed:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, "example.mp4")
.setFileUrl(fileUrl)
.startDownload();

SimpleDownloader creates a new file inside each folder. If a file with the same name already exists, it creates a unique name automatically.

Overwrite a file:

Use a DocumentFile URI or a specific MediaStore item URI:

DownloadTasktask = SimpleDownloader.with(context)
.overwrite(fileUri)
.setFileUrl(fileUrl)
.startDownload();

Or use a file path:

DownloadTasktask = SimpleDownloader.with(context)
.overwrite(outputFile.getAbsolutePath())
.setFileUrl(fileUrl)
.startDownload();

Subfolder:

A subfolder is an optional folder inside the main output folder where you want the downloaded file to be saved.

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, FileName.AUTO)
.setSubFolder("app")
.setFileUrl(fileUrl)
.startDownload();

Subfolders can be nested, for example "app/videos". They work with filesystem paths, SAF/document-tree output, and MediaStore output.

Listener for download updates

All DownloadListener callbacks run on the main thread. Every method is optional.

DownloadListenerlistener = newDownloadListener() {
@OverridepublicvoidonProgress(longid, intprogress, longspeed, longetaMs, DownloadTasktask) {
progressBar.setProgress(progress);
speedText.setText(Formator.formatSpeed(speed));
etaText.setText(Formator.formatEta(etaMs));
}
@OverridepublicvoidonComplete(longid, UrioutputUri, DownloadTasktask) {
// The download finished successfully.
}
@OverridepublicvoidonError(longid, UrioutputUri, Exceptionerror, DownloadTasktask) {
// The download failed.
}
};
DownloadTasktask = SimpleDownloader.with(this)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.addListener(listener)
.startDownload();

Other callbacks include:

onStart(longid, DownloadTasktask) {}
onQueued(longid, intposition, DownloadTasktask) {}
onPaused(longid, DownloadTasktask) {}
onResumed(longid, DownloadTasktask) {}
onCancelled(longid, DownloadTasktask) {}
onRemoved(longid, booleanoutputDeleted, DownloadTasktask) {}
onRetry(longid, intattempt, DownloadTasktask) {}
onWaitingForNetwork(longid, intnetworkType, DownloadTasktask) {}
onStatusChanged(longid, Statusstatus, DownloadTasktask) {}
onActiveChanged(longid, booleanisActive, DownloadTasktask) {}
onLifecycleChanged(longid, intlifecycle, DownloadTasktask) {}

onStart() can run again on resume and retry. Use onLifecycleChanged() to know the start or end of the full task lifecycle.

You can also add listeners directly on a task:

task.addListener(listener);
task.removeListener(listener);
task.releaseCallbacks();

Observe the task list

Use TaskListObserver when showing all downloads in a list, RecyclerView, etc.

TaskListObserverobserver = newTaskListObserver() {
@OverridepublicvoidonTasksChanged(List<DownloadTask> tasks) {
// The list order changed
}
@OverridepublicvoidonTaskUpdated(longid, DownloadTasktask) {
// Update only the matching item
}
};
SimpleDownloaderdownloader = SimpleDownloader.with(this)
.addObserver(observer);

The list passed to onTasksChanged() is an unmodifiable snapshot. DownloadTask objects inside it are live and can update.

Release the observer when not needed:

SimpleDownloader.releaseObserver(observer);

Control a task

task.pause();
task.resume();
task.cancel();
task.retry();
task.requeue();
task.remove();
task.forceDownload();

Check:

task.canPause();
task.canResume();
task.canRetry();

Change some task settings after creation:

task.setPriority(Priority.HIGH);
task.setWifiOnly(true);
task.setLockedInQueue(true);
task.setDeleteOnRemoval(true);

Note:

  • cancel() stops the task and deletes its output.
  • remove() removes the task from SimpleDownloader register.
  • remove() deletes the output only when setDeleteOnRemoval(true) is enabled.
  • forceDownload() starts a queued task even when it is locked. It doesn't care about concurrency limit

Task info

task.getId();
task.getFileUrl();
task.getFileName();
task.getMimeType();
task.getOutputUri();
task.getOutputFile();
task.getOutputDocumentFile();
task.getOutputFolderUri();
task.getOutputFolderPath();
task.getSubFolderPath();
task.getOutputPath();
task.getOverwriteUri();
task.getProgress();
task.getDownloadedBytes();
task.getTotalBytes();
task.getSpeed();
task.getEtaMs();
task.getStatus();
task.getPriority();
task.getError();
task.getCreatedAt();
task.getMaxRetryCount();
task.isActive();
task.isQueued();
task.isPaused();
task.isWaitingForNetwork();
task.isFinished();
task.isOccupiedSlot();

Reuse a configured instance

You can configure a SimpleDownloader instance once inside onCreate() and reuse it:

privateSimpleDownloaderdownloader;
@OverrideprotectedvoidonCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
downloader = SimpleDownloader.with(this)
.setMaxConcurrent(3)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setBufferSize(16 * 1024)
.enableHistory(true);
}
// You can configure all fields you want be the same across downloads.

Then use the configured instance whenever you start a download:

DownloadTasktask = downloader
.setOutput(folderUri, fileName)
.setMimeType(mimeType)
.setFileUrl(fileUrl)
.startDownload();

You can also use the same instance to restore tasks:

List<DownloadTask> tasks = downloader.restoreTasks();

Other common settings, such as retry policy, user agent, priority, Wi-Fi-only mode, notifications, foreground execution, etc can also be configured on the same downloader instance, instead of being set again for every download.

File URL, output destination, and custom ID should be set again before starting each task.

Global controls

Control a task by ID:

SimpleDownloader.pause(id);
SimpleDownloader.resume(id);
SimpleDownloader.cancel(id);
SimpleDownloader.retry(id);
SimpleDownloader.requeue(id);
SimpleDownloader.remove(id);
SimpleDownloader.forceDownload(id);

Control multiple tasks:

SimpleDownloader.pauseAll();
SimpleDownloader.resumeAll();
SimpleDownloader.cancelAll();
SimpleDownloader.retryAll();
SimpleDownloader.requeueAll();
SimpleDownloader.removeAll();
SimpleDownloader.pause(Priority.LOW);
SimpleDownloader.resumeAll(Priority.HIGH);
SimpleDownloader.remove(Status.COMPLETED);
SimpleDownloader.remove(Priority.LOW);

get task from registry:

DownloadTasktask = SimpleDownloader.getTask(id);
List<DownloadTask> all = SimpleDownloader.getTasks();
List<DownloadTask> completed = SimpleDownloader.getTasks(TaskField.STATUS, Status.COMPLETED);
SimpleDownloader.getTask(TaskField.FILE_NAME, fileName) // returns latest matching single task, null if not found.inttotal = SimpleDownloader.getTotalCount();
intactive = SimpleDownloader.getActiveCount();
intqueued = SimpleDownloader.getQueuedCount();
intoccupied = SimpleDownloader.getOccupiedCount();
intconcurrency = SimpleDownloader.getEffectiveMaxConcurrent();

Update a task by ID:

SimpleDownloader.setPriority(id, Priority.NEXT);
SimpleDownloader.setWifiOnly(id, true);
SimpleDownloader.setLockedInQueue(id, true);
SimpleDownloader.setDeleteOnRemoval(id, true);

Restore tasks

Restore methods immediately restore saved tasks using the downloader's current configuration.

Important: Configure all downloader settings before calling any restore method. Restore methods should always be the last configuration call, otherwise restored tasks may not receive the configuration called after restore.

Automatic Restore:

Enable automatic restoration of previously saved download tasks:

SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setRetryPolicy(...)
.enableNotifications(true)
.enableForeground(true)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setNotification(...)
.setAutoRestore(true); // Keep this last.

When enabled, SimpleDownloader automatically restores previously saved tasks and resumes eligible ones. You do not need to call restoreTasks() separately when using auto restore. Auto restore is disabled by default.

Explicit restore when your app ready to show or continue downloads:

restored = downloader.restoreTasks();

Active tasks are restored as paused. Resume the tasks you want to continue:

SimpleDownloader.resumeAll();
//orfor (DownloadTasktask : restored) {
task.resume();
}

Restore matching tasks:

restoreTasks(...) returns an empty list when no match:

List<DownloadTask> paused = downloader.restoreTasks(TaskField.STATUS, Status.PAUSED);
List<DownloadTask> videos = downloader.restoreTasks(TaskField.MIME_TYPE, "video/mp4");
List<DownloadTask> matchingUrl = downloader.restoreTasks(TaskField.FILE_URL, fileUrl);

restoreTask() returns a single newest matching task, or null when no match:

DownloadTasktask = downloader.restoreTask(TaskField.FILE_URL, fileUrl);
if (task != null) task.resume();

Available fields:

TaskField.IDTaskField.FILE_URLTaskField.STATUSTaskField.PRIORITYTaskField.MIME_TYPETaskField.FILE_NAMETaskField.CREATED_ATTaskField.WIFI_ONLYTaskField.BUFFER_SIZETaskField.PROGRESSTaskField.BYTES_DOWNLOADEDTaskField.TOTAL_BYTESTaskField.OUTPUT_URITaskField.OUTPUT_PATHTaskField.OVERWRITE_URITaskField.OVERWRITE_PATHTaskField.OUTPUT_FOLDER_URITaskField.OUTPUT_FOLDER_PATHTaskField.SUB_FOLDER_PATHTaskField.DELETE_ON_REMOVALTaskField.LOCKED_IN_QUEUE

Finished tasks are kept in the database only when history is enabled:

SimpleDownloader.with(context)
.enableHistory(true);

Queue and concurrency

By default, SimpleDownloader use automatic concurrency. It starts with 1 and go up to 10 slot beased on download speed.

auto concurrency starts when setMaxConcurrent(0) or not set.

Stop queued tasks from starting automatically when a slot becomes free:

SimpleDownloader.with(context)
.setDownloadOnSlotFree(false);

Lock task in the queue:

task.setLockedInQueue(true);

Priorities :

Priority.NEXTPriority.HIGHPriority.NORMALPriority.LOW// NEXT > HIGH > NORMAL > LOW

Task statuses:

Status.STARTINGStatus.QUEUEDStatus.CONNECTINGStatus.DOWNLOADINGStatus.PAUSEDStatus.CANCELLEDStatus.WAITING_FOR_NETWORKStatus.RETRYINGStatus.COMPLETEDStatus.FAILED

Retry policy

The default policy with one automatic retry. Configure:

RetryPolicyretryPolicy = RetryPolicy.builder()
.maxRetryCount(3)
.initialDelayMs(1000)
.multiplier(2.0)
.maxDelayMs(30_000)
.build();
SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setRetryPolicy(retryPolicy);

Retry settings stay on the SimpleDownloader instance used to create or restore tasks.

MIME Type

A MIME type (media type) is a used to identify the format of a file, SimpleDownloader resolves it automatically, but can be set explicitly:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, "manual.pdf")
.setMimeType("application/pdf")
.setFileUrl(fileUrl)
.startDownload();

Or use automatic resolution:

.setMimeType(MimeType.AUTO) // or MimeType.FROM_NAME

Network, headers, cookies

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.setHeader("Authorization", "Bearer " + token)
.setHeader("Referer", pageUrl)
.setCookies("session=" + sessionId)
.setWifiOnly(true)
.startDownload();

Add headers:

Map<String, String> headers = newHashMap<>();
headers.put("Authorization", "Bearer " + token);
headers.put("Referer", pageUrl);
SimpleDownloader.with(context)
.setHeaders(headers);

Network info:

booleanavailable = SimpleDownloader.isNetworkAvailable();
intnetworkType = SimpleDownloader.getNetworkType();

Network constants:

NETWORK_TYPE_NONENETWORK_TYPE_UNKNOWNNETWORK_TYPE_WIFINETWORK_TYPE_CELLULARNETWORK_TYPE_ETHERNETNETWORK_TYPE_BLUETOOTHNETWORK_TYPE_VPNNETWORK_TYPE_USBNETWORK_TYPE_ROAMING

Waiting tasks resume when the network becomes available by default. Change with:

SimpleDownloader.with(context)
.enableResumeOnNetworkGain(false);

Notifications

Notifications are optional and disabled by default.

DownloadNotificationnotification = newDownloadNotification()
.setSmallIcon(R.drawable.ic_download)
.setCompleteIcon(R.drawable.ic_download_done)
.setErrorIcon(R.drawable.ic_download_error)
.setColorAccent(0xFF0087E5)
.setShowPauseAction(true)
.setShowCancelAction(true)
.setShowRetryAction(true);
DownloadTasktask = SimpleDownloader.with(context)
.enableNotifications(true)
.setNotification(notification)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

DownloadNotification configuration is optional, only use enableNotifications(true) if you don't want to customize. It will use the default config.

Run tasks using a foreground service:

SimpleDownloader.with(context)
.enableForeground(true)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

SimpleDownloader has built in thumbnail system for notifications, it can generate a thumbnail from a video, image, audio (album art), APK, PDF automatically.

You can also set a thumbnail:

notification.setThumbnail(bitmap);

Or load it from a URL:

notification.setThumbnailUrl(thumbnailUrl, thumbHeaders);
// pass null for headers, if it not available

DownloadNotification can configure the channel, importance, lock-screen visibility, sound, vibration, color, update interval, actions, etc.

Checksums

Verify the completed file with algorithms supported by MessageDigest. Like SHA-256, SHA-1, or MD5:

DownloadTasktask = SimpleDownloader.with(context)
.setChecksum("SHA-256", expectedChecksum)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

File names and MIME types modes

FileName.AUTOFileName.TIME_BASED
MimeType.AUTOMimeType.FROM_NAME

AUTO uses the URL, response headers, file extension, and content type to resolve the name and MIME type.

Error handling

Failures exceptions are instance of DownloadException:

@OverridepublicvoidonError(longid, UrioutputUri, Exceptionerror, DownloadTasktask) {
if (!(errorinstanceofDownloadException)) return;
DownloadExceptionfailure = (DownloadException) error;
DownloadException.Typetype = failure.getType();
inthttpCode = failure.getCode();
booleanretryable = failure.isRetryable();
Throwablecause = failure.getCause();
}

Error types:

NETWORK_LOSTTIMEOUTDNS_ERRORSSL_ERRORHTTP_ERRORENOSPCFILE_ERRORSTORAGE_PERMISSION_DENIEDOUTPUT_INVALIDRANGE_NOT_SUPPORTEDEMPTY_RESPONSECHECKSUM_FAILEDCANCELLEDUNKNOWN

Other configuration

SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setId(customId)
.setUserAgent(userAgent)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setBufferSize(16 * 1024)
.enableHistory(true)
.enableSorting(true);

Custom IDs are optional. SimpleDownloader generates an ID when setId() is not used. An active task cannot be replaced by another task with the same ID.

Passing 0 for a connection or read timeout keeps the OkHttp default.

Use your own HTTP client:

OkHttpClientclient = newOkHttpClient.Builder()
.followRedirects(true)
.build();
SimpleDownloader.with(context)
.setHttpClient(client);

The HTTP client cannot be replaced while a worker is running or already scheduled.

Use a custom task list order:

SimpleDownloader.with(context)
.setTaskComparator(myComparator);

Formatting helpers

Stringsize = Formator.formatBytes(bytes);
Stringspeed = Formator.formatSpeed(bytesPerSecond);
Stringeta = Formator.formatEta(etaMs);

TypeResolver is used internally, but it is also available for resolving file extensions and MIME types for you 🙂.

Cleanup

When listeners and observers are owned by an Activity or Fragment, release them using the same owner object given to with(...):

@OverrideprotectedvoidonDestroy() {
SimpleDownloader.releaseCallbacks(this);
SimpleDownloader.releaseObserver(observer);
super.onDestroy();
}

Call shutdown() only when you intentionally want to stop the library and release all workers, network callbacks, HTTP resources, thumbnails, database, etc:

SimpleDownloader.shutdown();

You do not need to call shutdown() normally or when an Activity is destroyed.

Support

Found a problem or have a suggestion? Open an issue: https://github.com/jeetarc/SimpleDownloader/issues

Copyright © 2026 Jeet / Jeetarc.

About

A modern Android download manager library designed to simplify complex file downloading behind a simple API

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

SimpleDownloader

JitPackGitHub releaseAndroid API

SimpleDownloader is an Android download library project. It handles the parts that usually make downloading difficult: queues, concurrent downloads, pause and resume, unstable networks, scoped storage, task persistence, foreground, notifications, progress updates, etc.

The simple API:

DownloadTasktask = SimpleDownloader.with(context)
.enableForeground(true)
.setOutput(folderPath, FileName.AUTO) // Or .setOutput(folderUri, FileName)
.setFileUrl(fileUrl)
.startDownload();

Features

  • Multiple downloads with queue and priority support
  • automatic download concurrency
  • Pause, resume, cancel, retry, remove, requeue, and force download
  • Resume using HTTP range requests
  • Network loss handling and Wi-Fi-only downloads
  • Task persistence and restoration with filters.
  • Complete built-in storage support for filesystem paths, SAF/document-tree output, and MediaStore output.
  • Built-in Subfolder support
  • Built-in file overwrite support
  • Automatic file name and MIME type resolution
  • Progress, speed, ETA, status, and lifecycle callbacks
  • Progress, completion, and error notifications with actions, thumbnails, and more.
  • Optional foreground execution
  • Custom OkHttpClient support
  • Custom task Comparator
  • RetryPolicy, timeouts, headers, cookies, checksums and many more
  • Minimum Android version: API 21

Installation

Add JitPack to your repositories:

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
}
}

Add SimpleDownloader to your app module:

dependencies {
implementation "com.github.jeetarc:SimpleDownloader:1.0.0-beta.3"
}

SimpleDownloader is built with Java 8 and compileSdk 35.

Setup

If you enable notifications, add:

<uses-permissionandroid:name="android.permission.POST_NOTIFICATIONS" />

On Android 13 and newer, request this permission at runtime.

If you enable enableForeground(true), also add:

<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE" />
<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

Foreground mode can automatically enable notifications, also you can use enableNotifications(true).

If using normal file path, add:

<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permissionandroid:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

And request the permission at runtime. Storage permissions are not needed when saving to app-specific folder or using folderUri via MediaStore or Storage Access Framework.

All other permissions are added by default.

Quick start

SimpleDownloader has three built-in storage modes.

1. Download into a file system folder:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderPath, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

2. Download into a MediaStore collection (Android 10+):

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(MediaStore.Downloads.EXTERNAL_CONTENT_URI, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

You can also use other MediaStore collections:

MediaStore.Images.Media.EXTERNAL_CONTENT_URIMediaStore.Video.Media.EXTERNAL_CONTENT_URIMediaStore.Audio.Media.EXTERNAL_CONTENT_URI

3. Download into a selected folder via SAF/document-tree:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(treeUri, FileName.AUTO)
.setFileUrl("https://example.com/files/document.pdf")
.startDownload();

Keep the URI permission:

intflags = Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
getContentResolver().takePersistableUriPermission(treeUri, flags);

Use a custom name if needed:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, "example.mp4")
.setFileUrl(fileUrl)
.startDownload();

SimpleDownloader creates a new file inside each folder. If a file with the same name already exists, it creates a unique name automatically.

Overwrite a file:

Use a DocumentFile URI or a specific MediaStore item URI:

DownloadTasktask = SimpleDownloader.with(context)
.overwrite(fileUri)
.setFileUrl(fileUrl)
.startDownload();

Or use a file path:

DownloadTasktask = SimpleDownloader.with(context)
.overwrite(outputFile.getAbsolutePath())
.setFileUrl(fileUrl)
.startDownload();

Subfolder:

A subfolder is an optional folder inside the main output folder where you want the downloaded file to be saved.

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, FileName.AUTO)
.setSubFolder("app")
.setFileUrl(fileUrl)
.startDownload();

Subfolders can be nested, for example "app/videos". They work with filesystem paths, SAF/document-tree output, and MediaStore output.

Listener for download updates

All DownloadListener callbacks run on the main thread. Every method is optional.

DownloadListenerlistener = newDownloadListener() {
@OverridepublicvoidonProgress(longid, intprogress, longspeed, longetaMs, DownloadTasktask) {
progressBar.setProgress(progress);
speedText.setText(Formator.formatSpeed(speed));
etaText.setText(Formator.formatEta(etaMs));
}
@OverridepublicvoidonComplete(longid, UrioutputUri, DownloadTasktask) {
// The download finished successfully.
}
@OverridepublicvoidonError(longid, UrioutputUri, Exceptionerror, DownloadTasktask) {
// The download failed.
}
};
DownloadTasktask = SimpleDownloader.with(this)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.addListener(listener)
.startDownload();

Other callbacks include:

onStart(longid, DownloadTasktask) {}
onQueued(longid, intposition, DownloadTasktask) {}
onPaused(longid, DownloadTasktask) {}
onResumed(longid, DownloadTasktask) {}
onCancelled(longid, DownloadTasktask) {}
onRemoved(longid, booleanoutputDeleted, DownloadTasktask) {}
onRetry(longid, intattempt, DownloadTasktask) {}
onWaitingForNetwork(longid, intnetworkType, DownloadTasktask) {}
onStatusChanged(longid, Statusstatus, DownloadTasktask) {}
onActiveChanged(longid, booleanisActive, DownloadTasktask) {}
onLifecycleChanged(longid, intlifecycle, DownloadTasktask) {}

onStart() can run again on resume and retry. Use onLifecycleChanged() to know the start or end of the full task lifecycle.

You can also add listeners directly on a task:

task.addListener(listener);
task.removeListener(listener);
task.releaseCallbacks();

Observe the task list

Use TaskListObserver when showing all downloads in a list, RecyclerView, etc.

TaskListObserverobserver = newTaskListObserver() {
@OverridepublicvoidonTasksChanged(List<DownloadTask> tasks) {
// The list order changed
}
@OverridepublicvoidonTaskUpdated(longid, DownloadTasktask) {
// Update only the matching item
}
};
SimpleDownloaderdownloader = SimpleDownloader.with(this)
.addObserver(observer);

The list passed to onTasksChanged() is an unmodifiable snapshot. DownloadTask objects inside it are live and can update.

Release the observer when not needed:

SimpleDownloader.releaseObserver(observer);

Control a task

task.pause();
task.resume();
task.cancel();
task.retry();
task.requeue();
task.remove();
task.forceDownload();

Check:

task.canPause();
task.canResume();
task.canRetry();

Change some task settings after creation:

task.setPriority(Priority.HIGH);
task.setWifiOnly(true);
task.setLockedInQueue(true);
task.setDeleteOnRemoval(true);

Note:

  • cancel() stops the task and deletes its output.
  • remove() removes the task from SimpleDownloader register.
  • remove() deletes the output only when setDeleteOnRemoval(true) is enabled.
  • forceDownload() starts a queued task even when it is locked. It doesn't care about concurrency limit

Task info

task.getId();
task.getFileUrl();
task.getFileName();
task.getMimeType();
task.getOutputUri();
task.getOutputFile();
task.getOutputDocumentFile();
task.getOutputFolderUri();
task.getOutputFolderPath();
task.getSubFolderPath();
task.getOutputPath();
task.getOverwriteUri();
task.getProgress();
task.getDownloadedBytes();
task.getTotalBytes();
task.getSpeed();
task.getEtaMs();
task.getStatus();
task.getPriority();
task.getError();
task.getCreatedAt();
task.getMaxRetryCount();
task.isActive();
task.isQueued();
task.isPaused();
task.isWaitingForNetwork();
task.isFinished();
task.isOccupiedSlot();

Reuse a configured instance

You can configure a SimpleDownloader instance once inside onCreate() and reuse it:

privateSimpleDownloaderdownloader;
@OverrideprotectedvoidonCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
downloader = SimpleDownloader.with(this)
.setMaxConcurrent(3)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setBufferSize(16 * 1024)
.enableHistory(true);
}
// You can configure all fields you want be the same across downloads.

Then use the configured instance whenever you start a download:

DownloadTasktask = downloader
.setOutput(folderUri, fileName)
.setMimeType(mimeType)
.setFileUrl(fileUrl)
.startDownload();

You can also use the same instance to restore tasks:

List<DownloadTask> tasks = downloader.restoreTasks();

Other common settings, such as retry policy, user agent, priority, Wi-Fi-only mode, notifications, foreground execution, etc can also be configured on the same downloader instance, instead of being set again for every download.

File URL, output destination, and custom ID should be set again before starting each task.

Global controls

Control a task by ID:

SimpleDownloader.pause(id);
SimpleDownloader.resume(id);
SimpleDownloader.cancel(id);
SimpleDownloader.retry(id);
SimpleDownloader.requeue(id);
SimpleDownloader.remove(id);
SimpleDownloader.forceDownload(id);

Control multiple tasks:

SimpleDownloader.pauseAll();
SimpleDownloader.resumeAll();
SimpleDownloader.cancelAll();
SimpleDownloader.retryAll();
SimpleDownloader.requeueAll();
SimpleDownloader.removeAll();
SimpleDownloader.pause(Priority.LOW);
SimpleDownloader.resumeAll(Priority.HIGH);
SimpleDownloader.remove(Status.COMPLETED);
SimpleDownloader.remove(Priority.LOW);

get task from registry:

DownloadTasktask = SimpleDownloader.getTask(id);
List<DownloadTask> all = SimpleDownloader.getTasks();
List<DownloadTask> completed = SimpleDownloader.getTasks(TaskField.STATUS, Status.COMPLETED);
SimpleDownloader.getTask(TaskField.FILE_NAME, fileName) // returns latest matching single task, null if not found.inttotal = SimpleDownloader.getTotalCount();
intactive = SimpleDownloader.getActiveCount();
intqueued = SimpleDownloader.getQueuedCount();
intoccupied = SimpleDownloader.getOccupiedCount();
intconcurrency = SimpleDownloader.getEffectiveMaxConcurrent();

Update a task by ID:

SimpleDownloader.setPriority(id, Priority.NEXT);
SimpleDownloader.setWifiOnly(id, true);
SimpleDownloader.setLockedInQueue(id, true);
SimpleDownloader.setDeleteOnRemoval(id, true);

Restore tasks

Restore methods immediately restore saved tasks using the downloader's current configuration.

Important: Configure all downloader settings before calling any restore method. Restore methods should always be the last configuration call, otherwise restored tasks may not receive the configuration called after restore.

Automatic Restore:

Enable automatic restoration of previously saved download tasks:

SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setRetryPolicy(...)
.enableNotifications(true)
.enableForeground(true)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setNotification(...)
.setAutoRestore(true); // Keep this last.

When enabled, SimpleDownloader automatically restores previously saved tasks and resumes eligible ones. You do not need to call restoreTasks() separately when using auto restore. Auto restore is disabled by default.

Explicit restore when your app ready to show or continue downloads:

restored = downloader.restoreTasks();

Active tasks are restored as paused. Resume the tasks you want to continue:

SimpleDownloader.resumeAll();
//orfor (DownloadTasktask : restored) {
task.resume();
}

Restore matching tasks:

restoreTasks(...) returns an empty list when no match:

List<DownloadTask> paused = downloader.restoreTasks(TaskField.STATUS, Status.PAUSED);
List<DownloadTask> videos = downloader.restoreTasks(TaskField.MIME_TYPE, "video/mp4");
List<DownloadTask> matchingUrl = downloader.restoreTasks(TaskField.FILE_URL, fileUrl);

restoreTask() returns a single newest matching task, or null when no match:

DownloadTasktask = downloader.restoreTask(TaskField.FILE_URL, fileUrl);
if (task != null) task.resume();

Available fields:

TaskField.IDTaskField.FILE_URLTaskField.STATUSTaskField.PRIORITYTaskField.MIME_TYPETaskField.FILE_NAMETaskField.CREATED_ATTaskField.WIFI_ONLYTaskField.BUFFER_SIZETaskField.PROGRESSTaskField.BYTES_DOWNLOADEDTaskField.TOTAL_BYTESTaskField.OUTPUT_URITaskField.OUTPUT_PATHTaskField.OVERWRITE_URITaskField.OVERWRITE_PATHTaskField.OUTPUT_FOLDER_URITaskField.OUTPUT_FOLDER_PATHTaskField.SUB_FOLDER_PATHTaskField.DELETE_ON_REMOVALTaskField.LOCKED_IN_QUEUE

Finished tasks are kept in the database only when history is enabled:

SimpleDownloader.with(context)
.enableHistory(true);

Queue and concurrency

By default, SimpleDownloader use automatic concurrency. It starts with 1 and go up to 10 slot beased on download speed.

auto concurrency starts when setMaxConcurrent(0) or not set.

Stop queued tasks from starting automatically when a slot becomes free:

SimpleDownloader.with(context)
.setDownloadOnSlotFree(false);

Lock task in the queue:

task.setLockedInQueue(true);

Priorities :

Priority.NEXTPriority.HIGHPriority.NORMALPriority.LOW// NEXT > HIGH > NORMAL > LOW

Task statuses:

Status.STARTINGStatus.QUEUEDStatus.CONNECTINGStatus.DOWNLOADINGStatus.PAUSEDStatus.CANCELLEDStatus.WAITING_FOR_NETWORKStatus.RETRYINGStatus.COMPLETEDStatus.FAILED

Retry policy

The default policy with one automatic retry. Configure:

RetryPolicyretryPolicy = RetryPolicy.builder()
.maxRetryCount(3)
.initialDelayMs(1000)
.multiplier(2.0)
.maxDelayMs(30_000)
.build();
SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setRetryPolicy(retryPolicy);

Retry settings stay on the SimpleDownloader instance used to create or restore tasks.

MIME Type

A MIME type (media type) is a used to identify the format of a file, SimpleDownloader resolves it automatically, but can be set explicitly:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, "manual.pdf")
.setMimeType("application/pdf")
.setFileUrl(fileUrl)
.startDownload();

Or use automatic resolution:

.setMimeType(MimeType.AUTO) // or MimeType.FROM_NAME

Network, headers, cookies

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.setHeader("Authorization", "Bearer " + token)
.setHeader("Referer", pageUrl)
.setCookies("session=" + sessionId)
.setWifiOnly(true)
.startDownload();

Add headers:

Map<String, String> headers = newHashMap<>();
headers.put("Authorization", "Bearer " + token);
headers.put("Referer", pageUrl);
SimpleDownloader.with(context)
.setHeaders(headers);

Network info:

booleanavailable = SimpleDownloader.isNetworkAvailable();
intnetworkType = SimpleDownloader.getNetworkType();

Network constants:

NETWORK_TYPE_NONENETWORK_TYPE_UNKNOWNNETWORK_TYPE_WIFINETWORK_TYPE_CELLULARNETWORK_TYPE_ETHERNETNETWORK_TYPE_BLUETOOTHNETWORK_TYPE_VPNNETWORK_TYPE_USBNETWORK_TYPE_ROAMING

Waiting tasks resume when the network becomes available by default. Change with:

SimpleDownloader.with(context)
.enableResumeOnNetworkGain(false);

Notifications

Notifications are optional and disabled by default.

DownloadNotificationnotification = newDownloadNotification()
.setSmallIcon(R.drawable.ic_download)
.setCompleteIcon(R.drawable.ic_download_done)
.setErrorIcon(R.drawable.ic_download_error)
.setColorAccent(0xFF0087E5)
.setShowPauseAction(true)
.setShowCancelAction(true)
.setShowRetryAction(true);
DownloadTasktask = SimpleDownloader.with(context)
.enableNotifications(true)
.setNotification(notification)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

DownloadNotification configuration is optional, only use enableNotifications(true) if you don't want to customize. It will use the default config.

Run tasks using a foreground service:

SimpleDownloader.with(context)
.enableForeground(true)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

SimpleDownloader has built in thumbnail system for notifications, it can generate a thumbnail from a video, image, audio (album art), APK, PDF automatically.

You can also set a thumbnail:

notification.setThumbnail(bitmap);

Or load it from a URL:

notification.setThumbnailUrl(thumbnailUrl, thumbHeaders);
// pass null for headers, if it not available

DownloadNotification can configure the channel, importance, lock-screen visibility, sound, vibration, color, update interval, actions, etc.

Checksums

Verify the completed file with algorithms supported by MessageDigest. Like SHA-256, SHA-1, or MD5:

DownloadTasktask = SimpleDownloader.with(context)
.setChecksum("SHA-256", expectedChecksum)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

File names and MIME types modes

FileName.AUTOFileName.TIME_BASED
MimeType.AUTOMimeType.FROM_NAME

AUTO uses the URL, response headers, file extension, and content type to resolve the name and MIME type.

Error handling

Failures exceptions are instance of DownloadException:

@OverridepublicvoidonError(longid, UrioutputUri, Exceptionerror, DownloadTasktask) {
if (!(errorinstanceofDownloadException)) return;
DownloadExceptionfailure = (DownloadException) error;
DownloadException.Typetype = failure.getType();
inthttpCode = failure.getCode();
booleanretryable = failure.isRetryable();
Throwablecause = failure.getCause();
}

Error types:

NETWORK_LOSTTIMEOUTDNS_ERRORSSL_ERRORHTTP_ERRORENOSPCFILE_ERRORSTORAGE_PERMISSION_DENIEDOUTPUT_INVALIDRANGE_NOT_SUPPORTEDEMPTY_RESPONSECHECKSUM_FAILEDCANCELLEDUNKNOWN

Other configuration

SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setId(customId)
.setUserAgent(userAgent)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setBufferSize(16 * 1024)
.enableHistory(true)
.enableSorting(true);

Custom IDs are optional. SimpleDownloader generates an ID when setId() is not used. An active task cannot be replaced by another task with the same ID.

Passing 0 for a connection or read timeout keeps the OkHttp default.

Use your own HTTP client:

OkHttpClientclient = newOkHttpClient.Builder()
.followRedirects(true)
.build();
SimpleDownloader.with(context)
.setHttpClient(client);

The HTTP client cannot be replaced while a worker is running or already scheduled.

Use a custom task list order:

SimpleDownloader.with(context)
.setTaskComparator(myComparator);

Formatting helpers

Stringsize = Formator.formatBytes(bytes);
Stringspeed = Formator.formatSpeed(bytesPerSecond);
Stringeta = Formator.formatEta(etaMs);

TypeResolver is used internally, but it is also available for resolving file extensions and MIME types for you 🙂.

Cleanup

When listeners and observers are owned by an Activity or Fragment, release them using the same owner object given to with(...):

@OverrideprotectedvoidonDestroy() {
SimpleDownloader.releaseCallbacks(this);
SimpleDownloader.releaseObserver(observer);
super.onDestroy();
}

Call shutdown() only when you intentionally want to stop the library and release all workers, network callbacks, HTTP resources, thumbnails, database, etc:

SimpleDownloader.shutdown();

You do not need to call shutdown() normally or when an Activity is destroyed.

Support

Found a problem or have a suggestion? Open an issue: https://github.com/jeetarc/SimpleDownloader/issues

Copyright © 2026 Jeet / Jeetarc.

About

A modern Android download manager library designed to simplify complex file downloading behind a simple API

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

SimpleDownloader

JitPackGitHub releaseAndroid API

SimpleDownloader is an Android download library project. It handles the parts that usually make downloading difficult: queues, concurrent downloads, pause and resume, unstable networks, scoped storage, task persistence, foreground, notifications, progress updates, etc.

The simple API:

DownloadTasktask = SimpleDownloader.with(context)
.enableForeground(true)
.setOutput(folderPath, FileName.AUTO) // Or .setOutput(folderUri, FileName)
.setFileUrl(fileUrl)
.startDownload();

Features

  • Multiple downloads with queue and priority support
  • automatic download concurrency
  • Pause, resume, cancel, retry, remove, requeue, and force download
  • Resume using HTTP range requests
  • Network loss handling and Wi-Fi-only downloads
  • Task persistence and restoration with filters.
  • Complete built-in storage support for filesystem paths, SAF/document-tree output, and MediaStore output.
  • Built-in Subfolder support
  • Built-in file overwrite support
  • Automatic file name and MIME type resolution
  • Progress, speed, ETA, status, and lifecycle callbacks
  • Progress, completion, and error notifications with actions, thumbnails, and more.
  • Optional foreground execution
  • Custom OkHttpClient support
  • Custom task Comparator
  • RetryPolicy, timeouts, headers, cookies, checksums and many more
  • Minimum Android version: API 21

Installation

Add JitPack to your repositories:

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
}
}

Add SimpleDownloader to your app module:

dependencies {
implementation "com.github.jeetarc:SimpleDownloader:1.0.0-beta.3"
}

SimpleDownloader is built with Java 8 and compileSdk 35.

Setup

If you enable notifications, add:

<uses-permissionandroid:name="android.permission.POST_NOTIFICATIONS" />

On Android 13 and newer, request this permission at runtime.

If you enable enableForeground(true), also add:

<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE" />
<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

Foreground mode can automatically enable notifications, also you can use enableNotifications(true).

If using normal file path, add:

<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permissionandroid:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

And request the permission at runtime. Storage permissions are not needed when saving to app-specific folder or using folderUri via MediaStore or Storage Access Framework.

All other permissions are added by default.

Quick start

SimpleDownloader has three built-in storage modes.

1. Download into a file system folder:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderPath, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

2. Download into a MediaStore collection (Android 10+):

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(MediaStore.Downloads.EXTERNAL_CONTENT_URI, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

You can also use other MediaStore collections:

MediaStore.Images.Media.EXTERNAL_CONTENT_URIMediaStore.Video.Media.EXTERNAL_CONTENT_URIMediaStore.Audio.Media.EXTERNAL_CONTENT_URI

3. Download into a selected folder via SAF/document-tree:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(treeUri, FileName.AUTO)
.setFileUrl("https://example.com/files/document.pdf")
.startDownload();

Keep the URI permission:

intflags = Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
getContentResolver().takePersistableUriPermission(treeUri, flags);

Use a custom name if needed:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, "example.mp4")
.setFileUrl(fileUrl)
.startDownload();

SimpleDownloader creates a new file inside each folder. If a file with the same name already exists, it creates a unique name automatically.

Overwrite a file:

Use a DocumentFile URI or a specific MediaStore item URI:

DownloadTasktask = SimpleDownloader.with(context)
.overwrite(fileUri)
.setFileUrl(fileUrl)
.startDownload();

Or use a file path:

DownloadTasktask = SimpleDownloader.with(context)
.overwrite(outputFile.getAbsolutePath())
.setFileUrl(fileUrl)
.startDownload();

Subfolder:

A subfolder is an optional folder inside the main output folder where you want the downloaded file to be saved.

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, FileName.AUTO)
.setSubFolder("app")
.setFileUrl(fileUrl)
.startDownload();

Subfolders can be nested, for example "app/videos". They work with filesystem paths, SAF/document-tree output, and MediaStore output.

Listener for download updates

All DownloadListener callbacks run on the main thread. Every method is optional.

DownloadListenerlistener = newDownloadListener() {
@OverridepublicvoidonProgress(longid, intprogress, longspeed, longetaMs, DownloadTasktask) {
progressBar.setProgress(progress);
speedText.setText(Formator.formatSpeed(speed));
etaText.setText(Formator.formatEta(etaMs));
}
@OverridepublicvoidonComplete(longid, UrioutputUri, DownloadTasktask) {
// The download finished successfully.
}
@OverridepublicvoidonError(longid, UrioutputUri, Exceptionerror, DownloadTasktask) {
// The download failed.
}
};
DownloadTasktask = SimpleDownloader.with(this)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.addListener(listener)
.startDownload();

Other callbacks include:

onStart(longid, DownloadTasktask) {}
onQueued(longid, intposition, DownloadTasktask) {}
onPaused(longid, DownloadTasktask) {}
onResumed(longid, DownloadTasktask) {}
onCancelled(longid, DownloadTasktask) {}
onRemoved(longid, booleanoutputDeleted, DownloadTasktask) {}
onRetry(longid, intattempt, DownloadTasktask) {}
onWaitingForNetwork(longid, intnetworkType, DownloadTasktask) {}
onStatusChanged(longid, Statusstatus, DownloadTasktask) {}
onActiveChanged(longid, booleanisActive, DownloadTasktask) {}
onLifecycleChanged(longid, intlifecycle, DownloadTasktask) {}

onStart() can run again on resume and retry. Use onLifecycleChanged() to know the start or end of the full task lifecycle.

You can also add listeners directly on a task:

task.addListener(listener);
task.removeListener(listener);
task.releaseCallbacks();

Observe the task list

Use TaskListObserver when showing all downloads in a list, RecyclerView, etc.

TaskListObserverobserver = newTaskListObserver() {
@OverridepublicvoidonTasksChanged(List<DownloadTask> tasks) {
// The list order changed
}
@OverridepublicvoidonTaskUpdated(longid, DownloadTasktask) {
// Update only the matching item
}
};
SimpleDownloaderdownloader = SimpleDownloader.with(this)
.addObserver(observer);

The list passed to onTasksChanged() is an unmodifiable snapshot. DownloadTask objects inside it are live and can update.

Release the observer when not needed:

SimpleDownloader.releaseObserver(observer);

Control a task

task.pause();
task.resume();
task.cancel();
task.retry();
task.requeue();
task.remove();
task.forceDownload();

Check:

task.canPause();
task.canResume();
task.canRetry();

Change some task settings after creation:

task.setPriority(Priority.HIGH);
task.setWifiOnly(true);
task.setLockedInQueue(true);
task.setDeleteOnRemoval(true);

Note:

  • cancel() stops the task and deletes its output.
  • remove() removes the task from SimpleDownloader register.
  • remove() deletes the output only when setDeleteOnRemoval(true) is enabled.
  • forceDownload() starts a queued task even when it is locked. It doesn't care about concurrency limit

Task info

task.getId();
task.getFileUrl();
task.getFileName();
task.getMimeType();
task.getOutputUri();
task.getOutputFile();
task.getOutputDocumentFile();
task.getOutputFolderUri();
task.getOutputFolderPath();
task.getSubFolderPath();
task.getOutputPath();
task.getOverwriteUri();
task.getProgress();
task.getDownloadedBytes();
task.getTotalBytes();
task.getSpeed();
task.getEtaMs();
task.getStatus();
task.getPriority();
task.getError();
task.getCreatedAt();
task.getMaxRetryCount();
task.isActive();
task.isQueued();
task.isPaused();
task.isWaitingForNetwork();
task.isFinished();
task.isOccupiedSlot();

Reuse a configured instance

You can configure a SimpleDownloader instance once inside onCreate() and reuse it:

privateSimpleDownloaderdownloader;
@OverrideprotectedvoidonCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
downloader = SimpleDownloader.with(this)
.setMaxConcurrent(3)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setBufferSize(16 * 1024)
.enableHistory(true);
}
// You can configure all fields you want be the same across downloads.

Then use the configured instance whenever you start a download:

DownloadTasktask = downloader
.setOutput(folderUri, fileName)
.setMimeType(mimeType)
.setFileUrl(fileUrl)
.startDownload();

You can also use the same instance to restore tasks:

List<DownloadTask> tasks = downloader.restoreTasks();

Other common settings, such as retry policy, user agent, priority, Wi-Fi-only mode, notifications, foreground execution, etc can also be configured on the same downloader instance, instead of being set again for every download.

File URL, output destination, and custom ID should be set again before starting each task.

Global controls

Control a task by ID:

SimpleDownloader.pause(id);
SimpleDownloader.resume(id);
SimpleDownloader.cancel(id);
SimpleDownloader.retry(id);
SimpleDownloader.requeue(id);
SimpleDownloader.remove(id);
SimpleDownloader.forceDownload(id);

Control multiple tasks:

SimpleDownloader.pauseAll();
SimpleDownloader.resumeAll();
SimpleDownloader.cancelAll();
SimpleDownloader.retryAll();
SimpleDownloader.requeueAll();
SimpleDownloader.removeAll();
SimpleDownloader.pause(Priority.LOW);
SimpleDownloader.resumeAll(Priority.HIGH);
SimpleDownloader.remove(Status.COMPLETED);
SimpleDownloader.remove(Priority.LOW);

get task from registry:

DownloadTasktask = SimpleDownloader.getTask(id);
List<DownloadTask> all = SimpleDownloader.getTasks();
List<DownloadTask> completed = SimpleDownloader.getTasks(TaskField.STATUS, Status.COMPLETED);
SimpleDownloader.getTask(TaskField.FILE_NAME, fileName) // returns latest matching single task, null if not found.inttotal = SimpleDownloader.getTotalCount();
intactive = SimpleDownloader.getActiveCount();
intqueued = SimpleDownloader.getQueuedCount();
intoccupied = SimpleDownloader.getOccupiedCount();
intconcurrency = SimpleDownloader.getEffectiveMaxConcurrent();

Update a task by ID:

SimpleDownloader.setPriority(id, Priority.NEXT);
SimpleDownloader.setWifiOnly(id, true);
SimpleDownloader.setLockedInQueue(id, true);
SimpleDownloader.setDeleteOnRemoval(id, true);

Restore tasks

Restore methods immediately restore saved tasks using the downloader's current configuration.

Important: Configure all downloader settings before calling any restore method. Restore methods should always be the last configuration call, otherwise restored tasks may not receive the configuration called after restore.

Automatic Restore:

Enable automatic restoration of previously saved download tasks:

SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setRetryPolicy(...)
.enableNotifications(true)
.enableForeground(true)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setNotification(...)
.setAutoRestore(true); // Keep this last.

When enabled, SimpleDownloader automatically restores previously saved tasks and resumes eligible ones. You do not need to call restoreTasks() separately when using auto restore. Auto restore is disabled by default.

Explicit restore when your app ready to show or continue downloads:

restored = downloader.restoreTasks();

Active tasks are restored as paused. Resume the tasks you want to continue:

SimpleDownloader.resumeAll();
//orfor (DownloadTasktask : restored) {
task.resume();
}

Restore matching tasks:

restoreTasks(...) returns an empty list when no match:

List<DownloadTask> paused = downloader.restoreTasks(TaskField.STATUS, Status.PAUSED);
List<DownloadTask> videos = downloader.restoreTasks(TaskField.MIME_TYPE, "video/mp4");
List<DownloadTask> matchingUrl = downloader.restoreTasks(TaskField.FILE_URL, fileUrl);

restoreTask() returns a single newest matching task, or null when no match:

DownloadTasktask = downloader.restoreTask(TaskField.FILE_URL, fileUrl);
if (task != null) task.resume();

Available fields:

TaskField.IDTaskField.FILE_URLTaskField.STATUSTaskField.PRIORITYTaskField.MIME_TYPETaskField.FILE_NAMETaskField.CREATED_ATTaskField.WIFI_ONLYTaskField.BUFFER_SIZETaskField.PROGRESSTaskField.BYTES_DOWNLOADEDTaskField.TOTAL_BYTESTaskField.OUTPUT_URITaskField.OUTPUT_PATHTaskField.OVERWRITE_URITaskField.OVERWRITE_PATHTaskField.OUTPUT_FOLDER_URITaskField.OUTPUT_FOLDER_PATHTaskField.SUB_FOLDER_PATHTaskField.DELETE_ON_REMOVALTaskField.LOCKED_IN_QUEUE

Finished tasks are kept in the database only when history is enabled:

SimpleDownloader.with(context)
.enableHistory(true);

Queue and concurrency

By default, SimpleDownloader use automatic concurrency. It starts with 1 and go up to 10 slot beased on download speed.

auto concurrency starts when setMaxConcurrent(0) or not set.

Stop queued tasks from starting automatically when a slot becomes free:

SimpleDownloader.with(context)
.setDownloadOnSlotFree(false);

Lock task in the queue:

task.setLockedInQueue(true);

Priorities :

Priority.NEXTPriority.HIGHPriority.NORMALPriority.LOW// NEXT > HIGH > NORMAL > LOW

Task statuses:

Status.STARTINGStatus.QUEUEDStatus.CONNECTINGStatus.DOWNLOADINGStatus.PAUSEDStatus.CANCELLEDStatus.WAITING_FOR_NETWORKStatus.RETRYINGStatus.COMPLETEDStatus.FAILED

Retry policy

The default policy with one automatic retry. Configure:

RetryPolicyretryPolicy = RetryPolicy.builder()
.maxRetryCount(3)
.initialDelayMs(1000)
.multiplier(2.0)
.maxDelayMs(30_000)
.build();
SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setRetryPolicy(retryPolicy);

Retry settings stay on the SimpleDownloader instance used to create or restore tasks.

MIME Type

A MIME type (media type) is a used to identify the format of a file, SimpleDownloader resolves it automatically, but can be set explicitly:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, "manual.pdf")
.setMimeType("application/pdf")
.setFileUrl(fileUrl)
.startDownload();

Or use automatic resolution:

.setMimeType(MimeType.AUTO) // or MimeType.FROM_NAME

Network, headers, cookies

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.setHeader("Authorization", "Bearer " + token)
.setHeader("Referer", pageUrl)
.setCookies("session=" + sessionId)
.setWifiOnly(true)
.startDownload();

Add headers:

Map<String, String> headers = newHashMap<>();
headers.put("Authorization", "Bearer " + token);
headers.put("Referer", pageUrl);
SimpleDownloader.with(context)
.setHeaders(headers);

Network info:

booleanavailable = SimpleDownloader.isNetworkAvailable();
intnetworkType = SimpleDownloader.getNetworkType();

Network constants:

NETWORK_TYPE_NONENETWORK_TYPE_UNKNOWNNETWORK_TYPE_WIFINETWORK_TYPE_CELLULARNETWORK_TYPE_ETHERNETNETWORK_TYPE_BLUETOOTHNETWORK_TYPE_VPNNETWORK_TYPE_USBNETWORK_TYPE_ROAMING

Waiting tasks resume when the network becomes available by default. Change with:

SimpleDownloader.with(context)
.enableResumeOnNetworkGain(false);

Notifications

Notifications are optional and disabled by default.

DownloadNotificationnotification = newDownloadNotification()
.setSmallIcon(R.drawable.ic_download)
.setCompleteIcon(R.drawable.ic_download_done)
.setErrorIcon(R.drawable.ic_download_error)
.setColorAccent(0xFF0087E5)
.setShowPauseAction(true)
.setShowCancelAction(true)
.setShowRetryAction(true);
DownloadTasktask = SimpleDownloader.with(context)
.enableNotifications(true)
.setNotification(notification)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

DownloadNotification configuration is optional, only use enableNotifications(true) if you don't want to customize. It will use the default config.

Run tasks using a foreground service:

SimpleDownloader.with(context)
.enableForeground(true)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

SimpleDownloader has built in thumbnail system for notifications, it can generate a thumbnail from a video, image, audio (album art), APK, PDF automatically.

You can also set a thumbnail:

notification.setThumbnail(bitmap);

Or load it from a URL:

notification.setThumbnailUrl(thumbnailUrl, thumbHeaders);
// pass null for headers, if it not available

DownloadNotification can configure the channel, importance, lock-screen visibility, sound, vibration, color, update interval, actions, etc.

Checksums

Verify the completed file with algorithms supported by MessageDigest. Like SHA-256, SHA-1, or MD5:

DownloadTasktask = SimpleDownloader.with(context)
.setChecksum("SHA-256", expectedChecksum)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

File names and MIME types modes

FileName.AUTOFileName.TIME_BASED
MimeType.AUTOMimeType.FROM_NAME

AUTO uses the URL, response headers, file extension, and content type to resolve the name and MIME type.

Error handling

Failures exceptions are instance of DownloadException:

@OverridepublicvoidonError(longid, UrioutputUri, Exceptionerror, DownloadTasktask) {
if (!(errorinstanceofDownloadException)) return;
DownloadExceptionfailure = (DownloadException) error;
DownloadException.Typetype = failure.getType();
inthttpCode = failure.getCode();
booleanretryable = failure.isRetryable();
Throwablecause = failure.getCause();
}

Error types:

NETWORK_LOSTTIMEOUTDNS_ERRORSSL_ERRORHTTP_ERRORENOSPCFILE_ERRORSTORAGE_PERMISSION_DENIEDOUTPUT_INVALIDRANGE_NOT_SUPPORTEDEMPTY_RESPONSECHECKSUM_FAILEDCANCELLEDUNKNOWN

Other configuration

SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setId(customId)
.setUserAgent(userAgent)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setBufferSize(16 * 1024)
.enableHistory(true)
.enableSorting(true);

Custom IDs are optional. SimpleDownloader generates an ID when setId() is not used. An active task cannot be replaced by another task with the same ID.

Passing 0 for a connection or read timeout keeps the OkHttp default.

Use your own HTTP client:

OkHttpClientclient = newOkHttpClient.Builder()
.followRedirects(true)
.build();
SimpleDownloader.with(context)
.setHttpClient(client);

The HTTP client cannot be replaced while a worker is running or already scheduled.

Use a custom task list order:

SimpleDownloader.with(context)
.setTaskComparator(myComparator);

Formatting helpers

Stringsize = Formator.formatBytes(bytes);
Stringspeed = Formator.formatSpeed(bytesPerSecond);
Stringeta = Formator.formatEta(etaMs);

TypeResolver is used internally, but it is also available for resolving file extensions and MIME types for you 🙂.

Cleanup

When listeners and observers are owned by an Activity or Fragment, release them using the same owner object given to with(...):

@OverrideprotectedvoidonDestroy() {
SimpleDownloader.releaseCallbacks(this);
SimpleDownloader.releaseObserver(observer);
super.onDestroy();
}

Call shutdown() only when you intentionally want to stop the library and release all workers, network callbacks, HTTP resources, thumbnails, database, etc:

SimpleDownloader.shutdown();

You do not need to call shutdown() normally or when an Activity is destroyed.

Support

Found a problem or have a suggestion? Open an issue: https://github.com/jeetarc/SimpleDownloader/issues

Copyright © 2026 Jeet / Jeetarc.

About

A modern Android download manager library designed to simplify complex file downloading behind a simple API

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

SimpleDownloader

JitPackGitHub releaseAndroid API

SimpleDownloader is an Android download library project. It handles the parts that usually make downloading difficult: queues, concurrent downloads, pause and resume, unstable networks, scoped storage, task persistence, foreground, notifications, progress updates, etc.

The simple API:

DownloadTasktask = SimpleDownloader.with(context)
.enableForeground(true)
.setOutput(folderPath, FileName.AUTO) // Or .setOutput(folderUri, FileName)
.setFileUrl(fileUrl)
.startDownload();

Features

  • Multiple downloads with queue and priority support
  • automatic download concurrency
  • Pause, resume, cancel, retry, remove, requeue, and force download
  • Resume using HTTP range requests
  • Network loss handling and Wi-Fi-only downloads
  • Task persistence and restoration with filters.
  • Complete built-in storage support for filesystem paths, SAF/document-tree output, and MediaStore output.
  • Built-in Subfolder support
  • Built-in file overwrite support
  • Automatic file name and MIME type resolution
  • Progress, speed, ETA, status, and lifecycle callbacks
  • Progress, completion, and error notifications with actions, thumbnails, and more.
  • Optional foreground execution
  • Custom OkHttpClient support
  • Custom task Comparator
  • RetryPolicy, timeouts, headers, cookies, checksums and many more
  • Minimum Android version: API 21

Installation

Add JitPack to your repositories:

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url "https://jitpack.io" }
}
}

Add SimpleDownloader to your app module:

dependencies {
implementation "com.github.jeetarc:SimpleDownloader:1.0.0-beta.3"
}

SimpleDownloader is built with Java 8 and compileSdk 35.

Setup

If you enable notifications, add:

<uses-permissionandroid:name="android.permission.POST_NOTIFICATIONS" />

On Android 13 and newer, request this permission at runtime.

If you enable enableForeground(true), also add:

<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE" />
<uses-permissionandroid:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

Foreground mode can automatically enable notifications, also you can use enableNotifications(true).

If using normal file path, add:

<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permissionandroid:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

And request the permission at runtime. Storage permissions are not needed when saving to app-specific folder or using folderUri via MediaStore or Storage Access Framework.

All other permissions are added by default.

Quick start

SimpleDownloader has three built-in storage modes.

1. Download into a file system folder:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderPath, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

2. Download into a MediaStore collection (Android 10+):

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(MediaStore.Downloads.EXTERNAL_CONTENT_URI, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

You can also use other MediaStore collections:

MediaStore.Images.Media.EXTERNAL_CONTENT_URIMediaStore.Video.Media.EXTERNAL_CONTENT_URIMediaStore.Audio.Media.EXTERNAL_CONTENT_URI

3. Download into a selected folder via SAF/document-tree:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(treeUri, FileName.AUTO)
.setFileUrl("https://example.com/files/document.pdf")
.startDownload();

Keep the URI permission:

intflags = Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
getContentResolver().takePersistableUriPermission(treeUri, flags);

Use a custom name if needed:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, "example.mp4")
.setFileUrl(fileUrl)
.startDownload();

SimpleDownloader creates a new file inside each folder. If a file with the same name already exists, it creates a unique name automatically.

Overwrite a file:

Use a DocumentFile URI or a specific MediaStore item URI:

DownloadTasktask = SimpleDownloader.with(context)
.overwrite(fileUri)
.setFileUrl(fileUrl)
.startDownload();

Or use a file path:

DownloadTasktask = SimpleDownloader.with(context)
.overwrite(outputFile.getAbsolutePath())
.setFileUrl(fileUrl)
.startDownload();

Subfolder:

A subfolder is an optional folder inside the main output folder where you want the downloaded file to be saved.

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, FileName.AUTO)
.setSubFolder("app")
.setFileUrl(fileUrl)
.startDownload();

Subfolders can be nested, for example "app/videos". They work with filesystem paths, SAF/document-tree output, and MediaStore output.

Listener for download updates

All DownloadListener callbacks run on the main thread. Every method is optional.

DownloadListenerlistener = newDownloadListener() {
@OverridepublicvoidonProgress(longid, intprogress, longspeed, longetaMs, DownloadTasktask) {
progressBar.setProgress(progress);
speedText.setText(Formator.formatSpeed(speed));
etaText.setText(Formator.formatEta(etaMs));
}
@OverridepublicvoidonComplete(longid, UrioutputUri, DownloadTasktask) {
// The download finished successfully.
}
@OverridepublicvoidonError(longid, UrioutputUri, Exceptionerror, DownloadTasktask) {
// The download failed.
}
};
DownloadTasktask = SimpleDownloader.with(this)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.addListener(listener)
.startDownload();

Other callbacks include:

onStart(longid, DownloadTasktask) {}
onQueued(longid, intposition, DownloadTasktask) {}
onPaused(longid, DownloadTasktask) {}
onResumed(longid, DownloadTasktask) {}
onCancelled(longid, DownloadTasktask) {}
onRemoved(longid, booleanoutputDeleted, DownloadTasktask) {}
onRetry(longid, intattempt, DownloadTasktask) {}
onWaitingForNetwork(longid, intnetworkType, DownloadTasktask) {}
onStatusChanged(longid, Statusstatus, DownloadTasktask) {}
onActiveChanged(longid, booleanisActive, DownloadTasktask) {}
onLifecycleChanged(longid, intlifecycle, DownloadTasktask) {}

onStart() can run again on resume and retry. Use onLifecycleChanged() to know the start or end of the full task lifecycle.

You can also add listeners directly on a task:

task.addListener(listener);
task.removeListener(listener);
task.releaseCallbacks();

Observe the task list

Use TaskListObserver when showing all downloads in a list, RecyclerView, etc.

TaskListObserverobserver = newTaskListObserver() {
@OverridepublicvoidonTasksChanged(List<DownloadTask> tasks) {
// The list order changed
}
@OverridepublicvoidonTaskUpdated(longid, DownloadTasktask) {
// Update only the matching item
}
};
SimpleDownloaderdownloader = SimpleDownloader.with(this)
.addObserver(observer);

The list passed to onTasksChanged() is an unmodifiable snapshot. DownloadTask objects inside it are live and can update.

Release the observer when not needed:

SimpleDownloader.releaseObserver(observer);

Control a task

task.pause();
task.resume();
task.cancel();
task.retry();
task.requeue();
task.remove();
task.forceDownload();

Check:

task.canPause();
task.canResume();
task.canRetry();

Change some task settings after creation:

task.setPriority(Priority.HIGH);
task.setWifiOnly(true);
task.setLockedInQueue(true);
task.setDeleteOnRemoval(true);

Note:

  • cancel() stops the task and deletes its output.
  • remove() removes the task from SimpleDownloader register.
  • remove() deletes the output only when setDeleteOnRemoval(true) is enabled.
  • forceDownload() starts a queued task even when it is locked. It doesn't care about concurrency limit

Task info

task.getId();
task.getFileUrl();
task.getFileName();
task.getMimeType();
task.getOutputUri();
task.getOutputFile();
task.getOutputDocumentFile();
task.getOutputFolderUri();
task.getOutputFolderPath();
task.getSubFolderPath();
task.getOutputPath();
task.getOverwriteUri();
task.getProgress();
task.getDownloadedBytes();
task.getTotalBytes();
task.getSpeed();
task.getEtaMs();
task.getStatus();
task.getPriority();
task.getError();
task.getCreatedAt();
task.getMaxRetryCount();
task.isActive();
task.isQueued();
task.isPaused();
task.isWaitingForNetwork();
task.isFinished();
task.isOccupiedSlot();

Reuse a configured instance

You can configure a SimpleDownloader instance once inside onCreate() and reuse it:

privateSimpleDownloaderdownloader;
@OverrideprotectedvoidonCreate(BundlesavedInstanceState) {
super.onCreate(savedInstanceState);
downloader = SimpleDownloader.with(this)
.setMaxConcurrent(3)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setBufferSize(16 * 1024)
.enableHistory(true);
}
// You can configure all fields you want be the same across downloads.

Then use the configured instance whenever you start a download:

DownloadTasktask = downloader
.setOutput(folderUri, fileName)
.setMimeType(mimeType)
.setFileUrl(fileUrl)
.startDownload();

You can also use the same instance to restore tasks:

List<DownloadTask> tasks = downloader.restoreTasks();

Other common settings, such as retry policy, user agent, priority, Wi-Fi-only mode, notifications, foreground execution, etc can also be configured on the same downloader instance, instead of being set again for every download.

File URL, output destination, and custom ID should be set again before starting each task.

Global controls

Control a task by ID:

SimpleDownloader.pause(id);
SimpleDownloader.resume(id);
SimpleDownloader.cancel(id);
SimpleDownloader.retry(id);
SimpleDownloader.requeue(id);
SimpleDownloader.remove(id);
SimpleDownloader.forceDownload(id);

Control multiple tasks:

SimpleDownloader.pauseAll();
SimpleDownloader.resumeAll();
SimpleDownloader.cancelAll();
SimpleDownloader.retryAll();
SimpleDownloader.requeueAll();
SimpleDownloader.removeAll();
SimpleDownloader.pause(Priority.LOW);
SimpleDownloader.resumeAll(Priority.HIGH);
SimpleDownloader.remove(Status.COMPLETED);
SimpleDownloader.remove(Priority.LOW);

get task from registry:

DownloadTasktask = SimpleDownloader.getTask(id);
List<DownloadTask> all = SimpleDownloader.getTasks();
List<DownloadTask> completed = SimpleDownloader.getTasks(TaskField.STATUS, Status.COMPLETED);
SimpleDownloader.getTask(TaskField.FILE_NAME, fileName) // returns latest matching single task, null if not found.inttotal = SimpleDownloader.getTotalCount();
intactive = SimpleDownloader.getActiveCount();
intqueued = SimpleDownloader.getQueuedCount();
intoccupied = SimpleDownloader.getOccupiedCount();
intconcurrency = SimpleDownloader.getEffectiveMaxConcurrent();

Update a task by ID:

SimpleDownloader.setPriority(id, Priority.NEXT);
SimpleDownloader.setWifiOnly(id, true);
SimpleDownloader.setLockedInQueue(id, true);
SimpleDownloader.setDeleteOnRemoval(id, true);

Restore tasks

Restore methods immediately restore saved tasks using the downloader's current configuration.

Important: Configure all downloader settings before calling any restore method. Restore methods should always be the last configuration call, otherwise restored tasks may not receive the configuration called after restore.

Automatic Restore:

Enable automatic restoration of previously saved download tasks:

SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setRetryPolicy(...)
.enableNotifications(true)
.enableForeground(true)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setNotification(...)
.setAutoRestore(true); // Keep this last.

When enabled, SimpleDownloader automatically restores previously saved tasks and resumes eligible ones. You do not need to call restoreTasks() separately when using auto restore. Auto restore is disabled by default.

Explicit restore when your app ready to show or continue downloads:

restored = downloader.restoreTasks();

Active tasks are restored as paused. Resume the tasks you want to continue:

SimpleDownloader.resumeAll();
//orfor (DownloadTasktask : restored) {
task.resume();
}

Restore matching tasks:

restoreTasks(...) returns an empty list when no match:

List<DownloadTask> paused = downloader.restoreTasks(TaskField.STATUS, Status.PAUSED);
List<DownloadTask> videos = downloader.restoreTasks(TaskField.MIME_TYPE, "video/mp4");
List<DownloadTask> matchingUrl = downloader.restoreTasks(TaskField.FILE_URL, fileUrl);

restoreTask() returns a single newest matching task, or null when no match:

DownloadTasktask = downloader.restoreTask(TaskField.FILE_URL, fileUrl);
if (task != null) task.resume();

Available fields:

TaskField.IDTaskField.FILE_URLTaskField.STATUSTaskField.PRIORITYTaskField.MIME_TYPETaskField.FILE_NAMETaskField.CREATED_ATTaskField.WIFI_ONLYTaskField.BUFFER_SIZETaskField.PROGRESSTaskField.BYTES_DOWNLOADEDTaskField.TOTAL_BYTESTaskField.OUTPUT_URITaskField.OUTPUT_PATHTaskField.OVERWRITE_URITaskField.OVERWRITE_PATHTaskField.OUTPUT_FOLDER_URITaskField.OUTPUT_FOLDER_PATHTaskField.SUB_FOLDER_PATHTaskField.DELETE_ON_REMOVALTaskField.LOCKED_IN_QUEUE

Finished tasks are kept in the database only when history is enabled:

SimpleDownloader.with(context)
.enableHistory(true);

Queue and concurrency

By default, SimpleDownloader use automatic concurrency. It starts with 1 and go up to 10 slot beased on download speed.

auto concurrency starts when setMaxConcurrent(0) or not set.

Stop queued tasks from starting automatically when a slot becomes free:

SimpleDownloader.with(context)
.setDownloadOnSlotFree(false);

Lock task in the queue:

task.setLockedInQueue(true);

Priorities :

Priority.NEXTPriority.HIGHPriority.NORMALPriority.LOW// NEXT > HIGH > NORMAL > LOW

Task statuses:

Status.STARTINGStatus.QUEUEDStatus.CONNECTINGStatus.DOWNLOADINGStatus.PAUSEDStatus.CANCELLEDStatus.WAITING_FOR_NETWORKStatus.RETRYINGStatus.COMPLETEDStatus.FAILED

Retry policy

The default policy with one automatic retry. Configure:

RetryPolicyretryPolicy = RetryPolicy.builder()
.maxRetryCount(3)
.initialDelayMs(1000)
.multiplier(2.0)
.maxDelayMs(30_000)
.build();
SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setRetryPolicy(retryPolicy);

Retry settings stay on the SimpleDownloader instance used to create or restore tasks.

MIME Type

A MIME type (media type) is a used to identify the format of a file, SimpleDownloader resolves it automatically, but can be set explicitly:

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, "manual.pdf")
.setMimeType("application/pdf")
.setFileUrl(fileUrl)
.startDownload();

Or use automatic resolution:

.setMimeType(MimeType.AUTO) // or MimeType.FROM_NAME

Network, headers, cookies

DownloadTasktask = SimpleDownloader.with(context)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.setHeader("Authorization", "Bearer " + token)
.setHeader("Referer", pageUrl)
.setCookies("session=" + sessionId)
.setWifiOnly(true)
.startDownload();

Add headers:

Map<String, String> headers = newHashMap<>();
headers.put("Authorization", "Bearer " + token);
headers.put("Referer", pageUrl);
SimpleDownloader.with(context)
.setHeaders(headers);

Network info:

booleanavailable = SimpleDownloader.isNetworkAvailable();
intnetworkType = SimpleDownloader.getNetworkType();

Network constants:

NETWORK_TYPE_NONENETWORK_TYPE_UNKNOWNNETWORK_TYPE_WIFINETWORK_TYPE_CELLULARNETWORK_TYPE_ETHERNETNETWORK_TYPE_BLUETOOTHNETWORK_TYPE_VPNNETWORK_TYPE_USBNETWORK_TYPE_ROAMING

Waiting tasks resume when the network becomes available by default. Change with:

SimpleDownloader.with(context)
.enableResumeOnNetworkGain(false);

Notifications

Notifications are optional and disabled by default.

DownloadNotificationnotification = newDownloadNotification()
.setSmallIcon(R.drawable.ic_download)
.setCompleteIcon(R.drawable.ic_download_done)
.setErrorIcon(R.drawable.ic_download_error)
.setColorAccent(0xFF0087E5)
.setShowPauseAction(true)
.setShowCancelAction(true)
.setShowRetryAction(true);
DownloadTasktask = SimpleDownloader.with(context)
.enableNotifications(true)
.setNotification(notification)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

DownloadNotification configuration is optional, only use enableNotifications(true) if you don't want to customize. It will use the default config.

Run tasks using a foreground service:

SimpleDownloader.with(context)
.enableForeground(true)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

SimpleDownloader has built in thumbnail system for notifications, it can generate a thumbnail from a video, image, audio (album art), APK, PDF automatically.

You can also set a thumbnail:

notification.setThumbnail(bitmap);

Or load it from a URL:

notification.setThumbnailUrl(thumbnailUrl, thumbHeaders);
// pass null for headers, if it not available

DownloadNotification can configure the channel, importance, lock-screen visibility, sound, vibration, color, update interval, actions, etc.

Checksums

Verify the completed file with algorithms supported by MessageDigest. Like SHA-256, SHA-1, or MD5:

DownloadTasktask = SimpleDownloader.with(context)
.setChecksum("SHA-256", expectedChecksum)
.setOutput(folderUri, FileName.AUTO)
.setFileUrl(fileUrl)
.startDownload();

File names and MIME types modes

FileName.AUTOFileName.TIME_BASED
MimeType.AUTOMimeType.FROM_NAME

AUTO uses the URL, response headers, file extension, and content type to resolve the name and MIME type.

Error handling

Failures exceptions are instance of DownloadException:

@OverridepublicvoidonError(longid, UrioutputUri, Exceptionerror, DownloadTasktask) {
if (!(errorinstanceofDownloadException)) return;
DownloadExceptionfailure = (DownloadException) error;
DownloadException.Typetype = failure.getType();
inthttpCode = failure.getCode();
booleanretryable = failure.isRetryable();
Throwablecause = failure.getCause();
}

Error types:

NETWORK_LOSTTIMEOUTDNS_ERRORSSL_ERRORHTTP_ERRORENOSPCFILE_ERRORSTORAGE_PERMISSION_DENIEDOUTPUT_INVALIDRANGE_NOT_SUPPORTEDEMPTY_RESPONSECHECKSUM_FAILEDCANCELLEDUNKNOWN

Other configuration

SimpleDownloaderdownloader = SimpleDownloader.with(context)
.setId(customId)
.setUserAgent(userAgent)
.setConnectTimeout(30_000)
.setReadTimeout(30_000)
.setProgressInterval(300)
.setBufferSize(16 * 1024)
.enableHistory(true)
.enableSorting(true);

Custom IDs are optional. SimpleDownloader generates an ID when setId() is not used. An active task cannot be replaced by another task with the same ID.

Passing 0 for a connection or read timeout keeps the OkHttp default.

Use your own HTTP client:

OkHttpClientclient = newOkHttpClient.Builder()
.followRedirects(true)
.build();
SimpleDownloader.with(context)
.setHttpClient(client);

The HTTP client cannot be replaced while a worker is running or already scheduled.

Use a custom task list order:

SimpleDownloader.with(context)
.setTaskComparator(myComparator);

Formatting helpers

Stringsize = Formator.formatBytes(bytes);
Stringspeed = Formator.formatSpeed(bytesPerSecond);
Stringeta = Formator.formatEta(etaMs);

TypeResolver is used internally, but it is also available for resolving file extensions and MIME types for you 🙂.

Cleanup

When listeners and observers are owned by an Activity or Fragment, release them using the same owner object given to with(...):

@OverrideprotectedvoidonDestroy() {
SimpleDownloader.releaseCallbacks(this);
SimpleDownloader.releaseObserver(observer);
super.onDestroy();
}

Call shutdown() only when you intentionally want to stop the library and release all workers, network callbacks, HTTP resources, thumbnails, database, etc:

SimpleDownloader.shutdown();

You do not need to call shutdown() normally or when an Activity is destroyed.

Support

Found a problem or have a suggestion? Open an issue: https://github.com/jeetarc/SimpleDownloader/issues

Copyright © 2026 Jeet / Jeetarc.

About

A modern Android download manager library designed to simplify complex file downloading behind a simple API

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages