如何在作用域存储中保存PDF?

7

在引入作用域存储之前,我使用下载管理器在我的应用程序中下载PDF并从getExternalStorageDirectory获取PDF,但由于作用域存储的原因,我不能再使用已被弃用的getExternalStorageDirectory方法。我决定放弃使用下载管理器,因为它将文件下载到公共目录,而是改用Retrofit来下载PDF文件。

我知道我可以在Android清单中使用requiredLegacyStorage标签,但这对Android 11不适用,所以我不使用它。

以下是我的代码:

fun readAndDownloadFile(context: Context) {
        readQuraanInterface?.downloadFile()
        Coroutines.io {
            file = File(context.filesDir,"$DESTINATION_DIRECTORY/$FILE_NAME$FILE_EXTENSION")
            if (file?.exists() == true) {
                renderPDF()
                showPdf(mPageIndex, Direction.None)
            } else {

                Log.i("new","new0")
                val response = readQuraanRepository.downloadPdf()
                if (response.isSuccessful) {
                    Log.i("new","new00 ${file!!.path} ${response.body()?.byteStream().toString()}")
                    response.body()?.byteStream()?.let {
                        file!!.copyInputStreamToFile(
                            it
                        )
                    }
                    Log.i("new","new1")
//                    renderPDF()
//                    showPdf(mPageIndex, Direction.None)
                } else {
                    Log.i("new","new2")
                    Coroutines.main {
                        response.errorBody()?.string()
                            ?.let { readQuraanInterface?.downloadFailed(it) }
                    }
                }
            }

        }

    }

    private fun File.copyInputStreamToFile(inputStream: InputStream) {
        this.outputStream().use { fileOut ->
            Log.i("new","new30")
            inputStream.copyTo(fileOut)
        }
    }

虽然 PDF 已经被下载,但是使用我编写的 InputStream 助手函数时文件从未被存储。我需要将该 PDF 添加到应用程序的内部存储并进行呈现,而我正在使用 PDFRenderer 进行呈现。

你如何检查文件是否已存储?如果未存储,则会出现错误/异常。 - blackapps
renderPDF()?为什么这个函数没有路径参数? - blackapps
@blackapps 一旦PDF文件下载完成,我的应用程序就会崩溃。它无法创建该文件。 - BraveEvidence
你没有回答我的问题,很奇怪。它会崩溃,因为你没有捕获异常。Logcat 会告诉你问题所在。 - blackapps
@blackapps logcat 没有显示任何错误。应用程序并没有完全崩溃,它会自动恢复。 - Neat
3个回答

