Android SDK 30及以上版本如何访问外部存储中的任意文件?
Android 30+ 读取外部存储中非自建文件的解决方案
针对你在Android 30+版本中无法访问Documents/MyApp目录下非自建文件的问题,目前合规且可行的核心方案是通过SAF(存储访问框架)获取整个目录的访问权限,以下是具体修正和说明:
1. 修正SAF的使用方式(关键)
你之前使用ACTION_OPEN_DOCUMENT仅能选择单个文件,且初始Uri用Uri.fromFile()的方式在Scoped Storage下会被系统限制,导致无法看到非自建文件。正确做法是使用ACTION_OPEN_DOCUMENT_TREE让用户授权整个Documents/MyApp目录的访问权限,这样应用就能访问该目录下所有文件(包括用户上传的)。
示例代码:
private static final int REQUEST_CODE_PICK_DIR = 1001; // 唤起目录选择器,初始定位到Documents/MyApp private void requestMyAppDirAccess() { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); // 尝试设置初始目录为Documents/MyApp Uri externalFilesUri = MediaStore.Files.getContentUri("external"); String selection = MediaStore.Files.FileColumns.RELATIVE_PATH + "=?"; String[] selectionArgs = new String[]{"Documents/MyApp/"}; Cursor cursor = getContentResolver().query(externalFilesUri, null, selection, selectionArgs, null); if (cursor != null && cursor.moveToFirst()) { long docId = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)); Uri initialDirUri = DocumentsContract.buildDocumentUriUsingTree(externalFilesUri, String.valueOf(docId)); intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialDirUri); cursor.close(); } startActivityForResult(intent, REQUEST_CODE_PICK_DIR); } // 处理权限回调 @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_PICK_DIR && resultCode == RESULT_OK && data != null) { Uri treeUri = data.getData(); if (treeUri != null) { // 持久化权限,重启应用后仍有效 getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); // 遍历目录下的所有文件(包括非自建) traverseDirectory(treeUri); } } } // 遍历SAF授权的目录 private void traverseDirectory(Uri treeUri) { Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, DocumentsContract.getTreeDocumentId(treeUri)); Cursor cursor = getContentResolver().query(childrenUri, new String[]{DocumentsContract.Document.COLUMN_DISPLAY_NAME, DocumentsContract.Document.COLUMN_DOCUMENT_ID}, null, null, null); if (cursor != null) { while (cursor.moveToNext()) { String fileName = cursor.getString(0); Log.d("FileAccess", "找到文件:" + fileName); // 根据documentId获取文件Uri,进行读取操作 Uri fileUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, cursor.getString(1)); // 读取文件示例 try (InputStream is = getContentResolver().openInputStream(fileUri)) { // 处理文件流 } catch (IOException e) { e.printStackTrace(); } } cursor.close(); } }
2. 为什么之前的方案无效?
- EXTERNAL_STORAGE权限:Android 30+中该权限仅能让应用访问自己创建的文件,无法突破Scoped Storage对非自建文件的限制。
- READ_MEDIA权限:仅针对图片、音频、视频类媒体文件,你的自定义文档不在其覆盖范围内,所以无法生效。
- MANAGE_EXTERNAL_STORAGE权限:Google Play对该权限的审核极为严格,仅允许系统级或特殊用途应用使用,普通应用提交会被拒绝。
- 旧SAF代码问题:
ACTION_OPEN_DOCUMENT仅能授权单个文件,且Uri.fromFile()生成的file://Uri在Scoped Storage下被系统屏蔽,导致选择器无法显示非自建文件。
3. 关于intent-filter的补充
你之前注册的file scheme intent-filter在Android 10+已被限制,若要让其他应用唤起你的应用打开.custom文件,需改用content scheme:
<intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <data android:scheme="content" /> <data android:mimeType="application/octet-stream" /> <data android:pathPattern=".*\\.custom" /> </intent-filter>
但此方案仅解决外部应用唤起的场景,无法让你的应用主动访问目录下的非自建文件,核心需求仍需依赖SAF目录授权。
总结
在Android 30+的Scoped Storage规则下,SAF目录授权是唯一合规且可行的方案,无需移除功能。按照上述代码实现后,用户只需授权一次Documents/MyApp目录,应用就能正常访问其中所有文件。
内容的提问来源于stack exchange,提问作者Andrei Herford
相关产品推荐
相关产品推荐

