使用 Intent.ACTION_CREATE_DOCUMENT 选择文件后保存从 WebView 生成的 PDF 文件

Posted

技术标签:

【中文标题】使用 Intent.ACTION_CREATE_DOCUMENT 选择文件后保存从 WebView 生成的 PDF 文件【英文标题】:Save a PDF file generated from a WebView after picking the file with Intent.ACTION_CREATE_DOCUMENT 【发布时间】:2021-01-03 10:40:38 【问题描述】:

上下文:Android 10,API 29

我打印了一个从WebView 生成的 PDF 文件,但现在我想将它保存到一个文件中。所以我尝试使用Intent.ACTION_CREATE_DOCUMENT 选择文件并通过printAdapteronWrite 方法保存。

问题在于文件始终为空 - 0 字节 - 并且没有引发错误。它只是调用onWriteFailed,但错误消息为空。

choosenFileUri 的值类似于 content://com.android.providers.downloads.documents/document/37


我用来启动 Intent 以选择新文件的方法。请注意,此活动的结果是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)

我用来将 PDF“打印”到文件的方法。 fileUri 来自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)
    

我做什么选择文件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()

...HtmlHelpers.savePdfFromHtml 是:

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

        PrintHelpers.printPdfToFile(
            context,
            webView,
            fileUri)
    

...而generatePdfFromHtml 是:

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);



我检查了有关此主题的所有其他答案,但每个人都在 onWrite 方法中手动创建 ParcelFileDescriptor 而不是 it。每个人都这样做:

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

但我不能这样做,因为我只有 Uri。


编辑:按照@blackapps 的建议,我在得到FileDescriptor 后尝试打开输出流,但我仍然得到相同的结果

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

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

            )

    



【问题讨论】:

同时显示您的 onActivityResult() 代码。 您是否忘记为该描述符打开 FileOutputStream? @blackapps 我更新了答案! 不,仅此而已。希望其他人能提供更好的帮助。 我有同样的问题 content://com.android.providers.downloads.documents/document/37 是一个下载文件夹,然后我使用库来解决这个库 github.com/HBiSoft/PickiT 的 uri,希望这有帮助 【参考方案1】:

为了从 HTML 内容生成 PDF 文件,我使用了这个Library

我将 pdf 文件存储在共享存储(外部)的 download 文件夹中。使用以下方法检索位置。

       //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"
            )
        

然后使用库内置方法从HTML 内容加载到WebView 中生成PDF

//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) 

                
            );
        

现在,我们需要在获取传递的 html 内容的 pdf 后触发 Intent。

  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;
                        );
                    
                
            
    );

在 oreo 或以上设备中,我们使用上述方法,即 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()
        
    

注意:如果页数很大,即超过 200 页。然后它将无法在内部工作,因为它会在 WebView 中缓存页面然后加载它。因此,另一种方法是从 API 获取 PDF 文件的链接。

【讨论】:

【参考方案2】:

我检查了有关此主题的所有其他答案,但每个人都手动创建 ParcelFileDescriptor 而不是在 onWrite 方法中创建它。每个人都这样做:

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

但我不能这样做,因为我只有 Uri。

-> Uri 包含 getPath() 方法。您可以使用它来创建文件对象,

val file = File(uri.path) 

并在 ParcelFileDescriptor 中使用。空检查 getPath() 方法。

【讨论】:

如果我这样做,它会在打开 ParcelFileDescription 时抛出异常,提示该文件不存在!

以上是关于使用 Intent.ACTION_CREATE_DOCUMENT 选择文件后保存从 WebView 生成的 PDF 文件的主要内容,如果未能解决你的问题,请参考以下文章

在使用加载数据流步骤的猪中,使用(使用 PigStorage)和不使用它有啥区别?

今目标使用教程 今目标任务使用篇

Qt静态编译时使用OpenSSL有三种方式(不使用,动态使用,静态使用,默认是动态使用)

MySQL db 在按日期排序时使用“使用位置;使用临时;使用文件排序”

使用“使用严格”作为“使用强”的备份

Kettle java脚本组件的使用说明(简单使用升级使用)