5
您可以使用以下代码使用作用域存储下载并保存PDF。这里我使用Downloads目录。不要忘记授予所需的权限。
@RequiresApi(Build.VERSION_CODES.Q)
fun downloadPdfWithMediaStore() {
    CoroutineScope(Dispatchers.IO).launch {
        try {
            val url =
                URL("https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf")
            val connection: HttpURLConnection = url.openConnection() as HttpURLConnection
            connection.requestMethod = "GET"
            connection.doOutput = true
            connection.connect()
            val pdfInputStream: InputStream = connection.inputStream

            val values = ContentValues().apply {
                put(MediaStore.Downloads.DISPLAY_NAME, "test")
                put(MediaStore.Downloads.MIME_TYPE, "application/pdf")
                put(MediaStore.Downloads.IS_PENDING, 1)
            }

            val resolver = context.contentResolver

            val collection =
                MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)

            val itemUri = resolver.insert(collection, values)

            if (itemUri != null) {
                resolver.openFileDescriptor(itemUri, "w").use { parcelFileDescriptor ->
                    ParcelFileDescriptor.AutoCloseOutputStream(parcelFileDescriptor)
                        .write(pdfInputStream.readBytes())
                }
                values.clear()
                values.put(MediaStore.Downloads.IS_PENDING, 0)
                resolver.update(itemUri, values, null, null)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

1
如何在Java中使用此函数? - jazzbpn
有关 Xamarin.Forms 的任何线索吗? - Monica
1
无法在Android 11上使用自定义URL!出现以下错误:downloadPdfWithMediaStore$1.invokeSuspendSystem.err: java.io.FileNotFoundException: - Paramjit Singh Rana

4

如果您使用Retrofit动态Url保存文件,则会得到更清晰的解决方案。

  1. 创建Api
interface DownloadFileApi {

   @Streaming
   @GET
   suspend fun downloadFile(@Url fileUrl: String): Response<ResponseBody>
}

您可以像这样创建实例

 Retrofit.Builder()
         .baseUrl("http://localhost/") /* We use dynamic URL (@Url) the base URL will be ignored */
         .build()
         .create(DownloadFileApi::class.java)

注意:即使您不使用baseUrl,由于retrofit构建器需要它,因此需要设置一个有效的baseUrl。

  1. 将InputStream结果保存在存储设备中(您可以创建一个UseCase来执行此操作)。
class SaveInputStreamAsPdfFileOnDirectoryUseCase {

    /**
     * Create and save inputStream as a file in the indicated directory
     * the inputStream to save will be a PDF file with random UUID as name
     */
    suspend operator fun invoke(inputStream: InputStream, directory: File): File? {
        var outputFile: File? = null
        withContext(Dispatchers.IO) {
            try {
                val name = UUID.randomUUID().toString() + ".pdf"
                val outputDir = File(directory, "outputPath")
                outputFile = File(outputDir, name)
                makeDirIfShould(outputDir)
                val outputStream = FileOutputStream(outputFile, false)
                inputStream.use { fileOut -> fileOut.copyTo(outputStream) }
                outputStream.close()
            } catch (e: IOException) {
                // Something went wrong
            }
        }
        return outputFile
    }

    private fun makeDirIfShould(outputDir: File) {
        if (outputDir.exists().not()) {
            outputDir.mkdirs()
        }
    }
}
  1. 调用API并应用使用场景 :D
class DownloadFileRepository constructor(
    private val service: DownloadFileApi,
    private val saveInputStreamAsPdfFileOnDirectory: SaveInputStreamAsPdfFileOnDirectoryUseCase
) {

    /**
     * Download pdfUrl and save result as pdf file in the indicated directory
     *
     * @return Downloaded pdf file
     */
    suspend fun downloadFileIn(pdfUrl: String, directory: File): File? {
        val response = service.downloadFile(pdfUrl)
        val responseBody = responseToBody(response)
        return responseBody?.let { saveInputStreamAsFileOnDirectory(it.byteStream(), directory) }
    }
    
    fun responseToBody(response: Response<ResponseBody>): ResponseBody? {
        if (response.isSuccessful.not() || response.code() in 400..599) {
            return null
        }
        return response.body()
    }
}

注意: 您可以使用 ContextCompat.getExternalFilesDirs(applicationContext,“documents”).firstOrNull() 作为保存目录


0

我正在使用以下代码,目标 API 为 30,在下载后将其保存在内部下载目录中。

       DownloadManager.Request request = new DownloadManager.Request(Uri.parse(url));//url=The download url of file
                    request.setMimeType(mimetype);
                    //------------------------COOKIE!!------------------------
                    String cookies = CookieManager.getInstance().getCookie(url);
                    request.addRequestHeader("cookie", cookies);
                    //------------------------COOKIE!!------------------------
                    request.addRequestHeader("User-Agent", userAgent);
                    request.setDescription("Qawmi Library Downloading");//Description
                    request.setTitle(pdfFileName);//pdfFileName=String Name of Pdf file
                    request.allowScanningByMediaScanner();
                    request.setAllowedOverMetered(true);
                    request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
                    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
                        request.setDestinationInExternalPublicDir("/Qawmi Library"/*Custom directory name below api 29*/, pdfFileName);
                    } else {
//Higher then or equal api-29                        
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS,"/"+pdfFileName);
                    }
                    DownloadManager dm = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
                    dm.enqueue(request);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接