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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.07 09:52:38