Printing a PDF from a WebView

from the CommonsWare Community archives

At December 19, 2019, 2:54pm, Raptor asked:

Hello,

I have a… problem. I have a WebView that so far was receiving an HTML that I could print using the PrintManager in Android. Basically, it would launch the system’s printing screen and there I could save it as a PDF or actually print it on a portable printer.

However, the backend decided to send a pdf file, instead. The code is complicated because this is more or less (more) legacy code and right now I’m calling webView.loadUrl(OnlineEnrollmentRepositoryFactory.createRepository().termsAndConditionsWebPageUrl(), headers) to load the HTML (which is now a PDF).

The problem is that I can’t display the PDF in the WebView anymore, as it’s not an HTML. So we plan on getting rid of the WebView (or at least, set its visibility to GONE) and simply launch the printing screen as soon as the PDF has loaded in the now hidden WebView. But this doesn’t work: the printing screen keeps on saying “preparing to print” or something like that, forever.

How could I keep the current request (so that I’m using the same loadUrl() method to get the pdf instead of the former HTML) and then print it? This way I could get around refactoring the whole thing. Is it possible to get the PDF in the WebView and print it from there as soon as I get it?


At December 19, 2019, 10:05pm, mmurphy replied:

Don’t we all? :grin:

Um, how are you doing that? WebView has no built-in ability to render a PDF.

I doubt that is possible. I mean, you can use something like Mozilla’s PDF.js to show a PDF in a WebView. I demonstrate how to do that in the chapter on viewing PDFs in The Busy Coders’ Guide to Android Development. However:

Just print the PDF directly. I show how to that in the chapter on printing in The Busy Coder’s Guide to Android Development.


At December 20, 2019, 9:51am, Raptor replied:

Thanks, that’s what I will be doing. However, there’s still the issue of obtaining the PDF from the endpoint. In this flow the calls are configured to use RxJava. My Retrofit interface methods all return either Singles or Observables.

If I make a new call in my interface, what should it be? Meaning, what should it return? Observable<ByteArray>? In other words, what will be the “type” of the PDF that is being sent from the endpoint? Is it a MultiPart? The endpoint is sending a PDF file when you target it with a GET call from PostMan. Then, this PDF also needs to be saved in the phone. In the past, that was done from the print menu launched using PrintManager with the WebView as its parameter, and you could simply save it as PDF from there. Now, this will need to be done programmatically (supposedly, in the subscribe block where the call’s return type is observed).

So this is my conundrum right now.

In the meantime, I’ll take a look in your book at the chapters that you mentioned.


At December 20, 2019, 12:16pm, mmurphy replied:

I wouldn’t use Retrofit, personally, for this sort of thing. I would use an OkHttpClient (shared with my Retrofit instance) to make the HTTP request directly, then use Okio to stream the response to disk. In this sample repository from Elements of Android Q, I use OkHttp to stream down a video and write it to an OutputStream supplied by MediaStore. The MediaStore bits are clunky, but the OkHttp stuff is just a few lines.


At December 23, 2019, 1:02pm, Raptor replied:

So, I managed to get the PDF from the endpoint through RxJava, like this:

disposables.add(OnlineEnrollmentRepositoryFactory.createRepository().termsAndConditionGetCall()
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .doOnError {
                progressBar?.visibility = View.GONE
                Toast.makeText(context, "The PDF could not be obtained", Toast.LENGTH_SHORT).show()
            }
            .doOnSuccess {
                progressBar?.visibility = View.GONE
                printAction?.isEnabled = true
                val termsAndConditionsPdf = it.bytes()
                writePdfToInternalStorage(termsAndConditionsPdf)
            }
            .subscribe())

and I have also managed to save the file to my “Downloads” directory, like this:

private fun writePdfToInternalStorage(termsAndConditionsPdf: ByteArray) {
    val file = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "MyPDFFolder")
    if (!file.exists()) file.mkdir()

    try {
        val pdf = File(file, "${System.currentTimeMillis()}.pdf")
        val fileOutputStream = FileOutputStream(pdf)
        IOUtils.write(termsAndConditionsPdf, fileOutputStream)
    } catch (t: IOException) {
        Log.e("T&C", "Error writing the T&C PDF file")
    }
}

The only thing left, here, is to print the pdf. Now, I don’t know what the right approach should be or what is possible. Can I just open the saved PDF in my Downloads folder and print it from there (can I do this programmatically, as soon as the file is finished saving)? How do I know when has the file saving been finished (is there a callback so that I can open the PDF from there)?

Basically, I want to receive the PDF from the endpoint, save it, and then immediately launch the print dialog for that PDF, programmatically. Then, when the user presses the back button (or switches back to my app), I can continue with my flow. I have done the first two steps, what is a good way to do the third one?

Thanks!


At December 23, 2019, 1:24pm, mmurphy replied:

I think that you are doing your disk I/O on the main application thread.

Note that by default you do not have access to that filesystem location on Android 10.

AFAIK, it is complete once IOUtils.write() returns.

Presumably. I show in The Busy Coder’s Guide to Android Development how to print a PDF.


At December 23, 2019, 1:29pm, Raptor replied:

Yes, am I currently reading that chapter.


At December 23, 2019, 1:47pm, mmurphy replied:

Yeah, unfortunately Google makes it somewhat annoying to print an existing PDF. I filed an issue to get them to include PDF-printing capability in Android and was rebuffed. :man_shrugging:


At December 23, 2019, 1:51pm, Raptor replied:

What’s the situation in Android 10? I have a Note 10 phone that’s been recently upgraded to Android 10, maybe I should try on it, too.


At December 23, 2019, 2:08pm, mmurphy replied:

I wrote nearly a dozen blog posts about it this year (including this one), plus a chapter in Elements of Android Q. In a nutshell, you do not have access to external storage by default on Android 10, except via methods on Context. Environment.getExternalStoragePublicDirectory() is deprecated, and attempts to use it will fail by default.


At December 23, 2019, 2:10pm, Raptor replied:

Yeah, I remember these. Why do all these things have to be this complicated? lol