Android API 33如何获取自定义非媒体文件的外部存储读写权限?
Android API 33下自定义非媒体文件的外部存储权限解决方案
问题背景
现有Android应用支持将PDF等非媒体自定义文件存储到外部存储的Documents/MyApp/Reports目录,同时读取该目录下应用创建或用户上传的文件。在API 32及以下版本运行正常,但API 33上出现以下问题:
- 权限申请对话框不弹出,
onRequestPermissionsResult无需用户交互直接返回权限拒绝 - 仅能访问应用自身创建的文件,用户上传到该目录的文件无法通过
dir.listFiles()识别 - 替换为细分媒体权限(
READ_MEDIA_IMAGES等)会误导用户且无法解决问题;使用MANAGE_EXTERNAL_STORAGE权限也无效
现有代码
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> ... <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> ... </manifest>
SomeActivity.java
public class SomeActivity extends NavigationActivity { ... public boolean askForFilePermissions() { if (Build.VERSION.SDK_INT >= 23) { boolean hasPermission = this.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED; if (!hasPermission) { requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, 1); return true; } } return false; } @Override public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case 1: { // If request is cancelled, the result arrays are empty. if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // permission was granted, yay! } else { // permission denied, boo! } return; } } } ... }
解决方案
API 33开始,Android对外部存储权限模型进行了调整,传统的读写权限不再支持跨应用访问非媒体文件,官方推荐使用**存储访问框架(SAF)**来实现这类需求。以下是具体适配方案:
1. 权限清单适配
将原有读写权限限定在API 32及以下版本生效,API 33+无需申请这些权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
2. 实现SAF目录授权逻辑
通过SAF让用户主动选择Documents/MyApp/Reports目录,并获取持久化访问权限,后续即可通过SAF API操作目录下的所有文件(包括用户上传的)。在SomeActivity中添加以下代码:
private static final int REQUEST_CODE_PICK_REPORTS_DIR = 2; // 申请目录访问权限 public void requestReportsDirectoryAccess() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // 启动SAF目录选择器 Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); // 可选:指定初始目录为Documents/MyApp/Reports(如果目录已存在) Uri externalStorageUri = MediaStore.getExternalStorageUri(this); String treeDocumentId = DocumentsContract.getTreeDocumentId(externalStorageUri); Uri initialDirUri = DocumentsContract.buildDocumentUriUsingTree( externalStorageUri, treeDocumentId + "/Documents/MyApp/Reports" ); intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialDirUri); startActivityForResult(intent, REQUEST_CODE_PICK_REPORTS_DIR); } else { // API 32及以下版本,沿用原有权限申请逻辑 askForFilePermissions(); } } @Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_PICK_REPORTS_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 ); // 将Uri保存到SharedPreferences,供后续使用 SharedPreferences prefs = getSharedPreferences("AppStoragePrefs", MODE_PRIVATE); prefs.edit().putString("REPORTS_DIR_URI", treeUri.toString()).apply(); // 通过DocumentFile操作目录下的文件 DocumentFile reportsDir = DocumentFile.fromTreeUri(this, treeUri); if (reportsDir != null && reportsDir.exists()) { // 遍历目录下所有文件(包括用户上传的) for (DocumentFile file : reportsDir.listFiles()) { // 处理文件,例如获取文件名、Uri等 String fileName = file.getName(); Uri fileUri = file.getUri(); // ... 业务逻辑处理 } } } } }
3. 后续文件操作逻辑
后续需要访问Documents/MyApp/Reports目录时,从SharedPreferences取出保存的Uri,通过DocumentFile进行操作,示例:
// 获取保存的目录Uri SharedPreferences prefs = getSharedPreferences("AppStoragePrefs", MODE_PRIVATE); String dirUriStr = prefs.getString("REPORTS_DIR_URI", null); if (dirUriStr != null) { Uri dirUri = Uri.parse(dirUriStr); DocumentFile reportsDir = DocumentFile.fromTreeUri(this, dirUri); if (reportsDir != null && reportsDir.isDirectory()) { // 创建新文件 DocumentFile newPdf = reportsDir.createFile("application/pdf", "new_report.pdf"); // 读取文件内容 InputStream inputStream = getContentResolver().openInputStream(newPdf.getUri()); // ... } }
方案说明
- SAF是API 33+访问非媒体自定义文件的官方标准方式,通过用户主动授权目录访问,符合Android隐私规范,不会出现权限申请失效的问题。
MANAGE_EXTERNAL_STORAGE权限仅针对文件管理器类等特殊应用开放,普通应用使用该权限难以通过Google Play审核,不建议采用。- 适配后,API 32及以下版本仍保留原有逻辑,保证兼容性。
内容的提问来源于stack exchange,提问作者Andrei Herford
相关产品推荐
相关产品推荐

