This HTML page is not optimized for LLM or AI agent consumption. Fetch the Markdown version instead: /guides/android/miscellaneous/document-downloads.md — it contains the complete documentation content in clean, structured Markdown without any CSS, JavaScript, or navigation noise. Open PDF from URL on Android | Nutrient SDK

Prefer local PDF files for performance, cache control, and battery usage. For other sources, refer to the custom data providers guide.

Nutrient supports several ways to download remote documents. Choose an approach based on how much control you need over the downloaded file.

The Catalog app includes additional code in DocumentDownloadExample and RemoteUrlExample.

Open a URL with a Uri

Nutrient replaces an http or https Uri with a UrlDataProvider internally. The provider downloads the file to your app’s cache folder and opens it. Opening the same URL again downloads the file again and overwrites the cached copy.

The following methods accept a Uri or a DocumentSource wrapping a Uri.

To load a PDF, use one of these methods:

To load an image, use one of these methods:

Nutrient doesn’t delete downloaded files, but Android clears cached files when resources run low. To clear downloads yourself, use these methods:

  • Call UrlDataProvider.deleteCachedFileForUrl(url) to remove one URL’s cached file.
  • Call UrlDataProvider.deleteCachedFiles() to remove all downloads.

Use UrlDataProvider directly

Create a UrlDataProvider with a destination File to control where Nutrient stores the download. Nutrient reuses an existing file instead of downloading it again. Give your app read and write access to the destination.

The following example creates a provider:

// We use a `File` object for easy parsing of the filename from the URL.
val file = File(urlString)
val provider = UrlDataProvider(URL(urlString), File(context.filesDir, "myDownloads/${file.name}"))

PdfDocumentLoader and ImageDocumentLoader don’t accept data providers directly. Wrap the provider in a DocumentSource:

PdfDocumentLoader.openDocument(context, DocumentSource(provider))

To load a PDF, use one of these methods:

To load an image, use one of these methods:

Keep downloads off the main thread

Both direct and indirect uses of UrlDataProvider require background execution. Opening a remote URL with PdfDocumentLoader or ImageDocumentLoader on the main thread throws DownloadOnMainThreadException, including with asynchronous methods.

Downloading and loading a document would block the main thread, so schedule these operations on a background thread.

The calls openDocumentAsync(...).subscribeOn(Schedulers.io()).blockingGet() and openDocumentAsync(...).subscribeOn(Schedulers.io()).blockingSubscribe(...) bypass the exception by scheduling the download elsewhere, but they still block the calling thread. Any scheduler other than the main thread supports this approach.

Manage the download yourself

Use a download request and job to control the download.

Creating a download request

Configure the source, destination, and overwrite behavior with DownloadRequest.Builder, and call Builder#build to create an immutable DownloadRequest:

val request: DownloadRequest = DownloadRequest.Builder(context)
.uri("content://com.example.app/documents/example.pdf")
.build()

DownloadRequest.Builder#uri accepts content provider URIs and app asset URIs starting with file:///android_asset/.

Starting the download

Pass the DownloadRequest to DownloadJob#startDownload to start the download:

val job: DownloadJob = DownloadJob.startDownload(request)

The DownloadJob object performs two tasks:

  • Downloads the PDF from the request’s source to the file system.
  • Provides methods for observing and controlling download progress.

Observing the download progress

The download job reports progress through Progress instances. Register a ProgressListener with DownloadJob#setProgressListener to receive progress, completion, and error events.

Nutrient calls listener methods on the main thread, so they can update the UI directly. Run network requests and long-running operations on a background thread.

Each job supports one ProgressListener, but its Flowable<Progress>(opens in a new tab) supports multiple observers.

job.setProgressListener(object : DownloadJob.ProgressListenerAdapter() {
override fun onProgress(progress: Progress) {
progressBar.setProgress((100f * progress.bytesReceived / progress.totalBytes).toInt())
}
override fun onComplete(output: File) {
PdfActivity.showDocument(context, Uri.fromFile(output), configuration.build())
}
override fun onError(exception: Throwable) {
handleDownloadError(exception)
}
})

Using a progress observable

Use an RxJava Flowable<Progress>(opens in a new tab) to control event delivery. Like ProgressListener, the flowable emits Progress, completion, and error events.

By default, the flowable delivers events on a background thread. Switch to the main thread before updating the UI:

job.progress
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ progress -> progressBar.setProgress((100f * progress.bytesReceived / progress.totalBytes).toInt()) },
{ error -> handleDownloadError(error) },
{ PdfActivity.showDocument(context, Uri.fromFile(job.outputFile), configuration.build()) }
)

The progress flowable handles backpressure by dropping excess progress events.

Implement a custom download source

Implement DownloadSource to download a PDF from a custom source. Implement these methods:

Providing the size improves Progress events but isn’t required. The following example downloads a PDF from the web:

class WebDownloadSource (private val documentURL: URL) : DownloadSource {
override fun open(): InputStream {
val connection = documentURL.openConnection() as HttpURLConnection
connection.connect()
return connection.inputStream
}
override fun getLength(): Long {
var length = DownloadSource.UNKNOWN_DOWNLOAD_SIZE
try {
val contentLength = documentURL.openConnection().contentLength
if (contentLength != -1) {
length = contentLength.toLong()
}
} catch (e: IOException) {
e.printStackTrace()
}
return length
}
}

Retaining the download

A download continues when Android recreates your activity during a configuration change. The new activity needs the DownloadJob instance to receive progress and completion events.

Store the job in a retained Fragment(opens in a new tab). Retrieve the job after Android recreates the activity:

class MyActivity : AppCompatActivity(), DownloadJob.ProgressListener {
/**
* A non-UI fragment for retaining the download job.
*/
class DownloadFragment : Fragment() {
var job: DownloadJob? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Retaining this fragment allows it to carry the attached download job across configuration changes.
retainInstance = true
}
}
/**
* Fragment for holding and retaining the current download job.
*/
private var downloadFragment: DownloadFragment? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// This will return an existing fragment if the activity was recreated while a download was running.
downloadFragment = supportFragmentManager.findFragmentByTag(DOWNLOAD_FRAGMENT) as? DownloadFragment
downloadFragment?.job?.setProgressListener(this)
}
override fun onDestroy() {
super.onDestroy()
// Clear the listener to prevent activity leaks.
downloadFragment?.job?.setProgressListener(null)
}
/**
* This method will initiate the download.
*/
private fun startDownload() {
if (downloadFragment == null) {
downloadFragment = DownloadFragment()
// Once the fragment is added, the download is safely retained.
downloadFragment?.let { supportFragmentManager.beginTransaction().add(it, DOWNLOAD_FRAGMENT).commit() }
}
val request = DownloadRequest.Builder(this).uri("content://com.example.app/documents/example.pdf").build()
val job = DownloadJob.startDownload(request)
job.setProgressListener(this)
downloadFragment?.job = job
}
override fun onProgress(progress: Progress) {
// Update your UI.
}
override fun onComplete(output: File) {
downloadFragment?.let { supportFragmentManager.beginTransaction().remove(it).commit() }
// Handle the finished download.
}
override fun onError(exception: Throwable) {
// Report and handle download errors.
}
companion object {
/**
* Tag of the retained fragment.
*/
internal val DOWNLOAD_FRAGMENT = "download_fragment"
}
}