Document Downloads

PSPDFKit usually works best with PDF documents on the local file system of your device. While you can also open documents from practically any remote source using a custom data provider there are several reasons speaking for using local file system documents (performance, cache control, battery impact, ...). If you have a PDF document that is not on the local filesystem, you can use the document download API to store it on your device before opening it.

Tip: You can find further example code inside the DocumentDownloadExample of the catalog app.

Creating a download request

Before initiating a download you need to create a DownloadRequest object that defines all information required to perform the download. Using the DownloadRequest.Builder you can set the download source, the output file or folder, whether existing files should be overwritten, etc. A call to Builder#build will then output the immutable DownloadRequest object.

1
2
3
val request: DownloadRequest = DownloadRequest.Builder(context)
    .uri("content://com.example.app/documents/example.pdf")
    .build()
Copy
1
2
3
final DownloadRequest request = new DownloadRequest.Builder(context)
    .uri("content://com.example.app/documents/example.pdf")
    .build();

The DownloadRequest.Builder#uri method can handle URIs pointing to content providers or to file in the app's assets (i.e. URIs starting with file:///android_asset/).

Starting the download

Once you have created a DownloadRequest you can start the download by passing it to DownloadJob#startDownload.

1
val job: DownloadJob = DownloadJob.startDownload(request)
1
final DownloadJob job = DownloadJob.startDownload(request);

The DownloadJob object has three responsibilities:

  • It holds a task which will handle downloading the PDF file from the source defined in the request to the file system.
  • It provides methods for observing and controlling the download progress.
  • By retaining the DownloadJob instance across configuration changes you can keep the download job alive and from being interrupted.

Observing download progress

The download job will continuously report the download progress using Progress instances. You can set a ProgressListener via DownloadJob#setProgressListener for observing progress, completion, and download errors. All methods of the listener are called on the main-thread. While this is useful for updating your UI based on the events, be aware of this when invoking longer running operations or network access.

Note: While there can only be a single ProgressListener per job, you register as many observers to the Observable<Progress> as you'd like.

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
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)
    }
})
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
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

If you prefer more control of how to receive Progress events, you can retrieve an RxJava Observable<Progress>. Similar to the ProgressListener, the observable will emit Progress events, as well as completion and error events. Events are being received on a background thread by default, so your code needs to handle switching to the main-thread itself.

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
job.progress
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(object : Subscriber<Progress>() {
        override fun onCompleted() {
            PdfActivity.showDocument(context, Uri.fromFile(output), configuration.build())
        }

        override fun onError(e: Throwable) {
            handleDownloadError(e)
        }

        override fun onNext(progress: Progress) {
            progressBar.setProgress((100f * progress.bytesReceived / progress.totalBytes).toInt())
        }
    })
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
job.getProgress()
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Subscriber<Progress>() {
        @Override public void onCompleted() {
            PdfActivity.showDocument(context, Uri.fromFile(output), configuration.build());
        }

        @Override public void onError(Throwable e) {
            handleDownloadError(exception);
        }

        @Override public void onNext(Progress progress) {
            progressBar.setProgress((int) (100 * progress.bytesReceived / (float) progress.totalBytes));
        }
    });

Note: Since progress events are non-critical, the progress observable will automatically handle back-pressure by dropping excessive events.

Implementing a custom download source

If you want to download a PDF from a custom source, e.g. the web, you can use your own DownloadSource implementation. The open()] method has to return an InputStream instance that will return the complete content of the PDF file. The getLength() method has to either return the download size in bytes, or DownloadSource#UNKNOWN_DOWNLOAD_SIZE if the size is not known. The returned size is optional, since it only used for creating higher quality Progress events.

Here's an example of a DownloadSource that can download a PDF file from the web:

Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
    }
}
Copy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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

To keep downloads running uninterruptedly when your activity is recreated during a configuration change, you need to retain the DownloadJob instance of your download. The simplest way to do that is to store it inside a retained Fragment and to retrieve it after the activity has been recreated.

Copy
MyActivity.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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)

            // By retaining this fragment it can 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.
            supportFragmentManager.beginTransaction().add(downloadFragment, 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) {
        supportFragmentManager.beginTransaction().remove(downloadFragment).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"
    }
}
Copy
MyActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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);

            // By retaining this fragment it can 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.
    }
}