Android 14及以上无需特殊权限从URI获取PDF文件路径
问题描述
我通过ACTION_OPEN_DOCUMENT Intent获取到PDF文件的URI,但当该URI的媒体类型为document时(调试发现问题出在代码第126行),无法获取其文件路径。获取图片路径时,使用READ_MEDIA_IMAGES或READ_EXTERNAL_STORAGE权限(Android 12及以下)没有问题,但PDF文件存在此问题。我不想申请外部存储全权限,避免谷歌商店审核问题,请问在API 30及以上系统中,能否无需特殊权限获取PDF文件路径?
相关代码
package maximsblog.blogspot.com.visionsender; import android.annotation.SuppressLint; import android.content.ContentUris; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Build; import android.os.Environment; import android.provider.DocumentsContract; import android.provider.MediaStore; import android.text.TextUtils; import androidx.loader.content.CursorLoader; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; class AndroidUtilities { static class IOUtils { private static final int BUFFER_SIZE = 1024 * 2; static int copy(InputStream input, OutputStream output) throws IOException { byte[] buffer = new byte[BUFFER_SIZE]; BufferedInputStream b = new BufferedInputStream(input, BUFFER_SIZE); BufferedOutputStream out = new BufferedOutputStream(output, BUFFER_SIZE); int count = 0; int n = 0; try { while((n =input.read(buffer, 0, BUFFER_SIZE))!= -1) { out.write(buffer, 0, n); count += n; } out.flush(); } finally { try { out.close(); } catch (IOException e) { } try { input.close(); } catch (IOException e) { } } return count; } } static boolean isWhatsappUri(Uri uri) { return "com.whatsapp.provider.media".equals(uri.getAuthority()); } static boolean isExternalStorageDocument(Uri uri) { return "com.android.externalstorage.documents".equals(uri.getAuthority()); } static boolean isDownloadsDocument(Uri uri) { return "com.android.providers.downloads.documents".equals(uri.getAuthority()); } static boolean isMediaDocument(Uri uri) { return "com.android.providers.media.documents".equals(uri.getAuthority()); } static boolean isGooglePhotosUri(Uri uri) { return "com.google.android.apps.photos.content".equals(uri.getAuthority()); } static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs, String columnName) { Cursor cursor =null; if (columnName == null) columnName = "_data"; try { cursor = context.getContentResolver().query( uri, new String[] {columnName}, selection, selectionArgs,null); if (cursor == null) return uri.getPath(); if (cursor.moveToFirst()) { int column_index = cursor.getColumnIndexOrThrow(columnName); if (columnName.equals("_data")) return cursor.getString(column_index); else return uri.toString() + "/" + cursor.getString(column_index); } } finally { if (cursor != null) cursor.close(); } return null; } static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { return getDataColumn(context, uri, selection, selectionArgs, null); } @SuppressLint("NewApi") static String getRealPathFromURI_API19(Context context, Uri uri) { String result = null; if (DocumentsContract.isDocumentUri(context, uri)) { String[] split =null; String docId=null; if (isExternalStorageDocument(uri)) { docId = DocumentsContract.getDocumentId(uri); String[] splitParts = docId.split(":"); String type = splitParts[0]; if ("primary".equalsIgnoreCase(type)) { return Environment.getExternalStorageDirectory().toString() + "/" + splitParts[1]; } } else if (isDownloadsDocument(uri)) { docId = DocumentsContract.getDocumentId(uri); Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId)); return getDataColumn(context, contentUri, null, null); } else if (isMediaDocument(uri)) { docId = DocumentsContract.getDocumentId(uri); split = docId.split(":"); String type = split[0]; Uri contentUri =null; if ("image".equals(type)) { contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; } else if ("video".equals(type)) { contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; } else if ("audio".equals(type)) { contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; } else if("document".equals(type)) { //in this line (126) we have a problems. How to get path of file for this case? //contentUri =MediaStore.getDocumentUri(context,uri); //crash,when we call this method. } String selection = "_id=?"; return getDataColumn(context, contentUri, selection, new String[] {split[1]}); } } else if ("content".equalsIgnoreCase(uri.getScheme())) { if (isGooglePhotosUri(uri)) return uri.getLastPathSegment(); return getDataColumn(context, uri, null, null); } else if ("file".equalsIgnoreCase(uri.getScheme())) { return uri.getPath(); } else if (isWhatsappUri(uri)) { return getDataColumn(context, uri, null, null, "_display_name"); } return null; } @SuppressLint("NewApi") static String getRealPathFromURI_API11to18(Context context, Uri contentUri) { if (isWhatsappUri(contentUri)) { return new File(contentUri.getPath()).getAbsolutePath(); } String[] proj = new String[] {MediaStore.Images.Media.DATA}; String result = null; CursorLoader cursorLoader = new CursorLoader( context, contentUri, proj, null, null, null ); Cursor cursor = cursorLoader.loadInBackground(); if (cursor != null) { cursor.moveToFirst(); result = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)); } cursor.close(); return result; } static String getRealPathFromURI_BelowAPI11(Context context, Uri contentUri) { if (isWhatsappUri(contentUri)) { return new File(contentUri.getPath()).getAbsolutePath(); } Cursor cursor = context.getContentResolver().query(contentUri, new String[] {MediaStore.Images.Media.DATA}, null, null, null); cursor.moveToFirst(); String result = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)); cursor.close(); return result; } static String getRealPath(Context context, Uri uri) { if(Build.VERSION.SDK_INT < 11) return getRealPathFromURI_BelowAPI11(context, uri); else if(Build.VERSION.SDK_INT < 19) return getRealPathFromURI_API11to18(context, uri); else return getRealPathFromURI_API19(context, uri); } static String getFileName(String Fpath) { String[] result=Fpath.split("/"); return result[result.length-1]; } static void copy(Context context, Uri srcUri, File dstFile) { try { InputStream inputStream = context.getContentResolver().openInputStream( srcUri); FileOutputStream outputStream = new FileOutputStream(dstFile); IOUtils.copy(inputStream, outputStream); inputStream.close(); outputStream.close(); } catch (IOException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } static String getFilePathFromURI(Context context, Uri contentUri) { String fileName = getFileName(contentUri.getPath()); if (!TextUtils.isEmpty(fileName)) { File copyFile = new File(context.getCacheDir().toString() + "/" + fileName); copy(context, contentUri, copyFile); return copyFile.getAbsolutePath(); } return null; } }
解决方案
1. API30+无法直接获取PDF的原始文件路径
Android 10及以上的分区存储机制,加上ACTION_OPEN_DOCUMENT返回的是文档提供者的URI(并非直接文件路径),导致你无法在无特殊权限的前提下拿到PDF的真实系统路径:
- 对于
document类型的媒体条目,MediaStore并未暴露_data列(存储文件路径的字段) - 非媒体类文件(如PDF)可能未被收录到MediaStore的媒体表中,无法通过查询获取路径
2. 正确的处理方式:放弃获取原始路径,直接操作URI
不需要执着于拿到系统文件路径,有两种合规的处理方式:
(1)直接读取URI内容
通过ContentResolver.openInputStream(uri)直接读取PDF内容,无需路径,也不需要任何外部存储权限:
InputStream inputStream = context.getContentResolver().openInputStream(pdfUri); // 用inputStream读取PDF内容,比如传入PDF解析库
(2)复制到APP私有目录获取本地路径
如果你的业务逻辑必须要文件路径(比如某些第三方库要求),可以将PDF复制到APP的私有目录(cacheDir或filesDir),你的代码里已经实现了getFilePathFromURI方法,这个方法不需要额外权限,因为私有目录属于APP自身空间,系统允许直接操作。
3. 修复代码第126行的问题
修改getRealPathFromURI_API19方法中document类型的分支,直接调用getFilePathFromURI返回私有目录的路径即可:
else if("document".equals(type)) { // 复制到APP缓存目录并返回路径 return getFilePathFromURI(context, uri); }
这样修改后,既不需要申请外部存储全权限,也能满足业务对文件路径的需求,同时符合谷歌商店的审核规范。
内容的提问来源于stack exchange,提问作者alexandr kozlovskiy

