Open PDFs from URLs on Android
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:
PdfActivityIntentBuilder.fromUriPdfUiFragmentBuilder.fromUriPdfDocumentLoader.openDocumentPdfDocumentLoader.openDocumentAsync
To load an image, use one of these methods:
PdfActivityIntentBuilder.fromImageUriPdfUiFragmentBuilder.fromImageUriImageDocumentLoader.openDocumentImageDocumentLoader.openDocumentAsync
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}"))// We use a `File` object for easy parsing of the filename from the URL.final var file = new File(urlString);final var provider = new UrlDataProvider(new URL(urlString), new File(context.getFilesDir(), "myDownloads/" + file.getName()));PdfDocumentLoader and ImageDocumentLoader don’t accept data providers directly. Wrap the provider in a DocumentSource:
PdfDocumentLoader.openDocument(context, DocumentSource(provider))PdfDocumentLoader.openDocument(context, new DocumentSource(provider));To load a PDF, use one of these methods:
PdfActivityIntentBuilder.fromDataProviderPdfUiFragmentBuilder.fromDataProviderPdfDocumentLoader.openDocumentPdfDocumentLoader.openDocumentAsync
To load an image, use one of these methods:
PdfActivityIntentBuilder.fromImageProviderPdfUiFragmentBuilder.fromImageProviderImageDocumentLoader.openDocumentImageDocumentLoader.openDocumentAsync
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()final DownloadRequest request = new 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)final DownloadJob job = 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) }})job.setProgressListener(new DownloadJob.ProgressListenerAdapter() { @Override public void onProgress(@NonNull Progress progress) { progressBar.setProgress((int) (100 * progress.bytesReceived / (float) progress.totalBytes)); }
@Override public void onComplete(@NonNull File output) { PdfActivity.showDocument(context, Uri.fromFile(output), configuration.build()); }
@Override public void onError(@NonNull Throwable exception) { 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()) } )job.getProgress() .observeOn(AndroidSchedulers.mainThread()) .subscribe( progress -> progressBar.setProgress((int) (100 * progress.bytesReceived / (float) progress.totalBytes)), error -> handleDownloadError(error), () -> PdfActivity.showDocument(context, Uri.fromFile(job.getOutputFile()), 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:
- Return the complete PDF content through an
InputStream(opens in a new tab) fromopen(). - Return the download size in bytes from
getLength(), orDownloadSource#UNKNOWN_DOWNLOAD_SIZEwhen unknown.
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 }}private static class WebDownloadSource implements DownloadSource { @NonNull private final URL documentURL;
private WebDownloadSource(@NonNull URL documentURL) { this.documentURL = documentURL; }
@Override public InputStream open() throws IOException { final HttpURLConnection connection = (HttpURLConnection) documentURL.openConnection(); connection.connect(); return connection.getInputStream(); }
@Override public long getLength() { long length = DownloadSource.UNKNOWN_DOWNLOAD_SIZE;
try { final int contentLength = documentURL.openConnection().getContentLength(); if (contentLength != -1) { length = contentLength; } } catch (IOException e) { 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" }}public class MyActivity extends AppCompatActivity implements DownloadJob.ProgressListener {
/** * A non-UI fragment for retaining the download job. */ public final static class DownloadFragment extends Fragment { public DownloadJob job;
@Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState);
// Retaining this fragment allows it to carry the attached download job across configuration changes. setRetainInstance(true); } }
/** * Tag of the retained fragment. */ final static String DOWNLOAD_FRAGMENT = "download_fragment";
/** * Fragment for holding and retaining the current download job. */ private DownloadFragment downloadFragment;
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
// This will return an existing fragment if the activity was recreated while a download was running. downloadFragment = (DownloadFragment) getSupportFragmentManager().findFragmentByTag(DOWNLOAD_FRAGMENT); if (downloadFragment != null) { downloadFragment.job.setProgressListener(this); } }
@Override protected void onDestroy() { super.onDestroy();
// Clear the listener to prevent activity leaks. if (downloadFragment != null) { downloadFragment.job.setProgressListener(null); } }
/** * This method will initiate the download. */ private void startDownload() { if (downloadFragment == null) { downloadFragment = new DownloadFragment();
// Once the fragment is added, the download is safely retained. getSupportFragmentManager().beginTransaction().add(downloadFragment, DOWNLOAD_FRAGMENT).commit(); }
final DownloadRequest request = new DownloadRequest.Builder(this) .uri("content://com.example.app/documents/example.pdf") .build();
downloadFragment.job = DownloadJob.startDownload(request); downloadFragment.job.setProgressListener(this); }
@Override public void onProgress(@NonNull Progress progress) { // Update your UI. }
@Override public void onComplete(@NonNull File output) { getSupportFragmentManager().beginTransaction().remove(downloadFragment).commit();
// Handle the finished download. }
@Override public void onError(@NonNull Throwable exception) { // Report and handle download errors. }}