使用ACTION_OPEN_DOCUMENT_TREE返回Uri创建Excel文件遇FileNotFoundException求助
问题根源分析
嘿,你遇到的这个报错其实是个很常见的SAF(存储访问框架)使用误区:ACTION_OPEN_DOCUMENT_TREE返回的Uri是内容URI(以content://开头),根本不是本地文件系统的路径!
你直接把这个URI拼起来生成File对象,然后用FileOutputStream去写,肯定会失败——File类只认识本地文件系统的路径(比如/storage/emulated/0/Documents/这种),完全处理不了SAF的内容URI,这就是为什么会抛出FileNotFoundException。
而你用Activity.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)时能正常运行,是因为这个方法返回的是APP私有存储的本地路径,属于传统文件系统范畴,File类自然能搞定。
解决方案:用SAF专属API操作文件
要正确处理SAF返回的目录URI,你得用Android提供的DocumentFile和ContentResolverAPI来创建、读写文件,下面是修改后的完整代码:
1. 重构onActivityResult方法
不再把URI转成File,而是用DocumentFile.fromTreeUri()获取用户选择的目录,然后检查目标Excel文件是否存在:
@Override protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { Log.d(TAG, "onActivityResult: called"); super.onActivityResult(requestCode, resultCode, data); if (resultCode == Activity.RESULT_OK && requestCode == 2) { Log.d(TAG, "onActivityResult: treeUri = " + data.getData()); Uri treeUri = data.getData(); // 获取用户选择的目录对应的DocumentFile对象 DocumentFile selectedDir = DocumentFile.fromTreeUri(this, treeUri); if (selectedDir == null) { Toast.makeText(this, "无法访问所选目录", Toast.LENGTH_SHORT).show(); return; } // 检查目录下是否已有test.xlsx文件 DocumentFile excelFile = selectedDir.findFile("test.xlsx"); if (excelFile != null && excelFile.exists()) { updateExistingExcelFile(excelFile); } else { createNewExcelFile(selectedDir); } } }
2. 修改createNewExcelFile方法
用ContentResolver打开文件输出流,替代原来的FileOutputStream:
private void createNewExcelFile(DocumentFile parentDir) { Log.d(TAG, "createNewExcelFile: called"); Workbook workbook = new HSSFWorkbook(); Cell cell; Sheet sheet; sheet = workbook.createSheet("Name of sheet"); Row row = sheet.createRow(0); cell = row.createCell(0); cell.setCellValue("Name"); cell = row.createCell(1); cell.setCellValue("Number"); sheet.setColumnWidth(0, (10 * 200)); sheet.setColumnWidth(1, (10 * 200)); // 在选中的目录下创建新的Excel文件,注意MIME类型匹配 // HSSF(.xls)用application/vnd.ms-excel,XSSF(.xlsx)用application/vnd.openxmlformats-officedocument.spreadsheetml.sheet DocumentFile newExcelFile = parentDir.createFile("application/vnd.ms-excel", "test.xlsx"); if (newExcelFile == null) { Toast.makeText(this, "创建文件失败", Toast.LENGTH_SHORT).show(); return; } // 通过ContentResolver获取输出流,用try-with-resources自动关闭流 try (OutputStream outputStream = getContentResolver().openOutputStream(newExcelFile.getUri())) { workbook.write(outputStream); Toast.makeText(this, "Created", Toast.LENGTH_LONG).show(); } catch (IOException e) { Log.e(TAG, "createNewExcelFile: ", e); Toast.makeText(this, "写入文件失败", Toast.LENGTH_SHORT).show(); } }
3. 适配updateExistingExcelFile方法(可选)
如果需要更新已有文件,同样要用SAF API读写,不能用File相关方法:
private void updateExistingExcelFile(DocumentFile excelFile) { try (InputStream inputStream = getContentResolver().openInputStream(excelFile.getUri()); // 用WorkbookFactory兼容xls和xlsx格式 Workbook workbook = WorkbookFactory.create(inputStream); OutputStream outputStream = getContentResolver().openOutputStream(excelFile.getUri())) { // 这里写更新Excel的逻辑,比如新增一行数据 Sheet sheet = workbook.getSheetAt(0); Row newRow = sheet.createRow(sheet.getLastRowNum() + 1); newRow.createCell(0).setCellValue("New Entry"); newRow.createCell(1).setCellValue("98765"); workbook.write(outputStream); Toast.makeText(this, "文件已更新", Toast.LENGTH_LONG).show(); } catch (IOException e) { Log.e(TAG, "updateExistingExcelFile: ", e); Toast.makeText(this, "更新文件失败", Toast.LENGTH_SHORT).show(); } }
额外注意事项
- 持久化权限:如果需要后续不用再让用户选择目录就能访问这个文件夹,可以调用
getContentResolver().takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION)获取持久权限。 - MIME类型对应:根据你生成的Excel格式选择正确的MIME类型,避免创建出无法识别的文件。
- 资源泄漏:一定要用try-with-resources语法或者手动关闭流,避免出现IO资源泄漏问题。
内容的提问来源于stack exchange,提问作者René Jörg Spies
相关产品推荐
相关产品推荐

