Android 11+重命名同名文件后出现FileNotFoundException权限异常
Android 11+ 文件替换后权限拒绝问题解决方案
问题本质
Android 11+ 引入的存储沙箱机制中,应用通过路径直接访问外部存储文件时,系统会基于文件的MediaStore记录或应用自身的文件创建关联授予权限。当外部文件管理器替换同名文件(如测试场景中把data2.aux改回data.aux),新文件的inode与旧文件完全不同,应用之前的权限关联仅针对旧文件,导致系统判定应用无权限访问新文件,触发EACCES异常。
你之前用MediaScanner解决删除重建问题的原因是:扫描后系统移除了旧文件的MediaStore记录,应用重新创建文件时会生成新的记录并建立权限关联;但文件替换场景下,旧记录仍指向原文件inode,新文件未被应用“认领”,因此扫描无效。
合规解决方案(无需MANAGE_EXTERNAL_STORAGE)
方案1:采用存储访问框架(SAF)获取持久化目录权限
这是谷歌推荐的合规方案,完全适配Android 11+存储政策,能彻底解决文件替换后的权限问题。核心思路是让用户主动授权应用访问目标目录,应用通过SAF API而非路径直接操作文件。
步骤1:请求用户授权目标目录
private static final int REQUEST_CODE_PICK_DIR = 1001; // 触发目录选择界面 private void requestWorkDirPermission() { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); // 可选:引导用户直接进入Documents目录 intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, MediaStore.Downloads.EXTERNAL_CONTENT_URI); startActivityForResult(intent, REQUEST_CODE_PICK_DIR); }
步骤2:处理授权结果并持久化权限
@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); // 将Uri保存到SharedPreferences,供后续启动使用 SharedPreferences prefs = getSharedPreferences("AppStoragePrefs", MODE_PRIVATE); prefs.edit().putString("target_work_dir_uri", treeUri.toString()).apply(); } }
步骤3:通过SAF读写data.aux文件
private void readDataAuxFile() { SharedPreferences prefs = getSharedPreferences("AppStoragePrefs", MODE_PRIVATE); String treeUriStr = prefs.getString("target_work_dir_uri", null); if (treeUriStr == null) { // 未授权,引导用户重新授权 requestWorkDirPermission(); return; } Uri treeUri = Uri.parse(treeUriStr); DocumentFile rootDir = DocumentFile.fromTreeUri(this, treeUri); if (rootDir == null || !rootDir.exists()) { // 目录不存在,提示用户 return; } // 查找目标文件 DocumentFile dataFile = rootDir.findFile("data.aux"); if (dataFile == null || !dataFile.exists()) { // 文件不存在,创建新文件 dataFile = rootDir.createFile("application/octet-stream", "data.aux"); } // 读取文件内容 try (InputStream is = getContentResolver().openInputStream(dataFile.getUri())) { DataInputStream dis = new DataInputStream(is); // 处理文件内容逻辑 // ... } catch (IOException e) { System.err.println("读取文件异常: " + e.getMessage()); } }
方案2:访问失败时通过ContentResolver重试(临时兼容方案)
如果不想立刻切换到SAF,可在FileNotFoundException触发时,通过ContentResolver查询文件的Uri,再通过Uri打开输入流:
try { DataInputStream fis = new DataInputStream(new FileInputStream(dataFile)); // 正常处理逻辑 } catch (FileNotFoundException e) { // 尝试通过ContentResolver查询文件 Uri fileUri = MediaStore.Files.getContentUri("external"); String[] projection = {MediaStore.Files.FileColumns._ID}; String selection = MediaStore.Files.FileColumns.DATA + " = ?"; String[] selectionArgs = {dataFile.getAbsolutePath()}; Cursor cursor = getContentResolver().query(fileUri, projection, selection, selectionArgs, null); if (cursor != null && cursor.moveToFirst()) { long id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)); Uri targetUri = ContentUris.withAppendedId(MediaStore.Files.getContentUri("external"), id); try (InputStream is = getContentResolver().openInputStream(targetUri)) { DataInputStream dis = new DataInputStream(is); // 重新处理文件内容 } catch (IOException ex) { ex.printStackTrace(); } cursor.close(); } }
注意:此方案依赖MediaStore索引的及时性,若文件刚被替换还未完成索引,仍可能失败,仅作为临时过渡方案。
关键说明
- 放弃路径直接访问:Android 11+后,外部存储的路径访问权限仅对应用自身创建的文件有效,外部修改的文件必须通过SAF或ContentResolver访问,这是合规的核心要求。
- SAF的优势:用户明确授权目录,符合隐私政策,不受文件重命名、替换操作影响,无需申请特殊权限,完全满足跨设备复制文件的需求(用户只需在新设备上授权相同目录,复制文件后即可访问)。
内容的提问来源于stack exchange,提问作者aarelovich
相关产品推荐
相关产品推荐

