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(); } }
问题分析与解决方案
核心问题原因
- 分区存储权限限制:Android 13(API 33)下,
Documents目录属于公共存储区域,直接通过FileAPI操作该目录下的自定义子目录不符合分区存储规则。即使申请了WRITE_EXTERNAL_STORAGE,API 30+版本中该权限仅针对媒体文件,无法操作公共目录下的非媒体文件。 - 权限声明错误:Manifest中部分权限的包名大小写错误(如
ANDROID.PERMISSION.READ_EXTERNAL_STORAGE应为android.permission.READ_EXTERNAL_STORAGE),导致这些权限未被正确授予。 - 文件操作未适配MediaStore:公共存储区域的文件操作需通过
MediaStoreAPI或SAF(存储访问框架),直接用FileAPI会出现权限不稳定的情况,这就是时好时坏的原因——系统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
相关产品推荐
相关产品推荐

