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();- 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
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.
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.
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_URI3. 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();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.
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();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);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 whensetDeleteOnRemoval(true)is enabled.forceDownload()starts a queued task even when it is locked. It doesn't care about concurrency limit
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();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.
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 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.
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.
restored = downloader.restoreTasks();Active tasks are restored as paused. Resume the tasks you want to continue:
SimpleDownloader.resumeAll();
//orfor (DownloadTasktask : restored) {
task.resume();
}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_QUEUEFinished tasks are kept in the database only when history is enabled:
SimpleDownloader.with(context)
.enableHistory(true);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 > LOWTask statuses:
Status.STARTINGStatus.QUEUEDStatus.CONNECTINGStatus.DOWNLOADINGStatus.PAUSEDStatus.CANCELLEDStatus.WAITING_FOR_NETWORKStatus.RETRYINGStatus.COMPLETEDStatus.FAILEDThe 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.
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_NAMEDownloadTasktask = 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_ROAMINGWaiting tasks resume when the network becomes available by default. Change with:
SimpleDownloader.with(context)
.enableResumeOnNetworkGain(false);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 availableDownloadNotification can configure the channel, importance, lock-screen visibility, sound, vibration, color, update interval, actions, etc.
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();FileName.AUTOFileName.TIME_BASEDMimeType.AUTOMimeType.FROM_NAMEAUTO uses the URL, response headers, file extension, and content type to resolve the name and MIME type.
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_FAILEDCANCELLEDUNKNOWNSimpleDownloaderdownloader = 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);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 🙂.
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.
Found a problem or have a suggestion? Open an issue: https://github.com/jeetarc/SimpleDownloader/issues
Copyright © 2026 Jeet / Jeetarc.