You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 17:43:14