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

Android下载文件报open failed: EPERM权限异常排查

Android URL下载文件报EPERM权限异常修复

问题复现场景

实现网络URL下载文件到本地存储功能,已动态申请存储权限且系统提示授权成功,目标保存路径为/storage/emulated/0/Movies/RoveDashCam/Photos/2022_0103_124619_001.JPG,运行时抛出如下异常:

java.io.FileNotFoundException: /storage/emulated/0/Movies/RoveDashCam/Photos/2022_0103_124619_001.JPG: open failed: EPERM (Operation not permitted)

原核心下载实现代码:

private String downloadFile(String MY_URL) {
    int count;
    try {
        URL url = new URL(MY_URL);
        URLConnection connection = url.openConnection();
        connection.connect();
        long startTime = System.currentTimeMillis();

        int lengthOfFile = connection.getContentLength();
        InputStream input = new BufferedInputStream(url.openStream(), 8192);
        fileName = MY_URL.substring(MY_URL.lastIndexOf('/') + 1, MY_URL.length());
        File outputFile = new File(directory, fileName);
        OutputStream output = new FileOutputStream(outputFile);

        byte data[] = new byte[1024];
        long total = 0;

        while ((count = input.read(data)) != -1) {
            total += count;
            int status = (int) ((total * 100) / lengthOfFile);
            output.write(data, 0, count);

            handler.post(new Runnable() {
                @Override
                public void run() {
                    progress_circular_id.setProgress(status);
                    textview_progress_status_id.setText(String.valueOf(status) + "%");
                    if (status == 100) {
                        dialog.dismiss();
                    }
                }
            });
        }

        output.flush();
        output.close();
        input.close();
        return "Downloaded at: " + directory.getPath() + fileName;
    } catch (IOException e) {
        Log.e("Abhan", "Error: " + e.getLocalizedMessage());
        runOnUiThread(new Runnable() {
            public void run() {
                hideProgressDialog();
            }
        });
    }
    hideProgressDialog();
    return "Downloading Error";
}

问题根因

  • Android 10(API 29)及以上版本默认启用分区存储机制,应用即使获取了READ/WRITE_EXTERNAL_STORAGE权限,也无法直接通过File类随意访问外部存储公共目录下的自定义路径,直接创建FileOutputStream写入会被系统拦截抛出EPERM异常。
  • 原代码未做目标父目录存在性校验,若/storage/emulated/0/Movies/RoveDashCam/Photos/目录未提前创建,也会触发文件写入失败。
  • 若应用targetSdkVersion≥33(Android 13),WRITE_EXTERNAL_STORAGE权限已被系统废弃,即使动态申请也不会获得授权,直接访问文件路径的旧逻辑完全失效。
  • 原代码中更新UI使用的status变量为方法内局部变量,直接在匿名Runnable中访问会触发Java编译错误,需要做可访问性处理。

修复方案

推荐方案:使用MediaStore API写入公共目录(全版本兼容)

不要硬编码外部存储绝对路径,通过系统提供的MediaStore接口创建媒体文件,可直接指定保存到Movies公共目录下的自定义子目录,Android 10及以上版本写入应用自身创建的媒体文件无需申请任何存储权限,系统会自动创建对应父目录。
核心实现代码:

// 先在子线程执行下载逻辑
private String downloadFile(String fileUrl) {
    int count;
    InputStream input = null;
    OutputStream output = null;
    try {
        URL url = new URL(fileUrl);
        URLConnection connection = url.openConnection();
        connection.connect();
        int lengthOfFile = connection.getContentLength();
        input = new BufferedInputStream(url.openStream(), 8192);
        String fileName = fileUrl.substring(fileUrl.lastIndexOf('/') + 1);

        // 构造图片文件的ContentValues
        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
        // 指定保存到Movies目录下的自定义子目录,系统自动创建
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES + "/RoveDashCam/Photos");
        } else {
            // Android 9及以下旧版本兼容,提前创建目录
            File oldDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES), "RoveDashCam/Photos");
            if (!oldDir.exists()) oldDir.mkdirs();
            File oldFile = new File(oldDir, fileName);
            output = new FileOutputStream(oldFile);
        }

        Uri imageUri = null;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            imageUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
            if (imageUri != null) {
                output = getContentResolver().openOutputStream(imageUri);
            }
        }

        if (output == null) return "Downloading Error";

        byte[] data = new byte[1024];
        long total = 0;
        while ((count = input.read(data)) != -1) {
            total += count;
            // 用final修饰进度值,解决匿名内部类访问局部变量的限制
            final int status = (int) ((total * 100) / lengthOfFile);
            output.write(data, 0, count);

            handler.post(new Runnable() {
                @Override
                public void run() {
                    progress_circular_id.setProgress(status);
                    textview_progress_status_id.setText(status + "%");
                    if (status == 100) {
                        dialog.dismiss();
                    }
                }
            });
        }

        output.flush();
        return "Download success";
    } catch (IOException e) {
        Log.e("Abhan", "Error: " + e.getLocalizedMessage());
        runOnUiThread(() -> hideProgressDialog());
        return "Downloading Error";
    } finally {
        // 统一在finally关流,避免异常时流泄漏
        try {
            if (output != null) output.close();
            if (input != null) input.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

临时过渡方案(不推荐)

如果暂时不想重构代码,可以在AndroidManifest.xml的<application>标签下添加配置:

android:requestLegacyExternalStorage="true"

该配置仅在Android 10系统上生效,会临时关闭分区存储允许直接用File类访问路径,Android 11及以上版本系统会自动忽略该配置,无法解决高版本系统的权限问题,仅适合临时兼容使用。

权限配置注意事项

  • 若应用targetSdkVersion≥33,删除清单文件中WRITE_EXTERNAL_STORAGE权限声明,若需要读取其他应用创建的图片/视频文件,按需申请READ_MEDIA_IMAGES、READ_MEDIA_VIDEO权限即可。
  • Android 9及以下版本,仍然需要动态申请WRITE_EXTERNAL_STORAGE权限才能写入公共存储目录。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 07:03:16