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

MediaProvider间歇性拒绝外部文件访问权限问题(Android13/Pixel6a)

问题:Android 13设备上文件操作时MediaProvider权限拒绝日志

在运行Android 13的Pixel 6a设备上,执行文件创建/访问/删除操作时,Logcat偶尔会输出以下日志:

MediaProvider: Permission to access file storage/emulated/0/Documents/com.myappname/myfilename is denied

相关背景

  • 问题出在自定义Cordova插件调用的Java代码中,早期测试未出现该问题,目前仅在部分难以复现的场景下触发
  • 每次失败都会输出上述日志,已排除上层Javascript及Cordova中间层的问题
  • 存在偶发现象:有时能成功创建读取文件,但删除失败;甚至无法通过系统文件应用将文件移入回收站,但后续又能正常操作
  • 曾尝试在连续请求间增加延迟(怀疑文件缓冲),问题未解决
  • 怀疑与分区存储有关,但无法解释时好时坏的现象

AndroidManifest.xml 配置

...
<uses-sdk
    android:minSdkVersion="24"
    android:targetSdkVersion="33" />
...
<uses-permission android:name="ANDROID.PERMISSION.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="ANDROID.PERMISSION.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="ANDROID.PERMISSION.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
...

OfflineDataFile.java 代码

public class OfflineDataFile {  
    
    private final String filePath;

    public OfflineDataFile(Context context) {
        String pkg = context.getPackageName();
        String fileDir = pkg+".OfflineData";
        filePath = "/Documents/" + fileDir;
    }

    private File getAppDir() throws IOException {
        String state = Environment.getExternalStorageState();
        if ( Environment.MEDIA_MOUNTED.equals( state ) ) {
                return new File( Environment.getExternalStorageDirectory() + filePath );
        } else {
            throw new IOException("Could not mount storage");
        }
    }

    public void write(String filename, byte[] data) throws IOException {
        File appDirectory = getAppDir();

        if ( !appDirectory.exists() ) {
            appDirectory.mkdirs();
        }

        File dataFile = new File( appDirectory, filename );
        dataFile.createNewFile();

        FileOutputStream fos = new FileOutputStream(dataFile);
        fos.write(data);
        fos.close();
    }

    public byte[] read(String filename, Integer size) throws IOException {
            
        File appDirectory = getAppDir();

        if ( !appDirectory.exists() ) {
            throw new IOException("Data directory does not exist");
        }

        File dataFile = new File( appDirectory, filename );
        InputStream input = new FileInputStream(dataFile);
        byte [] data = OfflineDataUtil.readInputStream(input, size);
        input.close();
        return data;
    }

    protected void remove(String filename) {
        File appDirectory;

        try {
            appDirectory = getAppDir();
        } catch (IOException e) {           
            return;
        }

        if ( !appDirectory.exists() ) {
            return;
        }

        File dataFile = new File( appDirectory, filename );

        if ( !dataFile.exists() ) {
            return;
        }

        dataFile.delete();
    }

}

问题分析与解决方案

核心问题原因

  1. 分区存储权限限制:Android 13(API 33)下,Documents目录属于公共存储区域,直接通过File API操作该目录下的自定义子目录不符合分区存储规则。即使申请了WRITE_EXTERNAL_STORAGE,API 30+版本中该权限仅针对媒体文件,无法操作公共目录下的非媒体文件。
  2. 权限声明错误:Manifest中部分权限的包名大小写错误(如ANDROID.PERMISSION.READ_EXTERNAL_STORAGE应为android.permission.READ_EXTERNAL_STORAGE),导致这些权限未被正确授予。
  3. 文件操作未适配MediaStore:公共存储区域的文件操作需通过MediaStore API或SAF(存储访问框架),直接用File API会出现权限不稳定的情况,这就是时好时坏的原因——系统MediaProvider对公共目录的访问管控存在偶发的状态不一致。

具体修复步骤

1. 修正Manifest权限配置

删除重复权限,修正大小写错误,针对API 33调整权限:

<uses-sdk
    android:minSdkVersion="24"
    android:targetSdkVersion="33" />

<!-- 针对API 29-30的存储权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32"/>

<!-- API 33+的媒体权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>

<!-- 移除无用的MANAGE_DOCUMENTS权限,该权限仅用于文件管理器类应用 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

2. 更换文件存储路径

不要在公共Documents目录下创建自定义子目录,改用应用专属外部存储目录,这部分目录无需额外权限,且符合分区存储规则:
修改OfflineDataFile的构造方法和getAppDir方法:

public class OfflineDataFile {  
    
    private final File appDir;

    public OfflineDataFile(Context context) {
        // 获取应用专属外部存储的Documents目录,无需权限
        appDir = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), context.getPackageName() + ".OfflineData");
    }

    private File getAppDir() throws IOException {
        String state = Environment.getExternalStorageState();
        if ( Environment.MEDIA_MOUNTED.equals( state ) ) {
                return appDir;
        } else {
            throw new IOException("Could not mount storage");
        }
    }

    // 其余write/read/remove方法保持不变
}

应用专属外部存储目录路径为storage/emulated/0/Android/data/[你的包名]/files/Documents/[子目录],系统会自动管理该目录的权限,应用可自由读写删除。

3. 优化文件操作的异常处理

原remove方法直接捕获异常返回,无日志记录,建议添加日志便于排查:

protected void remove(String filename) {
    File appDirectory;

    try {
        appDirectory = getAppDir();
    } catch (IOException e) {           
        Log.e("OfflineDataFile", "Failed to get app directory", e);
        return;
    }

    if ( !appDirectory.exists() ) {
        Log.w("OfflineDataFile", "App directory does not exist");
        return;
    }

    File dataFile = new File( appDirectory, filename );

    if ( !dataFile.exists() ) {
        Log.w("OfflineDataFile", "File does not exist: " + filename);
        return;
    }

    if (!dataFile.delete()) {
        Log.e("OfflineDataFile", "Failed to delete file: " + filename);
    }
}

4. 若必须使用公共Documents目录

如果业务要求必须将文件放在公共Documents目录,需使用SAF(存储访问框架)让用户手动授权目录访问权限,示例代码如下:

// 启动SAF选择目录
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_CODE_PICK_DIR);

// 在onActivityResult中获取权限
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_CODE_PICK_DIR && resultCode == RESULT_OK) {
        Uri treeUri = data.getData();
        // 持久化权限,重启后依然有效
        getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
        // 后续通过DocumentFile操作该目录下的文件
        DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);
        // 创建子目录
        DocumentFile appDir = pickedDir.createDirectory(context.getPackageName() + ".OfflineData");
        // 创建文件
        DocumentFile dataFile = appDir.createFile("application/octet-stream", filename);
        // 写入数据
        OutputStream os = getContentResolver().openOutputStream(dataFile.getUri());
        os.write(data);
        os.close();
    }
}

这种方式需要用户手动授权,是Android 13下操作公共目录的唯一合规方式。


内容的提问来源于stack exchange,提问作者Lee

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 04:09:51