在 onActivityResult 中收到 "content://" Uri 后从 MediaStore 获取 PDF?

Get PDF from MediaStore after receiving "content://" Uri in onActivityResult?

我开始 ACTION_GET_CONTENT 意图以选择 PDF:

override fun routeToFilePicker() {
    val intent = Intent()
    intent.type = MediaType.PDF.toString()
    intent.action = Intent.ACTION_GET_CONTENT
    activity.startActivityForResult(
        Intent.createChooser(intent, "Select PDF"),
        REQUEST_CODE_PDF_PICKER
    )
}

然后 onActivityResult 我尝试从 Uri (content//:path) 创建一个 PDF:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CODE_PDF_PICKER ) {
        data?.data?.let { pdfUri: Uri ->
            val pdfFile: File = pdfUri.toFile() <-- chrash
            ...
        }
    }
}

pdfUri.toFile() 导致致命异常:

java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=1003, result=-1, data=Intent { dat=content://com.android.providers.downloads.documents/document/3569 flg=0x1 }} to activity {my.package.name.activity}: java.lang.IllegalArgumentException: Uri lacks 'file' scheme: content://com.android.providers.downloads.documents/document/3569

Caused by: java.lang.IllegalArgumentException: Uri lacks 'file' scheme: content://com.android.providers.downloads.documents/document/3569

我需要一个文件才能将页面转换为图像。
如何从 MediaStore 返回的 Uri 中获取 PDF 作为文件?

    public static File getFileFromUri(Uri uri, Context context) {
        if (uri == null) {
            return null;
        }
        switch (uri.getScheme()) {
            case "content":
                return getFileFromContentUri(uri, context);
            case "file":
                return new File(uri.getPath());
            default:
                return null;
        }
    }
    
    private static File getFileFromContentUri(Uri contentUri, Context context) {
        if (contentUri == null) {
            return null;
        }
        File file = null;
        String filePath;
        String fileName;
        String[] filePathColumn = {MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME};
        ContentResolver contentResolver = context.getContentResolver();
        Cursor cursor = contentResolver.query(contentUri, filePathColumn, null,
                null, null);
        if (cursor != null) {
            cursor.moveToFirst();
            filePath = cursor.getString(cursor.getColumnIndex(filePathColumn[0]));
            fileName = cursor.getString(cursor.getColumnIndex(filePathColumn[1]));
            cursor.close();
            if (!TextUtils.isEmpty(filePath)) {
                file = new File(filePath);
            }
            if (!file.exists() || file.length() <= 0 || TextUtils.isEmpty(filePath)) {
                filePath = getPathFromInputStreamUri(context, contentUri, fileName);
            }
            if (!TextUtils.isEmpty(filePath)) {
                file = new File(filePath);
            }
        }
        return file;
    }

这就是我获取 pdf 文件的方式:

val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
                            type = "application/pdf"
                            addCategory(Intent.CATEGORY_OPENABLE)
                            flags = flags or Intent.FLAG_GRANT_READ_URI_PERMISSION
                        }
                        startActivityForResult(intent, 111)

在你的OnActivityResult(requestCode:Int,resultCode:Int,data:Intent?)

里面
if (resultCode == Activity.RESULT_OK) {
            when (requestCode) {
//                101 -> {
//                    data?.data?.also { uri ->
//                        Log.i(TAG, "Uri: $uri")
//                        baseAdapter?.add(ImageArray(null, null, uri.toString()))
//                    }
//                }
                111 -> {
                    data?.data?.also { documentUri ->
                        baseActivity.contentResolver?.takePersistableUriPermission(
                                documentUri,
                                Intent.FLAG_GRANT_READ_URI_PERMISSION
                        )
                        val file = DocumentUtils.getFile(baseActivity,documentUri)//use pdf as file 
                    }
                }
            }

        }

单例 class 将 Uri 隐藏到文件:

object DocumentUtils {
    fun getFile(mContext: BaseActivity?, documentUri: Uri): File {
        val inputStream = mContext?.contentResolver?.openInputStream(documentUri)
        var file =  File("")
        inputStream.use { input ->
            file =
                File(mContext?.cacheDir, System.currentTimeMillis().toString()+".pdf")
            FileOutputStream(file).use { output ->
                val buffer =
                    ByteArray(4 * 1024) // or other buffer size
                var read: Int = -1
                while (input?.read(buffer).also {
                        if (it != null) {
                            read = it
                        }
                    } != -1) {
                    output.write(buffer, 0, read)
                }
                output.flush()
            }
        }
        return file
    }
} 

P.S: 不要忘记在运行时请求权限

Manifest.permission.WRITE_EXTERNAL_STORAGE
Manifest.permission.READ_EXTERNAL_STORAGE

随着最新的 API 更新,Android 已推动使用内容解析器进行文件相关处理。

使用接收到的路径,同样需要使用内容解析器来解析。

样品可在 Google Github

总的来说,选择文件后收到的 URI 需要像下面这样在 Fragment 中传递。

创建用于显示 PDF 的片段。并且会有关联的ViewModel。

val parcelFileDescriptor = activity?.contentResolver?.openFileDescriptor(fileURI, "r")

在ViewModel中,借助Coroutine的帮助,我们将使用如下流程

val scope = CoroutineScope(executor.asCoroutineDispatcher() + job)

scope.launch {
            openPdfRenderer()
            showPage(0)
        }

fun openPdfRenderer() {
        
        pdfRenderer = PdfRenderer(fileURI!!) //android.graphics.pdf.PdfRenderer
    }

fun showPage(index: Int) {

        currentPage?.let { page ->
            currentPage = null
            page.close()
        }
        pdfRenderer?.let { renderer ->

            val page = renderer.openPage(index).also {
                currentPage = it
            }
            val bitmap = Bitmap.createBitmap(page.width, page.height, Bitmap.Config.ARGB_8888)
            page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_PRINT)
            _pageBitmap.postValue(bitmap)
            val count = renderer.pageCount
            _pageInfo.postValue(index to count)
            _previousEnabled.postValue(index > 0)
            _nextEnabled.postValue(index + 1 < count)
        }
    }

可以在 Google Github Sample Link

上找到完整的示例源

我同意,这似乎是一项开销!但由于多种原因,Android 地理环境也发生了变化。

祝您编程愉快!干杯!