Save a PDF file generated from a WebView after picking the file with Intent.ACTION_CREATE_DOCUMENT

Context: Android 10, API 29 .

I print a PDF file generated from a WebView , but now I’d like to save it to a file. So I tried the Intent.ACTION_CREATE_DOCUMENT to pick the file and save it via the printAdapter 's onWrite method.

The problem is that the file is always empty - 0 bytes - and no errors are raised. It justs calls onWriteFailed , but with an empty error message.

choosenFileUri has a value like content://com.android.providers.downloads.documents/document/37


The method I use to start the intent to pick a new file. Note that the result of this activity is a Uri :

fun startIntentToCreatePdfFile(fragment: Fragment, filename : String) {

    val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "application/pdf"
        putExtra(Intent.EXTRA_TITLE, filename)
    }

    fragment.startActivityForResult(intent, IntentCreatePdfDocument)
}

The method I use to “print” the PDF to a file. The fileUri comes from the Intent.ACTION_CREATE_DOCUMENT :

fun printPdfToFile(
    context: Context,
    webView: WebView,
    fileUri: Uri
) {

    (context.getSystemService(Context.PRINT_SERVICE) as? PrintManager)?.let {
        val jobName = "Print PDF to save it"
        val printAdapter = webView.createPrintDocumentAdapter(jobName)

        val printAttributes = PrintAttributes.Builder()
            .setMediaSize(PrintAttributes.MediaSize.ISO_A4)
            .setResolution(PrintAttributes.Resolution("pdf", "pdf", 600, 600))
            .setMinMargins(PrintAttributes.Margins.NO_MARGINS).build()

        printAdapter.onLayout(null, printAttributes, null, object : LayoutResultCallback() {
            override fun onLayoutFinished(info: PrintDocumentInfo, changed: Boolean) {

                context.contentResolver.openFileDescriptor(fileUri, "w")?.use {
                    printAdapter.onWrite(
                        arrayOf(PageRange.ALL_PAGES),
                        it,
                        CancellationSignal(),
                        object : WriteResultCallback() {

                        })
                }

            }
        }, null)
    }
}

What I do pick file onActivityResult :

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)

    if (resultCode != Activity.RESULT_OK) {
        return null
    }

    if (requestCode != IntentCreatePdfDocument) {
        throw Exception("RequestCode not implemented: $requestCode")
    }

    val choosenFileUri = data?.data

    // If it is null, nothing to do
    if (choosenFileUri == null) {
        return
    }

    try {

        HtmlHelpers.savePdfFromHtml(
            requireContext(),
            "html document to be represented in the WebView",
            choosenFileUri)

    } catch (exception: Exception) {
        _logger.error(exception)
        Helpers.showError(requireActivity(), getString(R.string.generic_error))
    }

    dismiss()
}

…where HtmlHelpers.savePdfFromHtml is:

fun savePdfFromHtml(
    context: Context,
    htmlContent: String,
    fileUri: Uri
) {
    generatePdfFromHtml(
        context,
        htmlContent
    ) { webView ->

        PrintHelpers.printPdfToFile(
            context,
            webView,
            fileUri)
    }
}

…and generatePdfFromHtml is:

private fun generatePdfFromHtml(
    context: Context,
    htmlContent: String,
    onPdfCreated: (webView: WebView) -> Unit
) {

    val webView = WebView(context)
    webView.settings.javaScriptEnabled = true
    webView.webViewClient = object : WebViewClient() {

        override fun onPageFinished(webView: WebView, url: String) {
            onPdfCreated(webView)
        }

    }

    webView.loadDataWithBaseURL(
        null,
        htmlContent,
        "text/html; charset=utf-8",
        "UTF-8",
        null);

}

I checked all the other answer about this topic, but everyone creates manually the ParcelFileDescriptor instead of it in the onWrite method. Everyone does something like this:

fun getOutputFile(path: File, fileName: String): ParcelFileDescriptor? {
    val file = File(path, fileName)
    return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_WRITE)
}

But I cannot do this since I have only the Uri.


Can you please help me?
Thanks!

Have you tried using the IntelliJ debugger?

When I have a bug like this often I find just stepping through each line, watching the variables change, and stepping into each method usually allows me to narrow my search to a single line/method.

When debugging, there’s a button to execute arbitrary code at your current point. Often after I’ve narrowed the issue I’ll test out solutions by executing manual steps without rerunning the code. Additionally you can manually change variable values while debugging.

Another cool trick that IntelliJ provides is that you can “drop the last stackframe” meaning you can kind of rewind your code while debugging* (not really since side effects). This is sometimes helpful for searching backwards from a breakpoint. And you can always view each stack frame on left side of the debugger without dropping frames.

2 Likes

Hi arocnies, thanks for you suggestions!

I’m coding with Android Studio, and I already tried to debug every single line from the button press to the last line, unfortunately I’m not understanding what’s happening.

Of course I’m going on with the debug part to get more clues.

Thanks!

I think this might be an issue with permissions because when you print the Android system doesn’t need to give you the permission to access that file, it just gives it to the printing app. Maybe try to request the storage permission at the start of the app?

1 Like

Android Studio is essentially IntelliJ that comes pre-installed with Android plugins so you can use the same debugging tools.

I think the first step is to narrow your search. You can start by setting a breakpoints to make sure each method you expect to be called actually gets called. When the code stops at each breakpoint you set, check if variables match what you expect. For example, you could check that the htmlContent isn’t blank.

After a few checks, you should be able to eliminate most of the code in your example as a possible cause of the issue. Your question would then go from:

“Here’s a lot of code that isn’t working and I don’t understand where to look”

to something like:

“Here’s a single line that I expect to create a new file. I know parameters I pass in are all correct and not empty and no exception is thrown but it still creates an empty file. Is there something I’m missing?”
Or:
“I have a function that I have tested and correctly creates the file, but my parameter for the content is empty. Here’s the line or two that I’m retrieving the content but when I call this I get nothing back. What am I missing?”
Or:
“I know this function properly creates a file, and I know the content is being retrieved correctly, but for some reason, this code is never called. I think my control flow should call this code. What am I missing?”

1 Like

Hi kyay10! I just tried to ask for that permission but nothing, I still get the same result.