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

Viewed 876

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.


Edit: as suggested by @blackapps, I tried to open the output stream after I got the FileDescriptor, but I still got the same result:

context.contentResolver.openFileDescriptor(fileUri, "w")?.use {

    val fileDescriptor = it
    FileOutputStream(it.fileDescriptor).use {
        printAdapter.onWrite(
            arrayOf(PageRange.ALL_PAGES),
            fileDescriptor,
            CancellationSignal(),
            object : WriteResultCallback() {

            })

    }


}
2 Answers

For generating the PDF file from the HTML content I've used this Library

I'm storing the pdf file inside the download folder of shared storage(External). Use the below method to retrieve the location.

       //fileName is the name of the file that you want to save. 
      fun getSavedFile(context: Context, fileName: String): File {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                return File(
                    context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)!!,
                    "$fileName.pdf"
                )
            }
            return File(
                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
                "$fileName.pdf"
            )
        }

Then use the library inbuilt method to generate the PDF from the HTML content loading inside the WebView

//webView is the ID of WebView where we are loading the html content
// fileName : Pass whatever name you want.
            
   PDFUtil.generatePDFFromWebView(pdfDownloadUtil.getSavedFile(getApplicationContext(), fileName), webView, new PDFPrint.OnPDFPrintListener() {
                @Override
                public void onSuccess(File file) {
                    savedPdfFile = file;
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                        createFile(Uri.fromFile(file));
                    } 
                }

                @Override
                public void onError(Exception exception) {

                }
            });
        }

Now, We need to fire the intent after getting the pdf of passed html content.

  private void createFile(Uri pickerInitialUri) {
        Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("application/pdf");
        intent.putExtra(Intent.EXTRA_TITLE, fileName);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri);
        }
        createFileResult.launch(intent);
    }

   ActivityResultLauncher<Intent> createFileResult = registerForActivityResult(
            new ActivityResultContracts.StartActivityForResult(),
            result -> {
                if (result.getResultCode() == AppCompatActivity.RESULT_OK) {
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                        pdfDownloadUtil.writeFileContent(this, savedPdfFile, result.getData().getData(), () -> {
                            showSnackToOpenPdf();
                            return null;
                        });
                    }
                }
            }
    );

In oreo or above device, We are using the above method i.e writeFileContent

@RequiresApi(Build.VERSION_CODES.O)
    fun writeFileContent(
        context: @NotNull Context,
        savedPdfFile: @NotNull File,
        uri: @NotNull Uri,
        listener: () -> Unit
    ) {
        try {
            val file = uri.let { context.contentResolver.openFileDescriptor(it, "w") }

            file?.let {
                val fileOutputStream = FileOutputStream(it.fileDescriptor)
                fileOutputStream.write(Files.readAllBytes(savedPdfFile.toPath()))
                fileOutputStream.close()
                it.close()
            }
            listener()


        } catch (e: FileNotFoundException) {
            //print logs
            e.printStackTrace()
        } catch (e: IOException) {
            //print logs
            e.printStackTrace()
        }
    }

Note: If the number of pages is large i.e more than 200 pages. Then it won't work as internally it's cache the pages in the WebView and then load it. So alternative way is to get the link to the PDF file from the API.

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.

-> Uri contains the getPath() method. You can use this to create a file object,

val file = File(uri.path) 

and use in ParcelFileDescriptor. Null-check the getPath() method.

Related